From 0d22121a0169a360d987a50a162e765b63ce0080 Mon Sep 17 00:00:00 2001 From: lemon07r Date: Sat, 18 Apr 2026 23:02:03 -0400 Subject: [PATCH] Fix invalid login config hint --- AGENTS.md | 1 + README.md | 2 +- package.json | 2 +- src/index.ts | 10 ++++++---- test/plugin.test.ts | 5 +++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e71514d..805f282 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,7 @@ These are the invariants that, if broken, silently degrade K2.6 → K2.5 or prod 7. **Auth store is opencode's, not kimi-cli's.** We use opencode's auth store for tokens under the `kimi-for-coding-oauth` 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. Also note that opencode's SDK auth schema only persists the standard oauth fields, so model discovery metadata cannot be stored there durably. 8. **Provider id must not collide with any id in the [models.dev](https://models.dev) catalog.** models.dev publishes `kimi-for-coding` (static `KIMI_API_KEY` → `@ai-sdk/anthropic` → K2.5). If we registered under that same id, `opencode auth login kimi-for-coding` would surface two methods under one entry and users picking the API-key one would silently land on K2.5. We deliberately use `kimi-for-coding-oauth` instead; `MODEL_ID` on the wire stays `kimi-for-coding` (rule 6). 9. **`src/index.ts` must have exactly one export — the default plugin function.** opencode's plugin loader (`research/opencode/packages/opencode/src/plugin/index.ts` → `getLegacyPlugins`) iterates every export of the plugin module and throws `Plugin export is not a function` if any named export is not callable. The failure mode is silent in the CLI (the provider just doesn't appear in `opencode auth login`); the error only surfaces in `~/.local/share/opencode/log/*.log`. Keep constants in `src/constants.ts` and import them in `src/index.ts` rather than re-exporting. `test/exports.test.ts` guards this. +10. **The post-login config hint must not emit a partial `limit` object.** opencode's live config schema at `https://opencode.ai/config.json` requires both `limit.context` and `limit.output` whenever `limit` is present, while Kimi's `GET /coding/v1/models` only gives us `context_length`. Therefore `buildConfigBlock()` omits `limit` entirely and leaves `provider.models` to backfill `limit.context` at runtime. Do not invent `output` or set `input` heuristically; opencode's overflow logic treats `limit.input` as authoritative (`research/opencode/packages/opencode/src/session/overflow.ts`). ### Working on this repo diff --git a/README.md b/README.md index 83a6641..09324e1 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Two identifiers are load-bearing: opencode auth login -p kimi-for-coding-oauth ``` -The plugin returns a verification URL and user code. After browser approval it polls the device-auth endpoint, queries `/coding/v1/models` to discover the current model id and context length for your account, prints a config snippet with that context length filled in, and stores the token in opencode's auth store. Model discovery runs again on every token refresh, and a fresh loader instance will re-query `/coding/v1/models` on first use if it needs the current wire model id. Access tokens refresh automatically, and the loader retries once after a `401`. +The plugin returns a verification URL and user code. After browser approval it polls the device-auth endpoint, queries `/coding/v1/models` to discover the current model id and context length for your account, prints a ready-to-paste config snippet, and stores the token in opencode's auth store. The authorization message still includes the discovered context length; the snippet intentionally omits `limit` because opencode's schema requires `limit.output` too, and Kimi's models endpoint only exposes `context_length`. Model discovery runs again on every token refresh, and a fresh loader instance will re-query `/coding/v1/models` on first use if it needs the current wire model id. Access tokens refresh automatically, and the loader retries once after a `401`. ### Use diff --git a/package.json b/package.json index 59ecb1b..4e2c58d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-kimi-full", - "version": "1.2.2", + "version": "1.2.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": { diff --git a/src/index.ts b/src/index.ts index 9dc8969..0ac90f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -174,13 +174,17 @@ function applyDiscoveryToModels> } } -function buildConfigBlock(info: { model_id: string; context_length?: number; display?: string }) { +function buildConfigBlock(info: { model_id: string; display?: string }) { const name = info.display ?? "Kimi For Coding" - const ctx = info.context_length ?? 0 // The opencode-side model key is always MODEL_ID ("kimi-for-coding"); the // plugin rewrites the wire `model` body field to `info.model_id` inside // `loader.fetch`. This way both K2.5 and K2.6 users paste identical // config — only the wire request differs. + // + // Intentionally omit `limit`: opencode's config schema requires + // `limit.output` whenever a `limit` object is present, but Kimi's + // `/coding/v1/models` discovery only tells us `context_length`. The + // provider.models hook backfills `limit.context` at runtime. return JSON.stringify( { provider: { @@ -193,7 +197,6 @@ function buildConfigBlock(info: { model_id: string; context_length?: number; dis name, reasoning: true, options: {}, - ...(ctx > 0 ? { limit: { context: ctx } } : {}), variants: { off: { reasoning_effort: "off" }, auto: { reasoning_effort: "auto" }, @@ -462,7 +465,6 @@ const plugin: Plugin = async ({ client }) => { // this next to the "Authorized" message. const block = buildConfigBlock({ model_id: discovered.model_id, - context_length: discovered.context_length, display: discovered.model_display, }) console.log( diff --git a/test/plugin.test.ts b/test/plugin.test.ts index b37f4fa..b98d2d3 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -692,7 +692,7 @@ test("auth.methods[0].authorize returns URL + instructions + async callback", as expect(typeof cb.expires).toBe("number") }) -test("auth callback prints a config snippet with top-level model variants", async () => { +test("auth callback prints a schema-valid config snippet with top-level model variants", async () => { mock = installFetchMock((call) => { if (call.url.includes("device_authorization")) { return { @@ -744,7 +744,8 @@ test("auth callback prints a config snippet with top-level model variants", asyn } } const model = parsed.provider[PROVIDER_ID]!.models[MODEL_ID]! - expect(model.limit?.context).toBe(262144) + expect(text).toContain("context 262144") + expect(model.limit).toBeUndefined() expect(model.options).toEqual({}) expect(model.variants?.off).toEqual({ reasoning_effort: "off" }) expect(model.variants?.auto).toEqual({ reasoning_effort: "auto" })