fix: strip a matching provider: prefix like provider/ in model normalization
`hermes chat -m openai-codex:gpt-5.6-sol` reached the Codex wire as the literal
`openai-codex:gpt-5.6-sol` and the backend rejected it with HTTP 400 ("model is not
supported when using Codex with a ChatGPT account"). `parse_model_input` accepts the
`provider:model` form, but `_strip_matching_provider_prefix` only recognised `/`.
Treat the first `:` or `/` as the provider separator in the shared alias-aware helper,
so every provider that routes through it (openai-codex, copilot, zai, deepseek, custom,
the dot-to-hyphen set) gets the same behaviour; the `openai/` -> bare fallback for
openai-codex accepts `openai:` too. Only the first separator counts, so Ollama-style
tags (`qwen3:8b`) are never split on a later colon.
Fixes #64787
Co-authored-by: Maxim Sirotkin <sirotkin_me@transset.ru>
This commit is contained in:
@@ -124,13 +124,17 @@ def _normalize_provider_alias(provider_name: str) -> str:
|
||||
|
||||
|
||||
def _strip_matching_provider_prefix(model_name: str, target_provider: str) -> str:
|
||||
"""Strip ``provider/`` only when the prefix matches the target provider, so arbitrary slash-bearing
|
||||
ids aren't mangled while ``zai/glm-5.1`` is repaired for ``zai``. ``custom`` is a bucket, not a
|
||||
vendor: an alias resolving to it (``ollama``) may be a real LiteLLM-style routing prefix, so only a
|
||||
literal ``custom/`` prefix is redundant there."""
|
||||
if "/" not in model_name:
|
||||
"""Strip ``provider/`` or ``provider:`` only when the prefix matches the target provider, so
|
||||
arbitrary slash-bearing ids aren't mangled while ``zai/glm-5.1`` is repaired for ``zai``. The colon
|
||||
form is Hermes's own ``provider:model`` switch syntax (``-m openai-codex:gpt-5.6-sol``); left intact
|
||||
it reaches the wire and the Codex backend rejects it with HTTP 400 (#64787). Only the FIRST separator
|
||||
counts, so an Ollama-style ``qwen3:8b`` tag is never split on a later colon. ``custom`` is a bucket,
|
||||
not a vendor: an alias resolving to it (``ollama``) may be a real LiteLLM-style routing prefix, so
|
||||
only a literal ``custom/`` / ``custom:`` prefix is redundant there."""
|
||||
cut = min((i for i in (model_name.find("/"), model_name.find(":")) if i >= 0), default=-1)
|
||||
if cut < 0:
|
||||
return model_name
|
||||
prefix, remainder = model_name.split("/", 1)
|
||||
prefix, remainder = model_name[:cut], model_name[cut + 1:]
|
||||
if not prefix.strip() or not remainder.strip():
|
||||
return model_name
|
||||
normalized_target = _normalize_provider_alias(target_provider)
|
||||
@@ -237,8 +241,8 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
|
||||
|
||||
if provider in _STRIP_VENDOR_ONLY_PROVIDERS:
|
||||
stripped = _strip_matching_provider_prefix(name, provider)
|
||||
if stripped == name and name.startswith("openai/"):
|
||||
return name.split("/", 1)[1] # openai-codex maps openai/gpt-5.4 -> gpt-5.4
|
||||
if stripped == name and name.startswith(("openai/", "openai:")):
|
||||
return name[len("openai/"):] # openai-codex maps openai/gpt-5.4 and openai:gpt-5.4 -> gpt-5.4
|
||||
return stripped
|
||||
|
||||
if provider == "deepseek":
|
||||
|
||||
@@ -198,3 +198,25 @@ class TestIssue78796NvidiaPrefixRepair:
|
||||
== "anthropic/claude-sonnet-4.6"
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TestColonProviderPrefixIsStrippedLikeSlash:
|
||||
"""Issue #64787: ``-m openai-codex:gpt-5.6-sol`` (Hermes's own ``provider:model`` switch syntax)
|
||||
reached the Codex wire with the prefix attached and got HTTP 400. A matching ``provider:`` prefix
|
||||
must normalize exactly like ``provider/``; a later colon (Ollama tags) is never a separator."""
|
||||
|
||||
@pytest.mark.parametrize("model,provider,expected", [
|
||||
("openai-codex:gpt-5.6-sol", "openai-codex", "gpt-5.6-sol"),
|
||||
("openai:gpt-5.4", "openai-codex", "gpt-5.4"),
|
||||
("zai:glm-5.1", "zai", "glm-5.1"),
|
||||
("custom:qwen3:8b", "custom", "qwen3:8b"),
|
||||
])
|
||||
def test_matching_colon_prefix_stripped(self, model, provider, expected):
|
||||
assert normalize_model_for_provider(model, provider) == expected
|
||||
|
||||
@pytest.mark.parametrize("model,provider", [
|
||||
("qwen3:8b", "custom"), # bare Ollama tag: first colon is not a provider prefix
|
||||
("anthropic:claude-x", "openai-codex"), # non-matching prefix passes through untouched
|
||||
])
|
||||
def test_non_matching_colon_untouched(self, model, provider):
|
||||
assert normalize_model_for_provider(model, provider) == model
|
||||
|
||||
Reference in New Issue
Block a user