diff --git a/AGENTS.md b/AGENTS.md index 81039cf..e44f2b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,16 +12,13 @@ One plugin, one job: make `opencode` talk to Kimi's `kimi-for-coding` endpoint * ### 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. +> Moonshot's coding backend is entitlement-sensitive: the model-name string alone is not the whole story. -- 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. +Every design decision here follows from that: we do device-flow OAuth to mirror official `kimi-cli`, we do not accept API keys in this plugin, and 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 any non-`kimi-for-coding` model. opencode already handles other Moonshot / Baseten / Alibaba-CN / etc. entries itself. - 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. @@ -48,7 +45,7 @@ Data flow on a chat request: ### 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. +These are the invariants that, if broken, silently route requests onto the wrong auth/backend path 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`. 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` must mirror kimi-cli's `_device_model()` shape, including the Darwin/Windows special cases (`macOS `, `Windows 10/11 `, Linux `"{system} {release} {machine}"`) — 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. 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. @@ -66,9 +63,9 @@ These are the invariants that, if broken, silently degrade K2.6 → K2.5 or prod `auto` is the "let the server decide dynamically" variant — neither field is sent, matching kimi-cli's "nothing passed" default. When no effort is set at all, the plugin still emits `thinking: {type: "enabled"}` because the model is a reasoner. Compute this from `input.model.options` plus `input.model.variants[input.message.model.variant]`, not from `input.provider.info.id`. The `@opencode-ai/plugin` `ProviderContext` type claims `.info.id` exists, but the runtime shape opencode passes (see `research/opencode/packages/opencode/src/session/llm.ts::stream`, ~line 168, `provider: item`) is the flat `ProviderConfig` (`.id`). `input.model.providerID` is what every first-party plugin uses (cloudflare.ts, codex.ts, github-copilot/copilot.ts) and it avoids the runtime crash "undefined is not an object (evaluating 'input.provider.info.id')". Tested live 2026-04-17. 5. **`prompt_cache_key` only for `kimi-for-coding`.** Never attach it to unrelated models. The check is `input.model.id === MODEL_ID` in the Kimi chat hooks, and the actual wire injection happens in `loader.fetch`. -6. **Wire model id comes from `/coding/v1/models`, not from user config.** The opencode-side model id is a stable alias (`MODEL_ID = "kimi-for-coding"`); the plugin calls `GET /coding/v1/models` at login and on every token refresh (mirroring kimi-cli's `refresh_managed_models` in `research/kimi-cli/src/kimi_cli/auth/platforms.py`), caches the first returned `{id, context_length, display_name}` in loader memory, and rewrites the JSON body `model` field inside `loader.fetch` whenever the discovered id differs from `MODEL_ID` (the K2.5 case — server may return `k2p5` instead). A new loader instance re-discovers on first use if needed. K2.6 accounts see `id: "kimi-for-coding"` and the rewrite is a no-op. Do not strip the `kimi-` prefix; send whatever the server returned. Discovery failures are non-fatal (warm cached id still works; 401 retry flushes broken tokens). +6. **Wire model id comes from `/coding/v1/models`, not from user config.** The opencode-side model id is a stable alias (`MODEL_ID = "kimi-for-coding"`); the plugin calls `GET /coding/v1/models` at login and on every token refresh (mirroring kimi-cli's `refresh_managed_models` in `research/kimi-cli/src/kimi_cli/auth/platforms.py`), caches the first returned `{id, context_length, display_name}` in loader memory, rewrites the JSON body `model` field inside `loader.fetch` whenever the discovered id differs from `MODEL_ID`, and backfills runtime model metadata from the same discovery response. A new loader instance re-discovers on first use if needed. Do not strip the `kimi-` prefix; send whatever the server returned. Discovery failures are non-fatal (warm cached id still works; 401 retry flushes broken tokens). 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. The plugin may live-read opencode's `auth.json` entry for this provider to bypass stale `OPENCODE_AUTH_CONTENT` workspace snapshots, but writes still go through opencode's auth store (`client.auth.set`). 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). +8. **Provider id must not collide with any id in the [models.dev](https://models.dev) catalog.** models.dev publishes `kimi-for-coding` as a separate API-key-driven integration. If we registered under that same id, `opencode auth login kimi-for-coding` would surface two methods under one entry and users could silently land on the wrong integration path. 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`). 11. **Concurrent refreshes must collapse to one in-flight OAuth exchange, even across plugin instances.** `provider.models` and `auth.loader` can both notice an expiring token at about the same time, and separate opencode workspace/plugin instances can inherit stale auth snapshots. `refreshAuth()` in `src/index.ts` therefore shares one promise across overlapping callers, takes a provider-scoped auth-store lock before refreshing, re-reads opencode's live auth-store entry under that lock, and treats a changed on-disk token chain as authoritative. `test/plugin.test.ts` covers loader-vs-loader, provider.models-vs-loader, cross-instance lock reuse, and the `invalid_grant` self-heal path where another process already rotated the refresh token. @@ -109,7 +106,7 @@ Online (requires a real Kimi-for-coding account): 1. Install the local checkout via opencode's plugin flow (`opencode plugin /path/to/this/repo --global`) or point the `plugin` array in your opencode config at the repo root, as shown in `README.md`. 2. Paste the provider block from `README.md` into your opencode config. 3. `opencode auth login kimi-for-coding-oauth` — 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-oauth/kimi-for-coding`, and ask the model to self-identify. It should claim to be K2.6 / `kimi-for-coding`. +4. Start opencode, select `kimi-for-coding-oauth/kimi-for-coding`, and ask the model to self-identify. It should claim to be `kimi-for-coding` / Kimi Code. 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`). diff --git a/README.md b/README.md index f7343ab..669ac8b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## opencode-kimi-full -An [opencode](https://opencode.ai) plugin that adds a Kimi For Coding OAuth provider. +An [opencode](https://opencode.ai) plugin that adds the official Kimi For Coding OAuth provider plus the Kimi-specific request metadata that `kimi-cli` sends. This plugin: @@ -9,9 +9,10 @@ This plugin: - sends the same `User-Agent` / `X-Msh-*` fingerprint headers as `kimi-cli` - reuses `~/.kimi/device_id` for `X-Msh-Device-Id` - adds `prompt_cache_key`, `thinking`, and `reasoning_effort` for `kimi-for-coding` requests -- is currently the only way to get Kimi K2.6 outside of using Kimi CLI + OAuth +- discovers the authoritative wire model slug, API display name, and context length from `/coding/v1/models` +- keeps tokens in opencode's auth store while mirroring `kimi-cli`'s refresh / retry behavior -This is the K2.6 / `kimi-for-coding` OAuth path: Moonshot routes static `sk-kimi-...` API keys to K2.5, and OAuth tokens with `scope: kimi-code` to K2.6. +Kimi K2.6 is no longer exclusive to this path. The reason this plugin exists is different: it keeps opencode on the same OAuth, fingerprint, and Kimi-extension-field path as the official `kimi-cli`, without sharing `kimi-cli`'s credential files. Contributor and agent documentation lives in [`AGENTS.md`](./AGENTS.md). @@ -113,8 +114,8 @@ During login the plugin: - shows a verification URL and user code - stores the OAuth token in opencode's auth store -- discovers the exact model slug your account should send to Kimi -- prints a config hint with the discovered context length +- discovers the exact model slug, display name, and context length your account should send to Kimi +- prints a config hint that uses the discovered display name and leaves context backfill to runtime metadata discovery Access tokens refresh automatically while you use the model. @@ -147,12 +148,14 @@ These variants only affect Kimi's reasoning request fields. They do not switch m The exact behavioral difference between `low`, `medium`, and `high` is controlled by Kimi's backend, so this should be read as a server hint rather than a guaranteed latency/quality ladder. +Every `kimi-for-coding` request also gets `prompt_cache_key` set to opencode's session id. That mirrors `kimi-cli`'s cache hint so follow-up turns in the same session can reuse Kimi's prompt cache. + ---
Why this plugin exists -This plugin exists to bring the OAuth/device-flow `kimi-cli` path into opencode without sharing kimi-cli's credential files. +This plugin exists to bring the official Kimi Code OAuth path and Kimi-specific request extensions into opencode without sharing `kimi-cli`'s credential files. **What it changes.** @@ -162,6 +165,7 @@ This plugin exists to bring the OAuth/device-flow `kimi-cli` path into opencode - Paired `thinking` + `reasoning_effort` fields. - The seven `X-Msh-*` headers and a kimi-cli-shaped `User-Agent`. - `~/.kimi/device_id` shared with a locally-installed kimi-cli. +- Runtime model discovery from `/coding/v1/models`, including the server-reported wire slug, `display_name`, and `context_length`. - Tokens stored in opencode's auth store under a dedicated provider id, so the plugin and kimi-cli keep independent refresh-token chains and do not invalidate each other. - Live auth-store rereads plus a provider-scoped refresh lock, so concurrent opencode workspaces converge on the latest refresh-token chain instead of tripping `invalid_grant`. - Streaming, `reasoning_content` deltas, and tool-call schemas are handled upstream by `@ai-sdk/openai-compatible` — not reimplemented here. @@ -176,6 +180,7 @@ This plugin exists to bring the OAuth/device-flow `kimi-cli` path into opencode | `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`. | +| `/coding/v1/models` discovery | `id`, `display_name`, `context_length` | Supplies the authoritative wire model slug plus runtime model metadata. | | `~/.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 used by the plugin: @@ -214,6 +219,7 @@ No other state is persisted. Credentials are never written to `~/.kimi/credentia │ chat ──────▶ plugin.loader() │ custom fetch that: │ ├─▶ ensureFresh() │ • proactive refresh │ └─▶ kimiHeaders() │ • 7 X-Msh-* headers +│ │ • /models slug + display_name discovery │ │ • 401 → force-refresh + retry │ chat.params ─▶ plugin "chat.params" │ thinking / reasoning_effort / │ │ prompt_cache_key diff --git a/package.json b/package.json index c8d394e..b984e02 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "opencode-kimi-full", - "version": "1.2.6", - "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.", + "version": "1.2.7", + "description": "OpenCode plugin that brings the official Kimi OAuth device flow and Kimi-specific coding request fields to opencode, matching upstream kimi-cli.", "license": "MIT", "repository": { "type": "git", @@ -31,9 +31,11 @@ "opencode-plugin", "kimi", "kimi-for-coding", - "kimi-k26", "moonshot", - "oauth" + "oauth", + "device-flow", + "reasoning-effort", + "prompt-cache-key" ], "peerDependencies": { "@opencode-ai/plugin": ">=1.4.6" diff --git a/src/constants.ts b/src/constants.ts index 9007e28..ab1203a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,13 +1,13 @@ -// Values mirror kimi-cli v1.35.0 1:1. When upstream bumps, update here and +// Values mirror kimi-cli v1.37.0 1:1. When upstream bumps, update here and // nothing else in the codebase should hard-code these strings. // // Source of truth: research/kimi-cli/src/kimi_cli/constant.py, // research/kimi-cli/src/kimi_cli/auth/oauth.py // // NOTE: client_id is a public constant shipped inside the official CLI, not a -// secret. scope `kimi-code` is what routes the issued JWT to K2.6. +// secret. scope `kimi-code` is the upstream coding-agent OAuth scope. -export const KIMI_CLI_VERSION = "1.35.0" +export const KIMI_CLI_VERSION = "1.37.0" // Upstream: research/kimi-cli/src/kimi_cli/constant.py get_user_agent() → // f"KimiCLI/{get_version()}". This must match verbatim — Moonshot's // `kimi-for-coding` backend 403s on any other UA prefix @@ -27,9 +27,9 @@ export const MODEL_ID = "kimi-for-coding" // Provider id the user must use in their opencode config. Intentionally NOT // "kimi-for-coding" — models.dev publishes an entry under that id (static -// KIMI_API_KEY → K2.5 via @ai-sdk/anthropic), and sharing the id would surface -// two auth methods under one `opencode auth login` entry and silently route -// API-key users to the wrong backend. See AGENTS.md rule 7. +// KIMI_API_KEY flow via a different SDK / auth shape), and sharing the id +// would surface two auth methods under one `opencode auth login` entry and +// silently route users onto the wrong integration path. See AGENTS.md rule 7. export const PROVIDER_ID = "kimi-for-coding-oauth" // Refresh a bit before the server-reported expiry so we never race it. diff --git a/src/index.ts b/src/index.ts index 0994b99..20a1795 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,8 @@ type KimiBodyFields = { reasoning_effort?: string } -type ModelWithContextLimit = { +type ModelWithDiscoveryMetadata = { + name?: string limit?: { context?: number } @@ -251,7 +252,7 @@ function pickModelInfo(models: KimiModelInfo[]): ModelDiscovery { } } -function withDiscoveredContext(model: T, contextLength: number | undefined): T { +function withDiscoveredContext(model: T, contextLength: number | undefined): T { if (!contextLength || contextLength <= 0) return model if ((model.limit?.context ?? 0) > 0) return model return { @@ -263,10 +264,18 @@ function withDiscoveredContext(model: T, contex } } -function applyDiscoveryToModels>(models: T, discovery: ModelDiscovery): T { +function withDiscoveredDisplayName(model: T, displayName: string | undefined): T { + if (!displayName || model.name === displayName) return model + return { + ...model, + name: displayName, + } +} + +function applyDiscoveryToModels>(models: T, discovery: ModelDiscovery): T { const current = models[MODEL_ID] if (!current) return models - const next = withDiscoveredContext(current, discovery.context_length) + const next = withDiscoveredContext(withDiscoveredDisplayName(current, discovery.model_display), discovery.context_length) if (next === current) return models return { ...models, @@ -278,8 +287,8 @@ function buildConfigBlock(info: { model_id: string; display?: string }) { const name = info.display ?? "Kimi For Coding" // 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. + // `loader.fetch`. This way users paste identical config even if the + // server reports a different wire slug for their account. // // Intentionally omit `limit`: opencode's config schema requires // `limit.output` whenever a `limit` object is present, but Kimi's @@ -330,9 +339,9 @@ function buildConfigBlock(info: { model_id: string; display?: string }) { * (c) lazily discovers the current wire model id from * `GET /coding/v1/models`, and (d) retries once with a forced * refresh on 401. - * 3. `provider.models` — discovers `context_length` early enough to patch - * opencode's model metadata when the user's config still has - * the default zero context window. + * 3. `provider.models` — discovers `context_length` / `display_name` early + * enough to patch opencode's runtime model metadata when the + * user's config still has the default placeholder values. * 4. `chat.headers` — computes the Kimi-specific request body fields the * model actually needs (`thinking.type`, * `reasoning_effort`, `prompt_cache_key`) and passes them to @@ -502,9 +511,9 @@ const plugin: Plugin = async ({ client }) => { if (!force && !isExpiring(current)) return ensureDiscovered(current) const next = await refreshAuth(current, force) // kimi-cli re-runs `refresh_managed_models` on every successful - // refresh — we mirror that so entitlement changes (e.g. an - // account gaining/losing K2.6 access) are picked up without a - // full re-login. Failures here must not block the refresh: a + // refresh — we mirror that so entitlement or display-name changes + // are picked up without a full re-login. Failures here must not + // block the refresh: a // warm in-memory discovery still works for the common case, and // the request-path 401 retry will flush a broken access token. try { @@ -548,8 +557,8 @@ const plugin: Plugin = async ({ client }) => { // our opencode-side placeholder MODEL_ID. // This way `input.model.id` stays `kimi-for-coding` in // opencode's UI/config, while Moonshot sees whatever its - // /models endpoint says for this account (e.g. `k2p5` on - // K2.5 tiers). Mirrors kimi-cli's behavior — it always sends + // /models endpoint says for this account (for example a + // non-default slug). Mirrors kimi-cli's behavior — it always sends // exactly the id it got back from `/models`. let newInit = init const targetModel = auth.model_id diff --git a/src/oauth.ts b/src/oauth.ts index 49fab84..11c8ce2 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -187,9 +187,9 @@ export type KimiModelInfo = { /** * Calls `GET {API_BASE_URL}/models` with the user's JWT and returns the * server's authoritative model list for this account. Different account - * tiers see different slugs (K2.5 accounts may see `k2p5`, K2.6 accounts - * see `kimi-for-coding`); the `id` and `context_length` here are the only - * truth about what the user is actually entitled to. + * states may see different slugs; the `id`, `display_name`, and + * `context_length` here are the only truth about what the user is actually + * entitled to. * * kimi-cli calls this on login and on every successful token refresh * (see `refresh_managed_models` in platforms.py). We do the same. diff --git a/test/constants.test.ts b/test/constants.test.ts index fd7a5ab..7b4d6a9 100644 --- a/test/constants.test.ts +++ b/test/constants.test.ts @@ -2,7 +2,7 @@ import { test, expect } from "bun:test" import * as C from "../src/constants.ts" // These values form the "identity" of the plugin on the wire. Typos silently -// downgrade K2.6 → K2.5 (scope/client_id) or collide with models.dev +// send requests down the wrong auth / backend path or collide with models.dev // (PROVIDER_ID). See AGENTS.md "Contracts to keep intact". test("KIMI_CLI_VERSION is a non-empty semver", () => { @@ -19,7 +19,7 @@ test("USER_AGENT embeds KIMI_CLI_VERSION", () => { test("OAuth constants match upstream kimi-cli exactly", () => { // Pinned values from research/kimi-cli/src/kimi_cli/auth/oauth.py. If these // drift from upstream, tokens are issued against the wrong scope/client and - // the backend routes to K2.5. + // the plugin no longer mirrors official kimi-cli auth. expect(C.OAUTH_HOST).toBe("https://auth.kimi.com") expect(C.OAUTH_DEVICE_AUTH_URL).toBe("https://auth.kimi.com/api/oauth/device_authorization") expect(C.OAUTH_TOKEN_URL).toBe("https://auth.kimi.com/api/oauth/token") diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 2e32277..af0466a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -321,6 +321,20 @@ test("provider.models: fills limit.context from discovery when config still has expect(writes).toHaveLength(0) }) +test("provider.models: surfaces discovered display_name in runtime model metadata", async () => { + mock = installFetchMock((call) => { + if (call.url.endsWith("/coding/v1/models")) { + return { body: { data: [{ id: MODEL_ID, display_name: "Kimi Code", context_length: 262144 }] } } + } + return { body: { ok: true } } + }) + const { hooks } = await getHooks() + const provider = makeProviderState() + const next = await hooks.provider!.models!(provider as any, { auth: validAuth() } as any) + expect(next[MODEL_ID]!.name).toBe("Kimi Code") + expect(provider.models[MODEL_ID]!.name).toBe("Kimi For Coding") +}) + test("provider.models: preserves an explicit user context limit", async () => { mock = installFetchMock((call) => { if (call.url.endsWith("/coding/v1/models")) { @@ -789,7 +803,7 @@ test("auth.loader: caches discovered model info for subsequent requests in the s test("auth.loader: rewrites wire `model` to the discovered server id (Option A)", async () => { // Persisted auth already carries a discovered model_id different from the - // opencode-side MODEL_ID placeholder — this is the K2.5 account case. + // opencode-side MODEL_ID placeholder — this is the alternate-slug case. const current = { ...validAuth(), model_id: "k2p5", @@ -807,7 +821,7 @@ test("auth.loader: rewrites wire `model` to the discovered server id (Option A)" expect(sentBody.messages).toEqual([]) }) -test("auth.loader: leaves body untouched when discovered id equals MODEL_ID (K2.6 case)", async () => { +test("auth.loader: leaves body untouched when discovered id equals MODEL_ID", async () => { const current = { ...validAuth(), model_id: MODEL_ID } as unknown as ReturnType mock = installFetchMock(() => ({ body: { ok: true } })) const { fetch: f } = await getLoaderFetch(async () => current)