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.
off first (the most conservative, fastest option), then auto (server-picked
default), then the escalating levels. Matches the post-login config block
printed by the authorize callback and the user's live config.
Mirrors kimi-cli's refresh_managed_models behavior: at login (and on every
token refresh) the plugin now GETs /coding/v1/models with the user's JWT,
caches the first returned {id, context_length, display_name} in auth.json,
and rewrites the wire 'model' body field to the cached id inside
loader.fetch. K2.5 accounts (server returns e.g. 'k2p5') and K2.6 accounts
(server returns 'kimi-for-coding') now share identical opencode config.
After successful login, the authorize callback prints a ready-to-paste
provider config block with the discovered values filled in.
- src/oauth.ts: added listModels() (GET /coding/v1/models).
- src/index.ts: OAuthAuth extended with model_id/context_length/model_display;
ensureFresh() refetches on refresh; loader.fetch rewrites JSON body
'model' when cached id differs from MODEL_ID; authorize callback runs
listModels() + console.logs a ready-to-paste config block.
- README.md: drop hardcoded context/output limits and K2.6-specific name;
explain auto-discovery and the K2.5/K2.6 alias mechanism.
- AGENTS.md: rule 6 rewritten to describe the wire-rewrite contract.
- test/plugin.test.ts: +3 tests (discovery on refresh persists metadata,
graceful /models failure, body.model rewrite K2.5 + K2.6 no-op);
existing 401-retry and refresh tests updated for the extra /models call.
Tested live against a real K2.6 account — listModels() returns
[{id: 'kimi-for-coding', context_length: 262144, ...}].
Two user-visible fixes:
1. Runtime crash 'undefined is not an object (evaluating
input.provider.info.id)' on any chat. The @opencode-ai/plugin
ProviderContext type claims .info.id exists, but opencode's
session/llm.ts::stream passes the flat ProviderConfig as provider
(research/opencode/packages/opencode/src/session/llm.ts ~line 168).
Gate on input.model.providerID instead — matches cloudflare.ts,
codex.ts, github-copilot/copilot.ts.
2. Ctrl+T reasoning variants (off/auto/low/medium/high) were not
available. README config now declares model.variants, which opencode
merges into options before chat.params runs; the hook normalizes
'auto' to omit both thinking and reasoning_effort so Moonshot picks
dynamically, matching kimi-cli's 'nothing passed' default.
Also:
- AGENTS.md rule 4 updated with 'auto' row and the providerID gating
warning, citing the live 2026-04-17 repro.
- test/plugin.test.ts input shape updated to match runtime; added a
test for the 'auto' variant.
The previous display name 'Kimi K2.6 (for coding)' was confusing —
K2.6 is a model, not a provider, and it collided visually with the
models.dev 'Kimi For Coding' entry in interactive 'opencode auth login'.
Name now clearly signals this is the OAuth variant.
Plugin was silently 403ing every live request with
'access_terminated_error: only available for Coding Agents'. Discovered by
capturing what kimi-cli actually sends on the wire and diffing.
Three fingerprint bugs, each sufficient on its own to trigger the 403:
* User-Agent was 'KimiCodeCLI/<v>'; upstream sends 'KimiCLI/<v>'.
(research/kimi-cli/src/kimi_cli/constant.py::get_user_agent)
* X-Msh-Device-Model was 'x86_64'; upstream sends
'{system} {release} {machine}' e.g. 'Linux 7.0.0 x86_64'.
(research/kimi-cli/src/kimi_cli/auth/oauth.py::_device_model)
* X-Msh-Os-Version was '{type} {release}' e.g. 'Linux 7.0.0'; upstream
sends platform.version() i.e. the kernel build string. Node equivalent
is os.version().
Verified live against api.kimi.com/coding/v1 with a freshly-minted JWT:
200 OK and real K2.6 response after the fix; 403 before.
Locked in with regression tests in test/headers.test.ts and
test/constants.test.ts, and documented in AGENTS.md contract rule 1.
Adds one test file per source file plus a shared fetch mock helper:
- test/constants.test.ts: OAuth scope/client_id/PROVIDER_ID/MODEL_ID pins,
rules 1, 6, 8.
- test/headers.test.ts: seven fingerprint header keys, UA/X-Msh-Version
track KIMI_CLI_VERSION, ASCII-only values, device-id format and
stability across calls (rules 1, 2).
- test/oauth.test.ts: form-encoded bodies for device-start/refresh,
authorization_pending/expired_token/unknown-error branches, non-OK and
non-JSON error shapes.
- test/plugin.test.ts: chat.params provider+model gating, full effort
matrix (rule 4), prompt_cache_key gating (rule 5), camelCase effort
input, loader Authorization ownership (rule 3), refresh-on-expiry with
persistAuth, 401 single-retry no-loop, device-flow authorize wiring.
- test/_util/fetchMock.ts: zero-dep global-fetch swap returning canned
Responses with call capture.
Notes:
- Tests use the real ~/.kimi/device_id; getDeviceId is idempotent and the
file is shared with kimi-cli by design (AGENTS.md rule 2), so no HOME
redirect is needed. Attempted monkey-patching os.homedir instead is
fragile under Bun's eager import-binding resolution.
- pollDeviceToken tests use interval=1 because `device.interval || 5`
treats 0 as the default 5s and would push each iteration past the
default bun-test timeout.
Total: 35 tests, ~5s to run.
Plugin v1.0.0 exported a named PROVIDER_ID constant alongside the default
export. opencode's plugin loader (getLegacyPlugins) iterates every export
and requires each to be a function; the named string export caused it to
throw 'Plugin export is not a function', which was only visible in the
log file — the provider silently vanished from 'opencode auth login'.
- Move PROVIDER_ID into src/constants.ts and import it in src/index.ts.
- Add test/exports.test.ts as a regression guard (bun test).
- Add bunfig.toml to scope bun test away from research/ and node_modules/.
- Wire 'bun test' into the release workflow.
- Document rule 9 in AGENTS.md (single default export in src/index.ts).
- Also pick up the previous session's README fix (opencode auth login -p).