opencode's getLegacyPlugins iterates every module export and throws on
non-function values. On Windows, Bun's standalone-binary dynamic imports
can produce module namespace objects with extra non-function metadata,
silently preventing the plugin from loading (the provider never appears
in `opencode auth login`).
Switch the default export from a bare Plugin function to the v1
PluginModule object ({ id, server }), which opencode's readV1Plugin
detects and handles before getLegacyPlugins ever runs. Also add
exports["./server"] for explicit entry point resolution.
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.
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).