diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 4dad57dd54..148e30bfbc 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -33,21 +33,15 @@ def _get_model_config() -> Dict[str, Any]: return {} -def _get_configured_api_mode(model_cfg: Optional[Dict[str, Any]] = None) -> Optional[str]: - """Return an optional API mode override from env or config. +_VALID_API_MODES = {"chat_completions", "codex_responses"} - Allows custom OpenAI-compatible endpoints to opt into codex_responses - mode via HERMES_API_MODE env var or model.api_mode in config.yaml, - without requiring the OpenAI Codex OAuth provider path. - """ - candidate = os.getenv("HERMES_API_MODE", "").strip().lower() - if not candidate: - cfg = model_cfg if isinstance(model_cfg, dict) else _get_model_config() - raw = cfg.get("api_mode") - if isinstance(raw, str): - candidate = raw.strip().lower() - if candidate in {"chat_completions", "codex_responses"}: - return candidate + +def _parse_api_mode(raw: Any) -> Optional[str]: + """Validate an api_mode value from config. Returns None if invalid.""" + if isinstance(raw, str): + normalized = raw.strip().lower() + if normalized in _VALID_API_MODES: + return normalized return None @@ -104,11 +98,15 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An menu_key = f"custom:{name_norm}" if requested_norm not in {name_norm, menu_key}: continue - return { + result = { "name": name.strip(), "base_url": base_url.strip(), "api_key": str(entry.get("api_key", "") or "").strip(), } + api_mode = _parse_api_mode(entry.get("api_mode")) + if api_mode: + result["api_mode"] = api_mode + return result return None @@ -139,7 +137,7 @@ def _resolve_named_custom_runtime( return { "provider": "openrouter", - "api_mode": _get_configured_api_mode() or "chat_completions", + "api_mode": custom_provider.get("api_mode", "chat_completions"), "base_url": base_url, "api_key": api_key, "source": f"custom_provider:{custom_provider.get('name', requested_provider)}", @@ -208,11 +206,10 @@ def _resolve_openrouter_runtime( ) source = "explicit" if (explicit_api_key or explicit_base_url) else "env/config" - api_mode = _get_configured_api_mode(model_cfg) or "chat_completions" return { "provider": "openrouter", - "api_mode": api_mode, + "api_mode": _parse_api_mode(model_cfg.get("api_mode")) or "chat_completions", "base_url": base_url, "api_key": api_key, "source": source, diff --git a/tests/test_runtime_provider_resolution.py b/tests/test_runtime_provider_resolution.py index e72b8308bd..690c577697 100644 --- a/tests/test_runtime_provider_resolution.py +++ b/tests/test_runtime_provider_resolution.py @@ -328,10 +328,10 @@ def test_resolve_requested_provider_precedence(monkeypatch): assert rp.resolve_requested_provider() == "auto" -# ── api_mode override tests ───────────────────────────────────────────── +# ── api_mode config override tests ────────────────────────────────────── -def test_custom_endpoint_api_mode_from_config(monkeypatch): +def test_model_config_api_mode(monkeypatch): """model.api_mode in config.yaml should override the default chat_completions.""" monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter") monkeypatch.setattr( @@ -346,7 +346,6 @@ def test_custom_endpoint_api_mode_from_config(monkeypatch): monkeypatch.setenv("OPENAI_API_KEY", "test-key") monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False) monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - monkeypatch.delenv("HERMES_API_MODE", raising=False) resolved = rp.resolve_runtime_provider(requested="custom") @@ -354,28 +353,12 @@ def test_custom_endpoint_api_mode_from_config(monkeypatch): assert resolved["base_url"] == "http://127.0.0.1:9208/v1" -def test_env_api_mode_overrides_config(monkeypatch): - """HERMES_API_MODE env var takes precedence over config.""" - monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter") - monkeypatch.setattr(rp, "_get_model_config", lambda: {"api_mode": "chat_completions"}) - monkeypatch.setenv("OPENAI_BASE_URL", "http://127.0.0.1:9208/v1") - monkeypatch.setenv("OPENAI_API_KEY", "test-key") - monkeypatch.setenv("HERMES_API_MODE", "codex_responses") - monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False) - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) - - resolved = rp.resolve_runtime_provider(requested="custom") - - assert resolved["api_mode"] == "codex_responses" - - def test_invalid_api_mode_ignored(monkeypatch): """Invalid api_mode values should fall back to chat_completions.""" monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "openrouter") monkeypatch.setattr(rp, "_get_model_config", lambda: {"api_mode": "bogus_mode"}) monkeypatch.setenv("OPENAI_BASE_URL", "http://127.0.0.1:9208/v1") monkeypatch.setenv("OPENAI_API_KEY", "test-key") - monkeypatch.delenv("HERMES_API_MODE", raising=False) monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False) monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) @@ -384,16 +367,37 @@ def test_invalid_api_mode_ignored(monkeypatch): assert resolved["api_mode"] == "chat_completions" -def test_named_custom_provider_respects_api_mode(monkeypatch): - """Named custom providers should also pick up api_mode overrides.""" +def test_named_custom_provider_api_mode(monkeypatch): + """custom_providers entries with api_mode should use it.""" monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server") monkeypatch.setattr( rp, "_get_named_custom_provider", - lambda p: {"name": "my-server", "base_url": "http://localhost:8000/v1", "api_key": "sk-test"}, + lambda p: { + "name": "my-server", + "base_url": "http://localhost:8000/v1", + "api_key": "sk-test", + "api_mode": "codex_responses", + }, ) - monkeypatch.setenv("HERMES_API_MODE", "codex_responses") resolved = rp.resolve_runtime_provider(requested="my-server") assert resolved["api_mode"] == "codex_responses" assert resolved["base_url"] == "http://localhost:8000/v1" + + +def test_named_custom_provider_without_api_mode_defaults(monkeypatch): + """custom_providers entries without api_mode should default to chat_completions.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server") + monkeypatch.setattr( + rp, "_get_named_custom_provider", + lambda p: { + "name": "my-server", + "base_url": "http://localhost:8000/v1", + "api_key": "sk-test", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="my-server") + + assert resolved["api_mode"] == "chat_completions"