Add README and AGENTS.md

This commit is contained in:
lemon07r
2026-04-17 01:18:36 -04:00
parent c1fc4688a4
commit 9d47e4cb19
4 changed files with 255 additions and 1 deletions
+1
View File
@@ -2,3 +2,4 @@ research/
node_modules/
dist/
*.log
.junie/
+113
View File
@@ -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 <junie@jetbrains.com>"` 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 36 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".
+140
View File
@@ -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. |
| `<opencode data>/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.
+1 -1
View File
@@ -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",