hermes - 💡(How to fix) Fix kimi-coding provider returns HTTP 404 on every request — double /v1 in Anthropic Messages URL [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Every request through the kimi-coding (and kimi-coding-cn) provider fails with HTTP 404 because the resolved base URL keeps its trailing /v1, and the Anthropic SDK then appends its own /v1/messages — producing https://api.kimi.com/coding/v1/v1/messages (double /v1). Kimi For Coding's nginx returns 404 for that path. The correct path is /coding/v1/messages (single /v1), which works fine via curl with the same key and UA.

The fix pattern already exists in the codebase for OpenCode providers; kimi-coding / kimi-coding-cn simply need to be added to the same allow-list (or, better, the allow-list dropped entirely).

Error Message

ERROR root: API call failed after 3 retries.

Root Cause

File: hermes_cli/runtime_provider.py, lines ~281–286 (the post-resolve /v1-strip block).

Two facts collide:

  1. The credential-pool entry for kimi-coding stores base_url = https://api.kimi.com/coding/v1 (the OpenAI-wire form). This is what auth.json carries today and matches PROVIDER_REGISTRY["kimi-coding"].inference_base_url after _resolve_kimi_base_url() rewrites the legacy api.moonshot.ai/v1 to the Kimi Coding Plan endpoint.
  2. _detect_api_mode_for_url() (hermes_cli/runtime_provider.py:84) correctly classifies api.kimi.com/coding as api_mode = anthropic_messages, so requests are routed through build_anthropic_client() (agent/anthropic_adapter.py:514). The Anthropic SDK then prepends its own /v1/messages to whatever base_url it is given.

Net result:

https://api.kimi.com/coding/v1   +   /v1/messages
= https://api.kimi.com/coding/v1/v1/messages   →   HTTP 404
  (resource_not_found_error, from Kimi's own JSON 404 handler)

The codebase already contains the exact fix pattern for this class of bug — but only for OpenCode. From hermes_cli/runtime_provider.py ~L281–L286:

# OpenCode base URLs end with /v1 for OpenAI-compatible models, but
# the Anthropic SDK prepends its own /v1/messages to the base_url.
# Strip the trailing /v1 so the SDK constructs the correct path
# (e.g. https://opencode.ai/zen/go/v1/messages instead of
# .../v1/v1/messages).
if api_mode == "anthropic_messages" and provider in (
    "opencode-zen", "opencode-go"
):
    base_url = re.sub(r"/v1/?$", "", base_url)

kimi-coding and kimi-coding-cn were never added to that allow-list, even though they have the same OpenAI-wire-base + Anthropic-wire-protocol shape. The Anthropic-native branch a few lines above (~L247–L249) does strip /v1, but only when provider == "anthropic", so the kimi-coding else-branch falls straight through.

Why it surfaced now: the regression became user-visible after the "feat: provider modules — ProviderProfile ABC, 33 providers, fetch_models, transport single-path" refactor (commit 20a4f79ed, re-applied as abce1a5d0). My best guess is that before that refactor some kimi-coding requests still went through the OpenAI client (/chat/completions path); after it, every turn flows through build_anthropic_client() because api_mode = anthropic_messages is honoured uniformly.

Fix Action

Fixed

Code Example

WARNING run_agent: API call failed (attempt 1/3)
  error_type=NotFoundError
  provider=kimi-coding
  base_url=https://api.kimi.com/coding/v1
  model=kimi-k2.6
  summary=HTTP 404: The requested resource was not found
ERROR root: API call failed after 3 retries.
  HTTP 404: The requested resource was not found
  | provider=kimi-coding model=kimi-k2.6

---

POST https://api.kimi.com/coding/v1/v1/messages   ← double /v1
  User-Agent: claude-code/0.1.0
  x-api-key: sk-kimi-...

---

$ curl -sS -o /dev/null -w "%{http_code}\n" \
    -X POST "https://api.kimi.com/coding/v1/messages" \
    -H "x-api-key: sk-kimi-..." \
    -H "anthropic-version: 2023-06-01" \
    -H "User-Agent: claude-code/0.1.0" \
    -H "Content-Type: application/json" \
    -d '{"model":"kimi-k2.6","max_tokens":20,"messages":[{"role":"user","content":"hi"}]}'
200

---

Report     https://paste.rs/WNtPT
agent.log  https://paste.rs/uyjON

---

https://api.kimi.com/coding/v1   +   /v1/messages
= https://api.kimi.com/coding/v1/v1/messages   →   HTTP 404
  (resource_not_found_error, from Kimi's own JSON 404 handler)

---

# OpenCode base URLs end with /v1 for OpenAI-compatible models, but
# the Anthropic SDK prepends its own /v1/messages to the base_url.
# Strip the trailing /v1 so the SDK constructs the correct path
# (e.g. https://opencode.ai/zen/go/v1/messages instead of
# .../v1/v1/messages).
if api_mode == "anthropic_messages" and provider in (
    "opencode-zen", "opencode-go"
):
    base_url = re.sub(r"/v1/?$", "", base_url)

---

# hermes_cli/runtime_provider.py, around line 285

-    if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"):
+    if api_mode == "anthropic_messages" and provider in (
+        "opencode-zen", "opencode-go", "kimi-coding", "kimi-coding-cn",
+    ):
         base_url = re.sub(r"/v1/?$", "", base_url)

---

def test_kimi_coding_anthropic_base_url_strips_v1(...):
    # Credential pool stores https://api.kimi.com/coding/v1 (OpenAI-wire form).
    # api_mode is auto-detected as anthropic_messages.
    # Resolved base_url MUST NOT end in /v1, otherwise the Anthropic SDK
    # builds .../v1/v1/messages → 404.
    rt = resolve_runtime_provider(requested="kimi-coding", target_model="kimi-k2.6")
    assert rt["api_mode"] == "anthropic_messages"
    assert rt["base_url"] == "https://api.kimi.com/coding"
RAW_BUFFERClick to expand / collapse

Summary

Every request through the kimi-coding (and kimi-coding-cn) provider fails with HTTP 404 because the resolved base URL keeps its trailing /v1, and the Anthropic SDK then appends its own /v1/messages — producing https://api.kimi.com/coding/v1/v1/messages (double /v1). Kimi For Coding's nginx returns 404 for that path. The correct path is /coding/v1/messages (single /v1), which works fine via curl with the same key and UA.

The fix pattern already exists in the codebase for OpenCode providers; kimi-coding / kimi-coding-cn simply need to be added to the same allow-list (or, better, the allow-list dropped entirely).

Steps to Reproduce

  1. Have a valid Kimi For Coding API key (sk-kimi-…) configured for the kimi-coding provider in ~/.hermes/auth.json. Credential-pool entries end up with base_url = https://api.kimi.com/coding/v1, which is what _resolve_kimi_base_url() produces for any sk-kimi- key.
  2. Select kimi-coding + a kimi-coding model: hermes model → "Kimi / Moonshot" → kimi-k2.6.
  3. Send any message, e.g. hermes -z "say hi" --provider kimi-coding -m kimi-k2.6. (TUI / hermes chat / gateway flows all reproduce identically.)
  4. Every turn fails after 3 retries with HTTP 404.

Expected Behavior

Hermes sends the request to https://api.kimi.com/coding/v1/messages and Kimi For Coding responds normally, the way it does for the kimi CLI, Claude Code, Roo Code, etc.

Actual Behavior

Every turn fails with API call failed after 3 retries: HTTP 404: The requested resource was not found.

From agent.log:

WARNING run_agent: API call failed (attempt 1/3)
  error_type=NotFoundError
  provider=kimi-coding
  base_url=https://api.kimi.com/coding/v1
  model=kimi-k2.6
  summary=HTTP 404: The requested resource was not found
ERROR root: API call failed after 3 retries.
  HTTP 404: The requested resource was not found
  | provider=kimi-coding model=kimi-k2.6

httpx-level wire capture of the actual outbound request:

POST https://api.kimi.com/coding/v1/v1/messages   ← double /v1
  User-Agent: claude-code/0.1.0
  x-api-key: sk-kimi-...

Direct curl confirming the correct endpoint works with the same UA + key (single /v1):

$ curl -sS -o /dev/null -w "%{http_code}\n" \
    -X POST "https://api.kimi.com/coding/v1/messages" \
    -H "x-api-key: sk-kimi-..." \
    -H "anthropic-version: 2023-06-01" \
    -H "User-Agent: claude-code/0.1.0" \
    -H "Content-Type: application/json" \
    -d '{"model":"kimi-k2.6","max_tokens":20,"messages":[{"role":"user","content":"hi"}]}'
200

After applying the proposed fix below, the wire capture becomes the correct single-/v1 form and the same prompt round-trips cleanly.

Affected Component

Inference / provider routing — specifically hermes_cli/runtime_provider.py, the base-URL normalization for api_mode = anthropic_messages providers.

Reproduces in hermes -z, hermes chat, and hermes --tui. Not gateway/platform-specific.

Debug Report

Report     https://paste.rs/WNtPT
agent.log  https://paste.rs/uyjON

(Auto-deletes 6h after upload — happy to regenerate via hermes debug share on request.)

Environment

  • OS: Ubuntu 22.04.1 LTS (WSL2 on Windows 11, kernel 6.6.87.2-microsoft-standard-WSL2)
  • Python: 3.11.14 (the bundled hermes venv interpreter; system python3 is 3.10.12 but hermes ships its own)
  • Hermes: v0.12.0 (2026.4.30), git HEAD a494a614d (fix(tui): avoid main-screen scrollback reset loops)
  • OpenAI SDK: 2.32.0

Root Cause Analysis

File: hermes_cli/runtime_provider.py, lines ~281–286 (the post-resolve /v1-strip block).

Two facts collide:

  1. The credential-pool entry for kimi-coding stores base_url = https://api.kimi.com/coding/v1 (the OpenAI-wire form). This is what auth.json carries today and matches PROVIDER_REGISTRY["kimi-coding"].inference_base_url after _resolve_kimi_base_url() rewrites the legacy api.moonshot.ai/v1 to the Kimi Coding Plan endpoint.
  2. _detect_api_mode_for_url() (hermes_cli/runtime_provider.py:84) correctly classifies api.kimi.com/coding as api_mode = anthropic_messages, so requests are routed through build_anthropic_client() (agent/anthropic_adapter.py:514). The Anthropic SDK then prepends its own /v1/messages to whatever base_url it is given.

Net result:

https://api.kimi.com/coding/v1   +   /v1/messages
= https://api.kimi.com/coding/v1/v1/messages   →   HTTP 404
  (resource_not_found_error, from Kimi's own JSON 404 handler)

The codebase already contains the exact fix pattern for this class of bug — but only for OpenCode. From hermes_cli/runtime_provider.py ~L281–L286:

# OpenCode base URLs end with /v1 for OpenAI-compatible models, but
# the Anthropic SDK prepends its own /v1/messages to the base_url.
# Strip the trailing /v1 so the SDK constructs the correct path
# (e.g. https://opencode.ai/zen/go/v1/messages instead of
# .../v1/v1/messages).
if api_mode == "anthropic_messages" and provider in (
    "opencode-zen", "opencode-go"
):
    base_url = re.sub(r"/v1/?$", "", base_url)

kimi-coding and kimi-coding-cn were never added to that allow-list, even though they have the same OpenAI-wire-base + Anthropic-wire-protocol shape. The Anthropic-native branch a few lines above (~L247–L249) does strip /v1, but only when provider == "anthropic", so the kimi-coding else-branch falls straight through.

Why it surfaced now: the regression became user-visible after the "feat: provider modules — ProviderProfile ABC, 33 providers, fetch_models, transport single-path" refactor (commit 20a4f79ed, re-applied as abce1a5d0). My best guess is that before that refactor some kimi-coding requests still went through the OpenAI client (/chat/completions path); after it, every turn flows through build_anthropic_client() because api_mode = anthropic_messages is honoured uniformly.

Proposed Fix

Targeted one-liner — extend the existing allow-list:

# hermes_cli/runtime_provider.py, around line 285

-    if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"):
+    if api_mode == "anthropic_messages" and provider in (
+        "opencode-zen", "opencode-go", "kimi-coding", "kimi-coding-cn",
+    ):
         base_url = re.sub(r"/v1/?$", "", base_url)

A more defensive variant would drop the provider allow-list entirely and always strip a trailing /v1 whenever api_mode == "anthropic_messages", since the Anthropic SDK always appends /v1/messages regardless of provider. That would also pre-empt the same bug for any future Anthropic-wire third-party added with a /v1-suffixed base URL.

Suggested regression test (somewhere under tests/hermes_cli/, alongside the existing kimi-coding-cn coverage in test_doctor.py):

def test_kimi_coding_anthropic_base_url_strips_v1(...):
    # Credential pool stores https://api.kimi.com/coding/v1 (OpenAI-wire form).
    # api_mode is auto-detected as anthropic_messages.
    # Resolved base_url MUST NOT end in /v1, otherwise the Anthropic SDK
    # builds .../v1/v1/messages → 404.
    rt = resolve_runtime_provider(requested="kimi-coding", target_model="kimi-k2.6")
    assert rt["api_mode"] == "anthropic_messages"
    assert rt["base_url"] == "https://api.kimi.com/coding"

Happy to send a PR with either form — let me know which you prefer.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING