Skip to content

Commit ed7145c

Browse files
committed
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.
1 parent 64550fb commit ed7145c

6 files changed

Lines changed: 52 additions & 7 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Data flow on a chat request:
5050

5151
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.
5252

53-
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`.
53+
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.
5454
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.
5555
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.
5656
4. **Effort ↔ fields mapping** (kimi-cli `llm.py` / `kosong/chat_provider/kimi.py`):

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-kimi-full",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"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.",
55
"license": "MIT",
66
"repository": {

src/constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
// secret. scope `kimi-code` is what routes the issued JWT to K2.6.
99

1010
export const KIMI_CLI_VERSION = "1.35.0"
11-
export const USER_AGENT = `KimiCodeCLI/${KIMI_CLI_VERSION}`
11+
// Upstream: research/kimi-cli/src/kimi_cli/constant.py get_user_agent() →
12+
// f"KimiCLI/{get_version()}". This must match verbatim — Moonshot's
13+
// `kimi-for-coding` backend 403s on any other UA prefix
14+
// ("access_terminated_error: only available for Coding Agents").
15+
export const USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`
1216

1317
export const OAUTH_HOST = "https://auth.kimi.com"
1418
export const OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`

src/headers.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,27 @@ function ascii(value: string): string {
3333
return value.replace(/[^\x20-\x7e]/g, "?")
3434
}
3535

36-
/** Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request. */
36+
/**
37+
* Builds the 7 X-Msh-* / UA headers kimi-cli sends on every request.
38+
*
39+
* Values mirror research/kimi-cli/src/kimi_cli/auth/oauth.py → _common_headers
40+
* and _device_model. Deviations cause Moonshot's backend to 403 with
41+
* "access_terminated_error: Kimi For Coding is currently only available for
42+
* Coding Agents". Node equivalents:
43+
* - platform.system() → os.type() ("Linux"/"Darwin"/"Windows_NT")
44+
* - platform.release() → os.release()
45+
* - platform.machine() → os.machine?.() (Node 20+ "x86_64"; NOT os.arch() which says "x64")
46+
* - platform.version() → os.version() (kernel build string on Linux)
47+
*/
3748
export function kimiHeaders(): Record<string, string> {
49+
const machine = os.machine?.() || os.arch()
3850
return {
3951
"User-Agent": USER_AGENT,
4052
"X-Msh-Platform": "kimi_cli",
4153
"X-Msh-Version": KIMI_CLI_VERSION,
4254
"X-Msh-Device-Name": ascii(os.hostname() || "unknown"),
43-
"X-Msh-Device-Model": ascii(os.machine?.() || os.arch()),
55+
"X-Msh-Device-Model": ascii(`${os.type()} ${os.release()} ${machine}`),
4456
"X-Msh-Device-Id": getDeviceId(),
45-
"X-Msh-Os-Version": ascii(`${os.type()} ${os.release()}`),
57+
"X-Msh-Os-Version": ascii(os.version?.() || `${os.type()} ${os.release()}`),
4658
}
4759
}

test/constants.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ test("KIMI_CLI_VERSION is a non-empty semver", () => {
1010
})
1111

1212
test("USER_AGENT embeds KIMI_CLI_VERSION", () => {
13-
expect(C.USER_AGENT).toBe(`KimiCodeCLI/${C.KIMI_CLI_VERSION}`)
13+
// Must be `KimiCLI/<version>` verbatim — Moonshot's backend 403s on any
14+
// other UA prefix ("access_terminated_error"). See upstream
15+
// research/kimi-cli/src/kimi_cli/constant.py → get_user_agent.
16+
expect(C.USER_AGENT).toBe(`KimiCLI/${C.KIMI_CLI_VERSION}`)
1417
})
1518

1619
test("OAuth constants match upstream kimi-cli exactly", () => {

test/headers.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,29 @@ test("Device id is also present and matches in the headers map", () => {
5858
const h = kimiHeaders()
5959
expect(h["X-Msh-Device-Id"]).toBe(getDeviceId())
6060
})
61+
62+
// Regression guard: prior to v1.0.3 we were sending `X-Msh-Device-Model =
63+
// <arch>` and `X-Msh-Os-Version = <type release>`. That didn't match kimi-cli
64+
// (which uses `platform.system() + release() + machine()` for the model and
65+
// `platform.version()` — the kernel build string — for the os version) and
66+
// caused Moonshot to 403 every request from this plugin with
67+
// "access_terminated_error". Keep these shape-asserts strict so a future
68+
// "cleanup" of headers.ts can't silently regress the fingerprint.
69+
test("X-Msh-Device-Model matches kimi-cli _device_model() shape (system release machine)", () => {
70+
const h = kimiHeaders()
71+
const sys = os.type()
72+
const rel = os.release()
73+
const mach = os.machine?.() || os.arch()
74+
expect(h["X-Msh-Device-Model"]).toBe(`${sys} ${rel} ${mach}`)
75+
// Must contain whitespace-separated release and machine — i.e. more than a
76+
// bare arch string.
77+
expect(h["X-Msh-Device-Model"]).toContain(" ")
78+
})
79+
80+
test("X-Msh-Os-Version matches os.version() (kernel build string on Linux)", () => {
81+
const h = kimiHeaders()
82+
// `os.version()` exists in Node 13.13+ — be lenient if missing.
83+
if (typeof os.version === "function") {
84+
expect(h["X-Msh-Os-Version"]).toBe(os.version())
85+
}
86+
})

0 commit comments

Comments
 (0)