fix(models): resolve custom provider model ids

Map picker-prefixed custom provider selections back to their configured model IDs before validation, persistence, and API requests.

Fixes #68347
This commit is contained in:
ijevin 2026-07-21 13:46:59 +08:00 committed by Brooklyn Nicholson
parent 199f558058
commit 8ca4c745d0
2 changed files with 77 additions and 0 deletions

View file

@ -965,6 +965,38 @@ def _configured_provider_matches(
return matches
def _resolve_named_custom_model_id(
model_name: str,
target_provider: str,
custom_providers: Optional[list],
) -> str:
"""Map a picker-prefixed custom model selection to its configured ID."""
provider = str(target_provider or "").strip().lower()
if not provider.startswith("custom:") or "/" not in model_name:
return model_name
prefix, candidate = model_name.split("/", 1)
prefix = prefix.strip().lower()
candidate = candidate.strip()
if not prefix or not candidate:
return model_name
for entry in custom_providers or []:
if not isinstance(entry, dict):
continue
entry_slugs = {
custom_provider_slug(str(entry.get(key) or "")).lower()
for key in ("name", "provider_key")
if str(entry.get(key) or "").strip()
}
if provider not in entry_slugs or f"custom:{prefix}" not in entry_slugs:
continue
for model_id in _declared_model_ids(entry.get("models")):
if model_id.lower() == candidate.lower():
return model_id
return model_name
# ---------------------------------------------------------------------------
# Core model-switching pipeline
# ---------------------------------------------------------------------------
@ -1447,6 +1479,9 @@ def switch_model(
api_mode = determine_api_mode(target_provider, base_url)
# --- Normalize model name for target provider ---
new_model = _resolve_named_custom_model_id(
new_model, target_provider, custom_providers
)
new_model = normalize_model_for_provider(new_model, target_provider)
# --- Validate ---

View file

@ -283,6 +283,48 @@ def test_switch_model_accepts_explicit_named_custom_provider(monkeypatch):
assert result.api_key == "no-key-required"
def test_picker_selection_resolves_named_custom_provider_model_id(monkeypatch):
"""Picker prefixes must not leak into a named custom provider API model id."""
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
lambda **kwargs: {
"api_key": "test-key",
"base_url": "https://token.sensenova.cn/v1",
"api_mode": "chat_completions",
},
)
monkeypatch.setattr(
"hermes_cli.models.validate_requested_model",
lambda *a, **k: _MOCK_VALIDATION,
)
monkeypatch.setattr("hermes_cli.model_switch.get_model_info", lambda *a, **k: None)
monkeypatch.setattr(
"hermes_cli.model_switch.get_model_capabilities",
lambda *a, **k: None,
)
result = switch_model(
raw_input="sensenova/deepseek-v4-flash",
current_provider="openai-codex",
current_model="gpt-5.4",
explicit_provider="custom:sensenova",
user_providers={},
custom_providers=[
{
"name": "sensenova",
"base_url": "https://token.sensenova.cn/v1",
"models": [
{"id": "deepseek-v4-flash", "name": "deepseek-v4-flash"}
],
}
],
)
assert result.success is True
assert result.target_provider == "custom:sensenova"
assert result.new_model == "deepseek-v4-flash"
def test_list_groups_same_name_custom_providers_into_one_row(monkeypatch):
"""Multiple custom_providers entries sharing a name should produce one row
with all models collected, not N duplicate rows."""