From 9d47e4cb193c5a339394370235157f7be462ef45 Mon Sep 17 00:00:00 2001 From: lemon07r Date: Fri, 17 Apr 2026 01:18:36 -0400 Subject: [PATCH] Add README and AGENTS.md --- .gitignore | 1 + AGENTS.md | 113 +++++++++++++++++++++++++++++++++++++++++ README.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 35ffd58..bd901f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ research/ node_modules/ dist/ *.log +.junie/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ac18228 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +# AGENTS.md — working notes for coding agents (and humans) + +This file is the single source of truth for any AI agent (or human) modifying this repo. Read it top-to-bottom before touching code. If something you learn here contradicts what you see in the code, the **code wins** — update this file in the same commit. + +User-facing install / usage documentation lives in [`README.md`](./README.md). Do **not** duplicate it here. + +--- + +### Purpose + +One plugin, one job: make `opencode` talk to Kimi's `kimi-for-coding` endpoint **exactly the way the official `kimi-cli` does**. Everything in this repo exists to minimize drift from upstream kimi-cli. + +### The one rule that matters + +> Moonshot's backend picks the model (K2.5 vs K2.6) from the **auth token type**, not the model-name string. + +- Static `sk-kimi-...` API key → K2.5. +- OAuth JWT with `scope: kimi-code` → K2.6. + +Every design decision here follows from that: we do device-flow OAuth, we do not accept API keys, we do not let the upstream SDK attach its own Authorization header. + +### Non-goals + +- No support for K2.5 or any non-`kimi-for-coding` model. opencode already handles those via Moonshot / Baseten / Alibaba-CN / etc. +- No support for static API keys. Users who want that can use a different opencode provider entry. +- No custom SSE parser, tool-call normalizer, or message rewriter. `@ai-sdk/openai-compatible` already does SSE/`reasoning_content` correctly. + +--- + +### Architecture + +Three files, 1 job each. Do not add a fourth unless the existing three genuinely can't hold a new concern. + +| File | Responsibility | +|--------------------|--------------------------------------------------------------------------------| +| `src/constants.ts` | Pinned strings that must mirror upstream kimi-cli (version, endpoints, client id, scope). | +| `src/headers.ts` | The seven `X-Msh-*` / UA headers + the persistent `~/.kimi/device_id` file. | +| `src/oauth.ts` | Device-code start, device-code poll, refresh-token exchange. Nothing else. | +| `src/index.ts` | Plugin entry. Wires `auth` hook (login + loader) and `chat.params` hook. | + +Data flow on a chat request: + +1. opencode asks the `@ai-sdk/openai-compatible` provider for a language model. +2. Before instantiating it, opencode calls our `auth.loader`. We return `{ apiKey, fetch }`. +3. The SDK uses our `fetch` for every HTTP call (models, chat, whatever). +4. Our `fetch` calls `ensureFresh()` → maybe refreshes → sets Authorization + the seven `X-Msh-*` headers → on 401 refreshes once and retries. +5. Separately, opencode runs the `chat.params` hook and writes `thinking`, `reasoning_effort`, `prompt_cache_key` into `output.options`. opencode wraps those as `{ [providerID]: options }` and the openai-compatible SDK forwards them as top-level body fields. That is why those keys must use **exactly** the wire names (`prompt_cache_key`, `reasoning_effort`, `thinking`). + +### Contracts to keep intact + +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`. +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`): + + | Effort | `reasoning_effort` | `thinking.type` | + |----------|--------------------|-----------------| + | `off` | *(omitted)* | `"disabled"` | + | `low` | `"low"` | `"enabled"` | + | `medium` | `"medium"` | `"enabled"` | + | `high` | `"high"` | `"enabled"` | + + Do not send `thinking.type="enabled"` with no `reasoning_effort` unless the request never had one to begin with (the default "server picks" case). + +5. **`prompt_cache_key` only for `kimi-for-coding`.** Never attach it to unrelated models. The check is `input.model.id === MODEL_ID` in `chat.params`. +6. **Model id goes over the wire verbatim.** Don't strip the `kimi-` prefix — the backend expects exactly `kimi-for-coding`. +7. **Auth store is opencode's, not kimi-cli's.** We use `client.auth.get/set` against the `kimi-for-coding` provider id. Do not read/write `~/.kimi/credentials/kimi-code.json`; that's kimi-cli's file and sharing it across independent apps causes token-race bugs. + +### Working on this repo + +- **Code style:** see `tsconfig.json` (strict, `noUncheckedIndexedAccess`, ES2022). Prefer small pure functions, avoid `try`/`catch` except where we genuinely convert one error shape to another. +- **Comments:** match the existing density — only explain non-obvious upstream-parity reasoning. Do not narrate the obvious ("// refresh the token"); instead reference upstream files when the reasoning is "because kimi-cli does it that way". +- **Dependencies:** runtime deps stay at **zero**. The only dev/peer dep is `@opencode-ai/plugin` for types. +- **Git commits:** small, logical, imperative subject ("Add oauth device flow"). When committing, pass `--trailer "Co-authored-by: Junie "` if you are an AI agent. +- **Upstream research:** the `research/` directory is a read-only git-ignored pair of shallow clones (opencode + kimi-cli) for grep. Never edit files there; re-clone if you suspect drift. When citing upstream in a comment, use the `research/…` path so the reference is resolvable. +- **Version bumps:** when kimi-cli bumps, (1) pull a fresh `research/kimi-cli`, (2) update `KIMI_CLI_VERSION` in `src/constants.ts`, (3) re-diff `_kimi_default_headers()` / `oauth.py` against `src/headers.ts` and `src/oauth.ts`, (4) smoke-test with `opencode auth login kimi-for-coding` and a one-turn chat, (5) tag release. + +### What not to do + +- ❌ Don't add heuristics that look at the model id outside of `chat.params`. The `auth.loader` fetch is already scoped to this provider; the only place that needs to match on `kimi-for-coding` is the params hook. +- ❌ Don't add new header values that kimi-cli doesn't send. The fingerprint matters. +- ❌ Don't call out to other files to "share" the kimi-cli credentials. Different OAuth consumers must have independent refresh-token chains or one will invalidate the other. +- ❌ Don't introduce a build step. The plugin ships as `.ts` and opencode's bun-based loader handles it. +- ❌ Don't add tests that require real Kimi credentials and check them in. If you add offline unit tests, put them under `test/` and mock `fetch`. + +### How to verify a change + +Offline: + +```sh +bun build --target=node --no-bundle src/index.ts # syntax/type-ish check +``` + +Online (requires a real Kimi-for-coding account): + +1. `cd ~/.opencode && bun add /path/to/this/repo` +2. Paste the provider block from `README.md` into your opencode config. +3. `opencode auth login kimi-for-coding` — confirm a token lands in opencode's `auth.json` with `type: "oauth"`, a JWT `access`, and `expires` ~15 min in the future. +4. Start opencode, select `kimi-for-coding/kimi-for-coding`, and ask the model to self-identify. It should claim to be K2.6 / `kimi-for-coding`. +5. Confirm `reasoning_content` deltas render as thinking content (not assistant text). +6. In a second turn of the same session, confirm the response comes back faster (cache hit via `prompt_cache_key`). + +If any of 3–6 fails, diff `research/kimi-cli` against the contracts above. + +### House rules for AI agents + +- Read this file first. Every time. +- Don't grow the dependency footprint to "simplify" something; this plugin's value is being small and audit-able. +- When in doubt, mirror kimi-cli exactly, then comment the upstream reference. "We used to deviate, it broke" — document it here. +- Keep `README.md` user-focused and this file contributor-focused. If you catch yourself duplicating, move content here and link from the README. +- Any new rule you add here must have a real incident or a grep-verified upstream source behind it. No speculative "best practices". diff --git a/README.md b/README.md new file mode 100644 index 0000000..2dbc67b --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +## opencode-kimi-full + +An [opencode](https://opencode.ai) plugin for the **Kimi For Coding** plan. It authenticates the same way the official [`kimi` CLI](https://github.com/MoonshotAI/kimi-cli) does and mirrors its wire shape, so opencode requests to Moonshot's `/coding` endpoint match `kimi` CLI's byte-for-byte. + +Contributor and agent documentation lives in [`AGENTS.md`](./AGENTS.md). + +--- + +### Why this plugin exists + +There are two ways to talk to Moonshot's Kimi For Coding plan today: the way `kimi` CLI does it, and the way opencode does it. They target different endpoints and use different authentication. This plugin brings the `kimi` CLI approach into opencode. + +**How `kimi` CLI does it.** OAuth device-code flow against `auth.moonshot.cn` with `scope: kimi-code`, producing a short-lived JWT. Requests go to `https://api.kimi.com/coding/v1` (OpenAI-compatible) with the JWT as the bearer token, seven `X-Msh-*` fingerprint headers, a stable `~/.kimi/device_id`, and per-request extras: `prompt_cache_key` (an opt-in, session-scoped cache key) and paired `thinking.type` + `reasoning_effort` (sent together, matching `kimi` CLI). The backend routes this token to K2.6. + +**How opencode does it.** `opencode auth login` selects a Kimi For Coding provider from the catalog and prompts for a `KIMI_API_KEY` (a static `sk-kimi-...` key). The catalog entry uses `@ai-sdk/anthropic` against `api.kimi.com/coding`, which is valid since the endpoint exposes both OpenAI-compatible and Anthropic-compatible routes for third-party agents. Authentication is the static key; no Kimi-specific request extras are sent (opencode's generic plumbing has no code path for `prompt_cache_key`, the paired `thinking` + `reasoning_effort` shape, or the `X-Msh-*` headers). The backend currently routes a static `sk-kimi-...` key to K2.5. + +**What this plugin gives you.** Everything `kimi` CLI does, inside opencode. OAuth device flow with `scope: kimi-code` (so you land on K2.6), `prompt_cache_key` set to the opencode session id, paired `thinking` + `reasoning_effort`, the seven `X-Msh-*` headers and `kimi`-CLI-shaped UA, and a `~/.kimi/device_id` shared with a locally-installed `kimi` CLI. Tokens are stored in opencode's `auth.json` under a dedicated `kimi-for-coding` provider id, so the plugin and `kimi` CLI keep independent refresh-token chains and do not invalidate each other. Streaming, `reasoning_content` deltas, and tool-call schemas are handled upstream by `@ai-sdk/openai-compatible` and are not reimplemented. + +Two upstream changes would narrow the gap between the two paths. Even after both, the plugin remains a higher-fidelity alternative to opencode's built-in Kimi For Coding path: + +- If Moonshot starts routing `sk-kimi-...` keys to K2.6, opencode's built-in path reaches K2.6 too, but still without `prompt_cache_key` or the paired reasoning fields. Explicit session-scoped cache reuse via `prompt_cache_key` stays unavailable on that path (any automatic prefix caching Moonshot may do is orthogonal and would apply to both paths), and reasoning is controlled on the Anthropic route via `thinking.budget_tokens` (a token budget); the paired `reasoning_effort: low|medium|high` knob that `kimi` CLI exposes has no equivalent there. +- If opencode ships a native Kimi For Coding OAuth, the auth story converges, but the request-field gap stays until opencode's provider code emits these exact fields for `/coding`. `kimi` CLI is Moonshot's first-party client and targets the OpenAI-compatible route, so mirroring its wire shape is the lowest-risk way to stay aligned with upstream. Fingerprint parity with `kimi` CLI (same `X-Msh-Device-Id` and headers as `kimi` CLI, `kimi`-CLI-shaped UA) and independent refresh-token chains are unlikely to be replicated by a first-party integration. + +--- + +### Requirements + +- `opencode` ≥ 1.4.6 +- A Kimi account with an active **Kimi For Coding** subscription (the same plan that works with `kimi` CLI) + +### Install + +```sh +cd ~/.opencode +bun add opencode-kimi-full +``` + +From a local checkout: + +```sh +cd ~/.opencode +bun add /path/to/opencode-kimi-full +``` + +### Configure + +Add the plugin and a provider entry to `opencode.json` (or `~/.config/opencode/opencode.json`): + +```json +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["opencode-kimi-full"], + "provider": { + "kimi-for-coding": { + "name": "Kimi K2.6 (for coding)", + "npm": "@ai-sdk/openai-compatible", + "options": { + "baseURL": "https://api.kimi.com/coding/v1" + }, + "models": { + "kimi-for-coding": { + "name": "Kimi K2.6 Code Preview", + "limit": { "context": 262144, "output": 32768 }, + "reasoning": true, + "options": {} + } + } + } + } +} +``` + +Two identifiers are load-bearing and must not be renamed: + +- the **provider id** `kimi-for-coding`. The plugin's `auth` and `chat.params` hooks match on it. +- the **model id** `kimi-for-coding`. Sent to Moonshot verbatim; do not strip the `kimi-` prefix. + +### Log in + +```sh +opencode auth login kimi-for-coding +``` + +The plugin returns a verification URL and user code. After browser approval it polls the device-auth endpoint and persists tokens through opencode's `auth.json`. Access tokens have a ~15 minute TTL and refresh automatically; refresh tokens last ~30 days. + +### Use + +Select `kimi-for-coding/kimi-for-coding` in opencode. + +--- + +### Request fields in detail + +| Field | Wire shape | Purpose | +|---|---|---| +| `prompt_cache_key` | top-level body, snake_case, set to opencode's `sessionID` | Opt-in, session-scoped cache key, mirroring `kimi` CLI. | +| `thinking` + `reasoning_effort` | `thinking: { type: "enabled" \| "disabled" }` with sibling `reasoning_effort: "low" \| "medium" \| "high"` | Sent together, matching `kimi` CLI. | +| Seven `X-Msh-*` headers + UA | `User-Agent`, `X-Msh-Platform`, `X-Msh-Version`, `X-Msh-Device-Name`, `X-Msh-Device-Model`, `X-Msh-Device-Id`, `X-Msh-OS-Version` | Matches `kimi` CLI's `_kimi_default_headers()` at the pinned `KIMI_CLI_VERSION`. | +| `~/.kimi/device_id` | UUID persisted on disk, embedded in `X-Msh-Device-Id` | Sends the same `X-Msh-Device-Id` as a locally-installed `kimi` CLI. | + +Effort-to-field mapping, taken verbatim from `kimi` CLI: + +| user effort | `reasoning_effort` | `thinking.type` | +|---|---|---| +| `off` | *(omitted)* | `"disabled"` | +| `low` / `medium` / `high` | same string | `"enabled"` | + +--- + +### Files the plugin touches + +| Path | Purpose | +|---|---| +| `~/.kimi/device_id` | Stable UUID used in `X-Msh-Device-Id`. Shared with `kimi` CLI. | +| `/auth.json` | Token storage, managed by opencode through `client.auth.*`. | + +No other state is persisted. Credentials are never written to `~/.kimi/credentials/`; that path belongs to `kimi` CLI, and sharing it would cause refresh-token races between the two clients. + +### Architecture + +``` +┌────────────── opencode core ─────────────┐ +│ │ +│ auth.login ─▶ plugin.auth.authorize() │ device-code flow, poll +│ └─▶ oauth.ts │ +│ │ +│ chat ──────▶ plugin.loader() │ custom fetch that: +│ ├─▶ ensureFresh() │ • proactive refresh +│ └─▶ kimiHeaders() │ • 7 X-Msh-* headers +│ │ • 401 → force-refresh + retry +│ chat.params ─▶ plugin "chat.params" │ thinking / reasoning_effort / +│ │ prompt_cache_key +└──────────────────────────────────────────┘ +``` + +A full description of the invariants that keep this working is in [`AGENTS.md`](./AGENTS.md), under "Architecture" and "Contracts to keep intact". + +### License + +MIT. diff --git a/package.json b/package.json index 850f511..de8d553 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "opencode-kimi-for-coding", + "name": "opencode-kimi-full", "version": "0.1.0", "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",