diff --git a/acp_adapter/session.py b/acp_adapter/session.py index c40553f267268..c300aa8cd9cd5 100644 --- a/acp_adapter/session.py +++ b/acp_adapter/session.py @@ -582,7 +582,7 @@ def _make_agent( default_model = "" config_provider = None if isinstance(model_cfg, dict): - default_model = str(model_cfg.get("default") or default_model) + default_model = str(model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or default_model) config_provider = model_cfg.get("provider") elif isinstance(model_cfg, str) and model_cfg.strip(): default_model = model_cfg.strip() diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 18197ae309e8d..ce119e96c1c60 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -1683,7 +1683,7 @@ def _read_main_model() -> str: if isinstance(model_cfg, str) and model_cfg.strip(): return model_cfg.strip() if isinstance(model_cfg, dict): - default = model_cfg.get("default", "") + default = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" if isinstance(default, str) and default.strip(): return default.strip() except Exception: @@ -2026,7 +2026,7 @@ def _try_azure_foundry( return None, None final_model = _normalize_resolved_model( - model or str(model_cfg.get("default") or ""), + model or str(model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or ""), "azure-foundry", ) if not final_model: diff --git a/cli.py b/cli.py index c05c361a7c05a..026864d1b9c3f 100644 --- a/cli.py +++ b/cli.py @@ -3002,7 +3002,7 @@ def __init__( # authoritative. This avoids conflicts in multi-agent setups where # env vars would stomp each other. _model_config = CLI_CONFIG.get("model", {}) - _config_model = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "") + _config_model = (_model_config.get("default") or _model_config.get("name") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "") _DEFAULT_CONFIG_MODEL = "" self.model = model or _config_model or _DEFAULT_CONFIG_MODEL # Auto-detect model from local server if still on default diff --git a/cron/scheduler.py b/cron/scheduler.py index a51ade8efe651..3005958e9c99d 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1499,7 +1499,7 @@ def _run_job_impl(job: dict) -> tuple[bool, str, str, Optional[str]]: if isinstance(_model_cfg, str): model = _model_cfg elif isinstance(_model_cfg, dict): - model = _model_cfg.get("default", model) + model = _model_cfg.get("default") or _model_cfg.get("name") or _model_cfg.get("model") or model except Exception as e: logger.warning("Job '%s': failed to load config.yaml, using defaults: %s", job_id, e) diff --git a/gateway/run.py b/gateway/run.py index a2e41c6090f8b..8fa8471ea6153 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1455,7 +1455,7 @@ def _resolve_gateway_model(config: dict | None = None) -> str: if isinstance(model_cfg, str): return model_cfg elif isinstance(model_cfg, dict): - return model_cfg.get("default") or model_cfg.get("model") or "" + return model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" return "" @@ -8353,7 +8353,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g if isinstance(_model_cfg, str): _hyg_model = _model_cfg elif isinstance(_model_cfg, dict): - _hyg_model = _model_cfg.get("default") or _model_cfg.get("model") or _hyg_model + _hyg_model = _model_cfg.get("default") or _model_cfg.get("name") or _model_cfg.get("model") or _hyg_model # Read explicit context_length override from model config # (same as run_agent.py lines 995-1005) _raw_ctx = _model_cfg.get("context_length") @@ -10177,7 +10177,7 @@ async def _handle_model_command(self, event: MessageEvent) -> Optional[str]: if cfg: model_cfg = cfg.get("model", {}) if isinstance(model_cfg, dict): - current_model = model_cfg.get("default", "") + current_model = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" current_provider = model_cfg.get("provider", current_provider) current_base_url = model_cfg.get("base_url", "") user_provs = cfg.get("providers") diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index dbc486e87b104..cfc7be6e63e25 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -595,7 +595,7 @@ def run_doctor(args): model_section = cfg.get("model") or {} provider_raw = (model_section.get("provider") or "").strip() provider = provider_raw.lower() - default_model = (model_section.get("default") or model_section.get("model") or "").strip() + default_model = (model_section.get("default") or model_section.get("name") or model_section.get("model") or "").strip() known_providers: set = set() try: diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index c29ef19775ca1..0eb8353b7f9a5 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -132,7 +132,7 @@ def _get_model_and_provider(config: dict) -> tuple[str, str]: """Extract model and provider from config.""" model_cfg = config.get("model", "") if isinstance(model_cfg, dict): - model = model_cfg.get("default") or model_cfg.get("model") or model_cfg.get("name") or "(not set)" + model = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "(not set)" provider = model_cfg.get("provider") or "(auto)" elif isinstance(model_cfg, str): model = model_cfg or "(not set)" diff --git a/hermes_cli/fallback_cmd.py b/hermes_cli/fallback_cmd.py index 09142ea99eaff..010e5d13d48ba 100644 --- a/hermes_cli/fallback_cmd.py +++ b/hermes_cli/fallback_cmd.py @@ -62,7 +62,7 @@ def _extract_fallback_from_model_cfg(model_cfg: Any) -> Optional[Dict[str, Any]] return None provider = (model_cfg.get("provider") or "").strip() # The picker writes the selected model to ``model.default``. - model = (model_cfg.get("default") or model_cfg.get("model") or "").strip() + model = (model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "").strip() if not provider or not model: return None entry: Dict[str, Any] = {"provider": provider, "model": model} @@ -137,7 +137,7 @@ def _describe_primary(config: Dict[str, Any]) -> Optional[str]: model_cfg = config.get("model") if isinstance(model_cfg, dict): provider = (model_cfg.get("provider") or "?").strip() or "?" - model = (model_cfg.get("default") or model_cfg.get("model") or "?").strip() or "?" + model = (model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "?").strip() or "?" return f"{model} (via {provider})" if isinstance(model_cfg, str) and model_cfg.strip(): return model_cfg.strip() diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 5cf32d1c847c7..93d63e5040dcb 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -87,7 +87,7 @@ def load_picker_context() -> ConfigContext: cfg = load_config() model_cfg = cfg.get("model", {}) if isinstance(model_cfg, dict): - current_model = model_cfg.get("default", model_cfg.get("name", "")) or "" + current_model = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" current_provider = model_cfg.get("provider", "") or "" current_base_url = model_cfg.get("base_url", "") or "" else: diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 82df98f395da0..e3297dade8e76 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -506,7 +506,7 @@ def _has_any_provider_configured() -> bool: cfg = load_config() model_cfg = cfg.get("model") if isinstance(model_cfg, dict): - _model_name = (model_cfg.get("default") or "").strip() + _model_name = (model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "").strip() elif isinstance(model_cfg, str): _model_name = model_cfg.strip() else: @@ -2125,7 +2125,7 @@ def select_provider_and_model(args=None): config = load_config() current_model = config.get("model") if isinstance(current_model, dict): - current_model = current_model.get("default", "") + current_model = current_model.get("default") or current_model.get("name") or current_model.get("model") or "" current_model = current_model or "(not set)" # Read effective provider the same way the CLI does at startup: diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index b79644f67061f..31393505f03f8 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -239,7 +239,7 @@ def _run_agent( if isinstance(model_cfg, str): cfg_model = model_cfg else: - cfg_model = model_cfg.get("default") or model_cfg.get("model") or "" + cfg_model = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" env_model = os.getenv("HERMES_INFERENCE_MODEL", "").strip() effective_model = (model or "").strip() or env_model or cfg_model diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index ec315c7fdb1ba..1946cce7c6d9b 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -465,7 +465,7 @@ def _read_config_model(profile_dir: Path) -> tuple: if isinstance(model_cfg, str): return model_cfg, None if isinstance(model_cfg, dict): - return model_cfg.get("default") or model_cfg.get("model"), model_cfg.get("provider") + return model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model"), model_cfg.get("provider") return None, None except Exception: return None, None diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index c40316e02ccfc..8fb99bfc28eac 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -225,7 +225,7 @@ def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str: if configured_mode and _provider_supports_explicit_api_mode("copilot", configured_provider): return configured_mode - model_name = str(model_cfg.get("default") or "").strip() + model_name = str(model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "").strip() if not model_name: return "chat_completions" @@ -302,7 +302,7 @@ def _resolve_runtime_from_pool_entry( # longer matches the model actually being used — the bug that caused # opencode-zen /v1 to be stripped for chat_completions requests when # config.default was still a Claude model. - effective_model = (target_model or model_cfg.get("default") or "") + effective_model = (target_model or model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "") base_url = (getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) or "").rstrip("/") api_key = getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "") api_mode = "chat_completions" @@ -929,7 +929,7 @@ def _resolve_azure_foundry_runtime( # against them returns 400 "The requested operation is unsupported." # Upgrade api_mode when the model name matches, unless the user has # explicitly chosen anthropic_messages (Anthropic-style endpoint). - effective_model = str(target_model or model_cfg.get("default") or "").strip() + effective_model = str(target_model or model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "").strip() if effective_model and cfg_api_mode != "anthropic_messages": try: from hermes_cli.models import azure_foundry_model_api_mode @@ -1570,7 +1570,7 @@ def resolve_runtime_provider( # Dual-path routing: Claude models use AnthropicBedrock SDK for full # feature parity (prompt caching, thinking budgets, adaptive thinking). # Non-Claude models use the Converse API for multi-model support. - _current_model = str(model_cfg.get("default") or "").strip() + _current_model = str(model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "").strip() if is_anthropic_bedrock_model(_current_model): # Claude on Bedrock → AnthropicBedrock SDK → anthropic_messages path runtime = { @@ -1630,7 +1630,7 @@ def resolve_runtime_provider( # from base_url for chat_completions models and 404'ing. # Refs #16878. from hermes_cli.models import opencode_model_api_mode - _effective = target_model or model_cfg.get("default", "") + _effective = target_model or model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" api_mode = opencode_model_api_mode(provider, _effective) elif configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider): api_mode = configured_mode diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 1af23f3b9cd52..51c72c1d2ab64 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2720,7 +2720,7 @@ def _get_section_config_summary(config: dict, section_key: str) -> Optional[str] if isinstance(model, str) and model.strip(): return model.strip() if isinstance(model, dict): - return str(model.get("default") or model.get("model") or "configured") + return str(model.get("default") or model.get("name") or model.get("model") or "configured") return "configured" elif section_key == "terminal": diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index d8d7996b868e0..746278a54e431 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -853,7 +853,7 @@ def _normalize_config_for_web(config: Dict[str, Any]) -> Dict[str, Any]: if isinstance(model_val, dict): # Extract context_length before flattening the dict ctx_len = model_val.get("context_length", 0) - config["model"] = model_val.get("default", model_val.get("name", "")) + config["model"] = model_val.get("default") or model_val.get("name") or model_val.get("model") or "" config["model_context_length"] = ctx_len if isinstance(ctx_len, int) else 0 else: config["model_context_length"] = 0 @@ -901,7 +901,7 @@ def get_model_info(): # Extract model name and provider from the config if isinstance(model_cfg, dict): - model_name = model_cfg.get("default", model_cfg.get("name", "")) + model_name = model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or "" provider = model_cfg.get("provider", "") base_url = model_cfg.get("base_url", "") config_ctx = model_cfg.get("context_length") @@ -1038,7 +1038,7 @@ def get_auxiliary_models(): if isinstance(model_cfg, dict): main = { "provider": str(model_cfg.get("provider", "") or ""), - "model": str(model_cfg.get("default", model_cfg.get("name", "")) or ""), + "model": str(model_cfg.get("default") or model_cfg.get("name") or model_cfg.get("model") or ""), } else: main = {"provider": "", "model": str(model_cfg) if model_cfg else ""} diff --git a/tests/cli/test_cli_provider_resolution.py b/tests/cli/test_cli_provider_resolution.py index e71226da53f1b..58ece99e53b50 100644 --- a/tests/cli/test_cli_provider_resolution.py +++ b/tests/cli/test_cli_provider_resolution.py @@ -381,6 +381,43 @@ def _runtime_resolve(**kwargs): assert shell.model != "should-be-ignored" +def test_cli_uses_legacy_model_name_for_named_custom_provider(monkeypatch): + """Legacy/custom-provider configs may use model.name instead of model.default. + + Regression test for #34500: the CLI must pass that configured model through + to AIAgent so OpenAI-compatible custom backends do not receive model="". + """ + cli = _import_cli() + + monkeypatch.setitem(cli.CLI_CONFIG, "model", { + "name": "claude-sonnet-4-20250514", + "provider": "my-litellm", + }) + + def _runtime_resolve(**kwargs): + return { + "provider": "my-litellm", + "api_mode": "chat_completions", + "base_url": "http://litellm-proxy:4000/v1", + "api_key": "test-key", + "source": "config/custom_providers", + } + + class _DummyAgent: + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + + monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve) + monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc)) + monkeypatch.setattr(cli, "AIAgent", _DummyAgent) + + shell = cli.HermesCLI(compact=True, max_turns=1) + + assert shell.model == "claude-sonnet-4-20250514" + assert shell._init_agent() is True + assert shell.agent.kwargs["model"] == "claude-sonnet-4-20250514" + + def test_codex_config_model_not_replaced_by_normalization(monkeypatch): """When the user sets model.default in config.yaml to a specific codex model, _normalize_model_for_provider must NOT replace it with the latest diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 67e5864473863..b32a4cb5cb37d 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -786,7 +786,7 @@ def _resolve_model() -> str: return env m = _load_cfg().get("model", "") if isinstance(m, dict): - return str(m.get("default", "") or "").strip() + return str(m.get("default") or m.get("name") or m.get("model") or "").strip() if isinstance(m, str) and m: return m.strip() return "anthropic/claude-sonnet-4"