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:
teknium1
2026-09-18 23:39:44 -07:00
committed by Teknium
parent c38f7f6d4c
commit 50e262a74c
2 changed files with 34 additions and 8 deletions

View File

@@ -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":

View File

@@ -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