Release v1.0.3: fix header fingerprint (UA, Device-Model, Os-Version)

Plugin was silently 403ing every live request with
'access_terminated_error: only available for Coding Agents'. Discovered by
capturing what kimi-cli actually sends on the wire and diffing.

Three fingerprint bugs, each sufficient on its own to trigger the 403:

* User-Agent was 'KimiCodeCLI/<v>'; upstream sends 'KimiCLI/<v>'.
  (research/kimi-cli/src/kimi_cli/constant.py::get_user_agent)
* X-Msh-Device-Model was 'x86_64'; upstream sends
  '{system} {release} {machine}' e.g. 'Linux 7.0.0 x86_64'.
  (research/kimi-cli/src/kimi_cli/auth/oauth.py::_device_model)
* X-Msh-Os-Version was '{type} {release}' e.g. 'Linux 7.0.0'; upstream
  sends platform.version() i.e. the kernel build string. Node equivalent
  is os.version().

Verified live against api.kimi.com/coding/v1 with a freshly-minted JWT:
200 OK and real K2.6 response after the fix; 403 before.

Locked in with regression tests in test/headers.test.ts and
test/constants.test.ts, and documented in AGENTS.md contract rule 1.
This commit is contained in:
lemon07r
2026-04-17 03:51:45 -04:00
parent 64550fb057
commit ed7145c9c9
6 changed files with 52 additions and 7 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ Data flow on a chat request:
These are the invariants that, if broken, silently degrade K2.6 → K2.5 or produce fingerprint-based throttling. Do not "clean them up" without reading the linked upstream.
1. **`X-Msh-Version` and `User-Agent` must track `kimi-cli`.** Bumping involves exactly one line in `src/constants.ts`. See upstream `research/kimi-cli/src/kimi_cli/constant.py`.
1. **`X-Msh-Version` and `User-Agent` must track `kimi-cli`.** Bumping involves exactly one line in `src/constants.ts`. See upstream `research/kimi-cli/src/kimi_cli/constant.py`. The UA prefix is `KimiCLI/` (not `KimiCodeCLI/`) — Moonshot's `kimi-for-coding` backend 403s with `access_terminated_error: only available for Coding Agents such as Kimi CLI, Claude Code, Roo Code…` on any other prefix. Likewise, `X-Msh-Device-Model` is `"{system} {release} {machine}"` (e.g. `Linux 7.0.0 x86_64`) — NOT just `{arch}` — and `X-Msh-Os-Version` is the kernel build string from `os.version()`, NOT `"{type} {release}"`. Tested live against `api.kimi.com/coding/v1` on 2026-04-17 — any of those three fields off-spec → 403.
2. **`X-Msh-Device-Id` must be stable across runs.** Never regenerate a fresh UUID at import time. `getDeviceId()` reads/writes `~/.kimi/device_id`; that path is shared with `kimi-cli` on purpose.
3. **`Authorization` header is owned by `loader.fetch`.** Anything else (opencode core, the SDK, future hooks) must be overridden. Our `loader` deletes both `authorization` and `Authorization` before setting its own.
4. **Effort ↔ fields mapping** (kimi-cli `llm.py` / `kosong/chat_provider/kimi.py`):
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "opencode-kimi-full",
"version": "1.0.2",
"version": "1.0.3",
"description": "OpenCode plugin that adds first-class support for Kimi K2.6 (kimi-for-coding) via the official Kimi OAuth device flow, matching the upstream kimi-cli 1:1.",
"license": "MIT",
"repository": {
+5 -1
View File
@@ -8,7 +8,11 @@
// secret. scope `kimi-code` is what routes the issued JWT to K2.6.
export const KIMI_CLI_VERSION = "1.35.0"
export const USER_AGENT = `KimiCodeCLI/${KIMI_CLI_VERSION}`
// Upstream: research/kimi-cli/src/kimi_cli/constant.py get_user_agent() →
// f"KimiCLI/{get_version()}". This must match verbatim — Moonshot's
// `kimi-for-coding` backend 403s on any other UA prefix
// ("access_terminated_error: only available for Coding Agents").
export const USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`
export const OAUTH_HOST = "https://auth.kimi.com"
export const OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`
+15 -3
View File
@@ -33,15 +33,27 @@ function ascii(value: string): string {
return value.replace(/[^\x20-\x7e]/g, "?")
}
/** Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request. */
/**
* Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request.
*
* Values mirror research/kimi-cli/src/kimi_cli/auth/oauth.py → _common_headers
* and _device_model. Deviations cause Moonshot's backend to 403 with
* "access_terminated_error: Kimi For Coding is currently only available for
* Coding Agents". Node equivalents:
* - platform.system() → os.type() ("Linux"/"Darwin"/"Windows_NT")
* - platform.release() → os.release()
* - platform.machine() → os.machine?.() (Node 20+ "x86_64"; NOT os.arch() which says "x64")
* - platform.version() → os.version() (kernel build string on Linux)
*/
export function kimiHeaders(): Record<string, string> {
const machine = os.machine?.() || os.arch()
return {
"User-Agent": USER_AGENT,
"X-Msh-Platform": "kimi_cli",
"X-Msh-Version": KIMI_CLI_VERSION,
"X-Msh-Device-Name": ascii(os.hostname() || "unknown"),
"X-Msh-Device-Model": ascii(os.machine?.() || os.arch()),
"X-Msh-Device-Model": ascii(`${os.type()} ${os.release()} ${machine}`),
"X-Msh-Device-Id": getDeviceId(),
"X-Msh-Os-Version": ascii(`${os.type()} ${os.release()}`),
"X-Msh-Os-Version": ascii(os.version?.() || `${os.type()} ${os.release()}`),
}
}
+4 -1
View File
@@ -10,7 +10,10 @@ test("KIMI_CLI_VERSION is a non-empty semver", () => {
})
test("USER_AGENT embeds KIMI_CLI_VERSION", () => {
expect(C.USER_AGENT).toBe(`KimiCodeCLI/${C.KIMI_CLI_VERSION}`)
// Must be `KimiCLI/<version>` verbatim — Moonshot's backend 403s on any
// other UA prefix ("access_terminated_error"). See upstream
// research/kimi-cli/src/kimi_cli/constant.py → get_user_agent.
expect(C.USER_AGENT).toBe(`KimiCLI/${C.KIMI_CLI_VERSION}`)
})
test("OAuth constants match upstream kimi-cli exactly", () => {
+26
View File
@@ -58,3 +58,29 @@ test("Device id is also present and matches in the headers map", () => {
const h = kimiHeaders()
expect(h["X-Msh-Device-Id"]).toBe(getDeviceId())
})
// Regression guard: prior to v1.0.3 we were sending `X-Msh-Device-Model =
// <arch>` and `X-Msh-Os-Version = <type release>`. That didn't match kimi-cli
// (which uses `platform.system() + release() + machine()` for the model and
// `platform.version()` — the kernel build string — for the os version) and
// caused Moonshot to 403 every request from this plugin with
// "access_terminated_error". Keep these shape-asserts strict so a future
// "cleanup" of headers.ts can't silently regress the fingerprint.
test("X-Msh-Device-Model matches kimi-cli _device_model() shape (system release machine)", () => {
const h = kimiHeaders()
const sys = os.type()
const rel = os.release()
const mach = os.machine?.() || os.arch()
expect(h["X-Msh-Device-Model"]).toBe(`${sys} ${rel} ${mach}`)
// Must contain whitespace-separated release and machine — i.e. more than a
// bare arch string.
expect(h["X-Msh-Device-Model"]).toContain(" ")
})
test("X-Msh-Os-Version matches os.version() (kernel build string on Linux)", () => {
const h = kimiHeaders()
// `os.version()` exists in Node 13.13+ — be lenient if missing.
if (typeof os.version === "function") {
expect(h["X-Msh-Os-Version"]).toBe(os.version())
}
})