mirror of
https://github.com/lemon07r/opencode-kimi-full.git
synced 2026-06-02 06:14:16 +02:00
Release v1.1.2: prefer canonical kimi-for-coding slug; drop K2.6-specific wording
All Kimi For Coding plans currently return `kimi-for-coding` from
/coding/v1/models, so there's no real K2.5 vs K2.6 differentiation to
surface. Keep the wire-rewrite safety net (in case Moonshot ever ships
a non-canonical slug), but:
- Selection now prefers the `kimi-for-coding` entry when the server
returns it, falling back to the first element only when absent.
Applies to both loader refresh and the post-login authorize callback.
- Post-login console message no longer implies K2.6-specific validation
("Authorized for Kimi For Coding" instead of "Kimi for Coding:
authorized" followed by tier speculation).
- README drops K2.5/K2.6 tier routing language that was never quite
accurate and is now moot. The model-id rewrite is described as a
safety net, not a per-tier behavior.
- Added test/plugin.test.ts coverage for the canonical-slug preference.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
An [opencode](https://opencode.ai) plugin for the **Kimi For Coding** plan.
|
||||
|
||||
This plugin authenticates the same way the official [kimi-cli](https://github.com/MoonshotAI/kimi-cli) does and mirrors its wire shape, so opencode's requests to Moonshot's `/coding` endpoint match kimi-cli byte-for-byte, including OAuth, fingerprint headers, session-scoped prompt caching, and paired thinking / reasoning-effort fields for higher fidelity. The Kimi specific extras exposed by the /coding endpoint used by kimi-cli are not implemented in opencode. Bonus side effect; users with access to Kimi K2.6 Code Preview will get access to it in opencode with this plugin.
|
||||
This plugin authenticates the same way the official [kimi-cli](https://github.com/MoonshotAI/kimi-cli) does and mirrors its wire shape, so opencode's requests to Moonshot's `/coding` endpoint match kimi-cli byte-for-byte, including OAuth, fingerprint headers, session-scoped prompt caching, and paired thinking / reasoning-effort fields for higher fidelity. The Kimi-specific extras exposed by the `/coding` endpoint used by kimi-cli are not otherwise implemented in opencode.
|
||||
|
||||
Contributor and agent documentation lives in [`AGENTS.md`](./AGENTS.md).
|
||||
|
||||
@@ -67,9 +67,9 @@ Add the plugin and a provider entry to `opencode.json` (or `~/.config/opencode/o
|
||||
Two identifiers are load-bearing:
|
||||
|
||||
- **provider id** `kimi-for-coding-oauth` — the plugin's `auth` and `chat.params` hooks match on it.
|
||||
- **model id** `kimi-for-coding` — a stable opencode-side alias. The plugin rewrites the wire `model` field to whatever `/coding/v1/models` reports for your account (e.g. `kimi-for-coding` on K2.6 tiers, `k2p5` on K2.5 tiers). Both tiers use identical config.
|
||||
- **model id** `kimi-for-coding` — a stable opencode-side alias. At login and on every token refresh the plugin queries `/coding/v1/models` and, if the server reports a different slug for your account, rewrites the wire `model` field accordingly. In practice every plan currently returns `kimi-for-coding`, so this is a safety net, not something you need to think about.
|
||||
|
||||
> **Note.** The provider id is intentionally not `kimi-for-coding`. That id is already published by [models.dev](https://models.dev) and points at a static-API-key flow that routes to K2.5. Using a distinct id keeps the two paths from colliding under a single `opencode auth login` entry.
|
||||
> **Note.** The provider id is intentionally not `kimi-for-coding`. That id is already published by [models.dev](https://models.dev) and points at a static-API-key flow using a different SDK and auth shape. Using a distinct id keeps the two paths from colliding under a single `opencode auth login` entry.
|
||||
|
||||
### Log in
|
||||
|
||||
@@ -92,13 +92,13 @@ Press **Ctrl+T** to pick a reasoning variant (`off`, `auto`, `low`, `medium`, `h
|
||||
|
||||
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 kimi-cli parity 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`. The backend routes this token to K2.6.
|
||||
**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`.
|
||||
|
||||
**How opencode does it out of the box.** `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. No Kimi-specific request extras are sent. The backend currently routes a static `sk-kimi-...` key to K2.5.
|
||||
**How opencode does it out of the box.** `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. No Kimi-specific request extras are sent, and the auth token type differs from what kimi-cli uses.
|
||||
|
||||
**What this plugin gives you.** Everything kimi-cli does, inside opencode:
|
||||
|
||||
- OAuth device flow with `scope: kimi-code`, so you land on K2.6 (if you have access).
|
||||
- OAuth device flow with `scope: kimi-code`, routed to the Kimi For Coding endpoint the same way kimi-cli does.
|
||||
- `prompt_cache_key` set to opencode's session id, for session-scoped cache reuse.
|
||||
- Paired `thinking` + `reasoning_effort` fields.
|
||||
- The seven `X-Msh-*` headers and a kimi-cli-shaped `User-Agent`.
|
||||
@@ -113,7 +113,7 @@ There are two ways to talk to Moonshot's Kimi For Coding plan today: the way kim
|
||||
|
||||
Two upstream changes would narrow the gap, but neither would make this plugin redundant:
|
||||
|
||||
- **If Moonshot routes `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 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` — the paired `reasoning_effort: low|medium|high` knob that kimi-cli exposes has no equivalent there.
|
||||
- **If Moonshot's built-in opencode path gains parity on the routing side**, opencode's built-in entry still doesn't send `prompt_cache_key` or the paired reasoning fields. Explicit session-scoped cache reuse 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` — 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 (same `X-Msh-Device-Id` and headers, kimi-cli-shaped UA) and independent refresh-token chains are unlikely to be replicated by a first-party integration.
|
||||
|
||||
</details>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencode-kimi-full",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"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": {
|
||||
|
||||
+16
-6
@@ -19,9 +19,10 @@ type OAuthAuth = {
|
||||
// created before v1.1.0; fallback to MODEL_ID and no context-length hint.
|
||||
//
|
||||
// `model_id` is the slug Moonshot actually expects on the wire for this
|
||||
// account — K2.5 tiers can see `k2p5`, K2.6 tiers see `kimi-for-coding`.
|
||||
// `context_length` is surfaced to users in the post-login config block
|
||||
// so their opencode config matches their real entitlement.
|
||||
// account. In practice every current tier returns `kimi-for-coding`, but
|
||||
// the field exists so a future server-side slug change doesn't require a
|
||||
// plugin update. `context_length` is surfaced to users in the post-login
|
||||
// config block so their opencode config matches their real entitlement.
|
||||
model_id?: string
|
||||
context_length?: number
|
||||
model_display?: string
|
||||
@@ -130,7 +131,12 @@ const plugin: Plugin = async ({ client }) => {
|
||||
}
|
||||
try {
|
||||
const models = await listModels(tokens.access_token)
|
||||
const picked = models[0]
|
||||
// Prefer the canonical `kimi-for-coding` id when the server
|
||||
// offers it; otherwise take the first entry. Mirrors kimi-cli's
|
||||
// behavior of treating `/models` as authoritative while keeping
|
||||
// a stable slug when possible, so users see the same id across
|
||||
// tiers in practice.
|
||||
const picked = models.find((m) => m.id === MODEL_ID) ?? models[0]
|
||||
if (picked) {
|
||||
discovered = {
|
||||
model_id: picked.id,
|
||||
@@ -231,7 +237,11 @@ const plugin: Plugin = async ({ client }) => {
|
||||
let discovered: Pick<OAuthAuth, "model_id" | "context_length" | "model_display"> = {}
|
||||
try {
|
||||
const models = await listModels(tokens.access_token)
|
||||
const picked = models[0]
|
||||
// Same preference as the loader: canonical id wins,
|
||||
// else first. In practice every tier currently returns
|
||||
// `kimi-for-coding`; the fallback exists only so a
|
||||
// future server change can't brick the plugin.
|
||||
const picked = models.find((m) => m.id === MODEL_ID) ?? models[0]
|
||||
if (picked) {
|
||||
discovered = {
|
||||
model_id: picked.id,
|
||||
@@ -246,7 +256,7 @@ const plugin: Plugin = async ({ client }) => {
|
||||
display: picked.display_name,
|
||||
})
|
||||
console.log(
|
||||
`\n✓ Kimi for Coding: authorized (model: ${picked.id}${
|
||||
`\n✓ Authorized for Kimi For Coding (model: ${picked.id}${
|
||||
picked.context_length ? `, context ${picked.context_length}` : ""
|
||||
})\n\nAdd this to your opencode config (~/.config/opencode/opencode.json) if you haven't already:\n\n${block}\n`,
|
||||
)
|
||||
|
||||
@@ -215,6 +215,33 @@ test("auth.loader: refreshes when expiry is within safety window", async () => {
|
||||
expect(reads).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("auth.loader: prefers the canonical MODEL_ID slug when /models returns multiple", async () => {
|
||||
// Server returns several entries; the canonical `kimi-for-coding` is not first.
|
||||
// Selection must still prefer it over the first element.
|
||||
const current = validAuth({ expires: Date.now() + REFRESH_SAFETY_WINDOW_MS / 2 })
|
||||
mock = installFetchMock((call) => {
|
||||
if (call.url.includes("/oauth/token")) {
|
||||
return { body: { access_token: "a", refresh_token: "r", token_type: "Bearer", expires_in: 900 } }
|
||||
}
|
||||
if (call.url.endsWith("/coding/v1/models")) {
|
||||
return {
|
||||
body: {
|
||||
data: [
|
||||
{ id: "some-other-slug", context_length: 100000 },
|
||||
{ id: MODEL_ID, context_length: 262144, display_name: "Kimi" },
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
return { body: { ok: true } }
|
||||
})
|
||||
const { writes, fetch: f } = await getLoaderFetch(async () => current)
|
||||
await f("https://api.kimi.com/coding/v1/chat")
|
||||
const persisted = writes[0]!.body as { model_id?: string; context_length?: number }
|
||||
expect(persisted.model_id).toBe(MODEL_ID)
|
||||
expect(persisted.context_length).toBe(262144)
|
||||
})
|
||||
|
||||
test("auth.loader: model discovery failure does not break refresh (graceful)", async () => {
|
||||
const current = validAuth({ expires: Date.now() + REFRESH_SAFETY_WINDOW_MS / 2, access: "old" })
|
||||
mock = installFetchMock((call) => {
|
||||
|
||||
Reference in New Issue
Block a user