Update README and sync kimi-cli 1.37.0 parity

This commit is contained in:
lemon07r
2026-04-21 16:57:22 -04:00
parent c6b13940ac
commit 79938fe1bd
8 changed files with 75 additions and 47 deletions
+7 -10
View File
@@ -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 <version> <arch>`, `Windows 10/11 <arch>`, 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`).
+12 -6
View File
@@ -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.
---
<details>
<summary><strong>Why this plugin exists</strong></summary>
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
+6 -4
View File
@@ -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"
+6 -6
View File
@@ -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.
+23 -14
View File
@@ -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<T extends ModelWithContextLimit>(model: T, contextLength: number | undefined): T {
function withDiscoveredContext<T extends ModelWithDiscoveryMetadata>(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<T extends ModelWithContextLimit>(model: T, contex
}
}
function applyDiscoveryToModels<T extends Record<string, ModelWithContextLimit>>(models: T, discovery: ModelDiscovery): T {
function withDiscoveredDisplayName<T extends ModelWithDiscoveryMetadata>(model: T, displayName: string | undefined): T {
if (!displayName || model.name === displayName) return model
return {
...model,
name: displayName,
}
}
function applyDiscoveryToModels<T extends Record<string, ModelWithDiscoveryMetadata>>(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
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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")
+16 -2
View File
@@ -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<typeof validAuth>
mock = installFetchMock(() => ({ body: { ok: true } }))
const { fetch: f } = await getLoaderFetch(async () => current)