From b2ba351380001ba5766764700dfd570dbd843a2c Mon Sep 17 00:00:00 2001 From: Teknium Date: Tue, 21 Apr 2026 19:42:33 -0700 Subject: [PATCH] fix(kimi): reconcile sk-kimi- routing with Anthropic SDK URL semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-ups after salvaging xiaoqiang243's kimi-for-coding patches: - KIMI_CODE_BASE_URL: drop trailing /v1 (was /coding/v1). The /coding endpoint speaks Anthropic Messages, and the Anthropic SDK appends /v1/messages internally. /coding/v1 + SDK suffix produced /coding/v1/v1/messages (a 404). /coding + SDK suffix now yields /coding/v1/messages correctly. - kimi-coding ProviderConfig: keep legacy default api.moonshot.ai/v1 so non-sk-kimi- moonshot keys still authenticate. sk-kimi- keys are already redirected to api.kimi.com/coding via _resolve_kimi_base_url. - doctor.py: update Kimi UA to claude-code/0.1.0 (was KimiCLI/1.30.0) and rewrite /coding base URLs to /coding/v1 for the /models health check (Anthropic surface has no /models). - test_kimi_env_vars: accept KIMI_CODING_API_KEY as a secondary env var. E2E verified: sk-kimi- → https://api.kimi.com/coding/v1/messages (Anthropic) sk- → https://api.moonshot.ai/v1/chat/completions (OpenAI) UA: claude-code/0.1.0, x-api-key: --- hermes_cli/auth.py | 17 +++++++++++++---- hermes_cli/doctor.py | 12 ++++++++---- tests/hermes_cli/test_api_key_providers.py | 6 +++++- 3 files changed, 26 insertions(+), 9 deletions(-) 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):