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:
|
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
|
"""Strip ``provider/`` or ``provider:`` only when the prefix matches the target provider, so
|
||||||
ids aren't mangled while ``zai/glm-5.1`` is repaired for ``zai``. ``custom`` is a bucket, not a
|
arbitrary slash-bearing ids aren't mangled while ``zai/glm-5.1`` is repaired for ``zai``. The colon
|
||||||
vendor: an alias resolving to it (``ollama``) may be a real LiteLLM-style routing prefix, so only a
|
form is Hermes's own ``provider:model`` switch syntax (``-m openai-codex:gpt-5.6-sol``); left intact
|
||||||
literal ``custom/`` prefix is redundant there."""
|
it reaches the wire and the Codex backend rejects it with HTTP 400 (#64787). Only the FIRST separator
|
||||||
if "/" not in model_name:
|
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
|
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():
|
if not prefix.strip() or not remainder.strip():
|
||||||
return model_name
|
return model_name
|
||||||
normalized_target = _normalize_provider_alias(target_provider)
|
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:
|
if provider in _STRIP_VENDOR_ONLY_PROVIDERS:
|
||||||
stripped = _strip_matching_provider_prefix(name, provider)
|
stripped = _strip_matching_provider_prefix(name, provider)
|
||||||
if stripped == name and name.startswith("openai/"):
|
if stripped == name and name.startswith(("openai/", "openai:")):
|
||||||
return name.split("/", 1)[1] # openai-codex maps openai/gpt-5.4 -> gpt-5.4
|
return name[len("openai/"):] # openai-codex maps openai/gpt-5.4 and openai:gpt-5.4 -> gpt-5.4
|
||||||
return stripped
|
return stripped
|
||||||
|
|
||||||
if provider == "deepseek":
|
if provider == "deepseek":
|
||||||
|
|||||||
@@ -198,3 +198,25 @@ class TestIssue78796NvidiaPrefixRepair:
|
|||||||
== "anthropic/claude-sonnet-4.6"
|
== "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