diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 137e52d19..9f3b3cae9 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -168,7 +168,10 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = { id="kimi-coding", name="Kimi / Moonshot", auth_type="api_key", - inference_base_url="https://api.kimi.com/coding", + # Legacy platform.moonshot.ai keys use this endpoint (OpenAI-compat). + # sk-kimi- (Kimi Code) keys are auto-redirected to api.kimi.com/coding + # by _resolve_kimi_base_url() below. + inference_base_url="https://api.moonshot.ai/v1", api_key_env_vars=("KIMI_API_KEY", "KIMI_CODING_API_KEY"), base_url_env_var="KIMI_BASE_URL", ), @@ -340,10 +343,16 @@ def get_anthropic_key() -> str: # ============================================================================= # Kimi Code (kimi.com/code) issues keys prefixed "sk-kimi-" that only work -# on api.kimi.com/coding/v1. Legacy keys from platform.moonshot.ai work on -# api.moonshot.ai/v1 (the default). Auto-detect when user hasn't set +# on api.kimi.com/coding. Legacy keys from platform.moonshot.ai work on +# api.moonshot.ai/v1 (the old default). Auto-detect when user hasn't set # KIMI_BASE_URL explicitly. -KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1" +# +# Note: the base URL intentionally has NO /v1 suffix. The /coding endpoint +# speaks the Anthropic Messages protocol, and the anthropic SDK appends +# "/v1/messages" internally — so "/coding" + SDK suffix → "/coding/v1/messages" +# (the correct target). Using "/coding/v1" here would produce +# "/coding/v1/v1/messages" (a 404). +KIMI_CODE_BASE_URL = "https://api.kimi.com/coding" def _resolve_kimi_base_url(api_key: str, default_url: str, env_override: str) -> str: diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index e16f0bf5e..2fc50321f 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -943,18 +943,22 @@ def run_doctor(args): try: import httpx _base = os.getenv(_base_env, "") if _base_env else "" - # Auto-detect Kimi Code keys (sk-kimi-) → api.kimi.com + # Auto-detect Kimi Code keys (sk-kimi-) → api.kimi.com/coding/v1 + # (OpenAI-compat surface, which exposes /models for health check). if not _base and _key.startswith("sk-kimi-"): _base = "https://api.kimi.com/coding/v1" - # Anthropic-compat endpoints (/anthropic) don't support /models. - # Rewrite to the OpenAI-compat /v1 surface for health checks. + # Anthropic-compat endpoints (/anthropic, api.kimi.com/coding + # with no /v1) don't support /models. Rewrite to the OpenAI-compat + # /v1 surface for health checks. if _base and _base.rstrip("/").endswith("/anthropic"): from agent.auxiliary_client import _to_openai_base_url _base = _to_openai_base_url(_base) + if base_url_host_matches(_base, "api.kimi.com") and _base.rstrip("/").endswith("/coding"): + _base = _base.rstrip("/") + "/v1" _url = (_base.rstrip("/") + "/models") if _base else _default_url _headers = {"Authorization": f"Bearer {_key}"} if base_url_host_matches(_base, "api.kimi.com"): - _headers["User-Agent"] = "KimiCLI/1.30.0" + _headers["User-Agent"] = "claude-code/0.1.0" _resp = httpx.get( _url, headers=_headers, diff --git a/tests/hermes_cli/test_api_key_providers.py b/tests/hermes_cli/test_api_key_providers.py index 2af003ea0..7d0674b03 100644 --- a/tests/hermes_cli/test_api_key_providers.py +++ b/tests/hermes_cli/test_api_key_providers.py @@ -71,7 +71,11 @@ class TestProviderRegistry: def test_kimi_env_vars(self): pconfig = PROVIDER_REGISTRY["kimi-coding"] - assert pconfig.api_key_env_vars == ("KIMI_API_KEY",) + # KIMI_API_KEY is the primary env var; KIMI_CODING_API_KEY is a + # secondary fallback for Kimi Code sk-kimi- keys so users don't + # have to overload the same variable. + assert "KIMI_API_KEY" in pconfig.api_key_env_vars + assert "KIMI_CODING_API_KEY" in pconfig.api_key_env_vars assert pconfig.base_url_env_var == "KIMI_BASE_URL" def test_minimax_env_vars(self):