mirror of
https://github.com/lemon07r/opencode-kimi-full.git
synced 2026-06-02 06:14:16 +02:00
Add video_in discovery, clamp xhigh/max effort, fix image config
This commit is contained in:
@@ -63,23 +63,25 @@ These are the invariants that, if broken, silently route requests onto the wrong
|
||||
| `low` | `"low"` | `{type:"enabled"}` |
|
||||
| `medium` | `"medium"` | `{type:"enabled"}` |
|
||||
| `high` | `"high"` | `{type:"enabled"}` |
|
||||
| `xhigh` | `"high"` (clamped) | `{type:"enabled"}` |
|
||||
| `max` | `"high"` (clamped) | `{type:"enabled"}` |
|
||||
|
||||
`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.
|
||||
`auto` is the "let the server decide dynamically" variant — neither field is sent, matching kimi-cli's "nothing passed" default. `xhigh` and `max` are clamped to `"high"` because Kimi's backend does not support higher tiers (kimi-cli's `Kimi.with_thinking()` does the same). 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, 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).
|
||||
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, supports_image_in, supports_video_in}` 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` 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 `PluginModule` object `{ id, server }`.** opencode's plugin loader (`research/opencode/packages/opencode/src/plugin/index.ts`) first tries `readV1Plugin` (detect mode) on the default export. If it finds an object with `server` (and optional `id`), it uses the v1 path directly. The older legacy path (`getLegacyPlugins`) iterates every export and throws `Plugin export is not a function` on any non-callable value — a problem that surfaced on Windows where Bun's standalone-binary dynamic imports can produce module namespace objects with unexpected non-function metadata. The v1 format bypasses `getLegacyPlugins` entirely. Keep constants in `src/constants.ts` and import them in `src/index.ts` rather than re-exporting. `test/exports.test.ts` guards this. The failure mode of a broken export 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`.
|
||||
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.
|
||||
12. **Image-input capability must be backfilled from `/coding/v1/models`.** `supports_image_in` from Kimi discovery is not cosmetic metadata: opencode's provider transform (`research/opencode/packages/opencode/src/provider/transform.ts::unsupportedParts`) rewrites every image part into local `ERROR: Cannot read ... (this model does not support image input)` text before the request reaches our loader when `capabilities.input.image` is false. Therefore `provider.models` must patch runtime model metadata for `kimi-for-coding`, and `buildConfigBlock()` must include `attachment: true` plus `modalities.input = ["text","image"]` / `modalities.output = ["text"]` when discovery says images are supported. `test/plugin.test.ts` covers both paths.
|
||||
12. **Media-input capabilities must be backfilled from `/coding/v1/models`.** `supports_image_in` and `supports_video_in` from Kimi discovery are not cosmetic metadata: opencode's provider transform (`research/opencode/packages/opencode/src/provider/transform.ts::unsupportedParts`) rewrites every image part into local `ERROR: Cannot read ... (this model does not support image input)` text before the request reaches our loader when `capabilities.input.image` is false. Therefore `provider.models` must patch runtime model metadata for `kimi-for-coding`, and `buildConfigBlock()` must include `attachment: true` plus appropriate `modalities.input` / `modalities.output` when discovery says images/video are supported. `test/plugin.test.ts` covers both paths.
|
||||
|
||||
### Working on this repo
|
||||
|
||||
- **Code style:** see `tsconfig.json` (strict, `noUncheckedIndexedAccess`, ES2022). Prefer small pure functions, avoid `try`/`catch` except where we genuinely convert one error shape to another.
|
||||
- **Comments:** match the existing density — only explain non-obvious upstream-parity reasoning. Do not narrate the obvious ("// refresh the token"); instead reference upstream files when the reasoning is "because kimi-cli does it that way".
|
||||
- **Dependencies:** runtime deps stay at **zero**. The only dev/peer dep is `@opencode-ai/plugin` for types.
|
||||
- **Dependencies:** runtime deps are limited to `@opentui/core` and `@opentui/solid` (for the TUI slash command). The only dev/peer dep is `@opencode-ai/plugin` for types. Do not add further runtime deps.
|
||||
- **Git commits:** small, logical, imperative subject ("Add oauth device flow"). Do not add a `Co-authored-by` trailer.
|
||||
- **Upstream research:** the `research/` directory is a read-only git-ignored pair of shallow clones (opencode + kimi-cli) for grep. Never edit files there; re-clone if you suspect drift. When citing upstream in a comment, use the `research/…` path so the reference is resolvable.
|
||||
- **Version bumps:** when kimi-cli bumps, (1) pull a fresh `research/kimi-cli`, (2) update `KIMI_CLI_VERSION` in `src/constants.ts`, (3) re-diff `_kimi_default_headers()` / `oauth.py` against `src/headers.ts` and `src/oauth.ts`, (4) smoke-test with `opencode auth login kimi-for-coding-oauth` and a one-turn chat, (5) tag release.
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
## opencode-kimi-full
|
||||
|
||||
An [opencode](https://opencode.ai) plugin that extends the Kimi Code path in opencode work like the official `kimi-cli` and make use of it's Kimi-specific extensions, instead of just working like a generic OpenAI-compatible provider.
|
||||
An [opencode](https://opencode.ai) plugin that makes the Kimi Code path in opencode work like the official `kimi-cli`, using Kimi-specific extensions instead of just a generic OpenAI-compatible provider.
|
||||
|
||||
Compared with stock opencode Kimi setups, this plugin:
|
||||
|
||||
- uses the official Kimi device flow against `https://auth.kimi.com` with `scope: kimi-code`
|
||||
- uses the official Kimi device-flow OAuth against `https://auth.kimi.com`
|
||||
- talks to `https://api.kimi.com/coding/v1` through `@ai-sdk/openai-compatible`
|
||||
- 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
|
||||
- discovers the authoritative wire model slug, API display name, context length, and image-input capability from `/coding/v1/models`
|
||||
- discovers the authoritative wire model slug, display name, context length, and media-input capabilities from `/coding/v1/models`
|
||||
- keeps tokens in opencode's auth store while mirroring `kimi-cli`'s refresh / retry behavior
|
||||
|
||||
That is the value of using this plugin instead of a plain opencode provider entry: it preserves the Kimi-only OAuth path, fingerprint, and request extensions that the generic route does not.
|
||||
- provides a `/kimi:usage` TUI command to check subscription usage
|
||||
|
||||
Contributor and agent documentation lives in [`AGENTS.md`](./AGENTS.md).
|
||||
|
||||
@@ -28,7 +27,7 @@ Contributor and agent documentation lives in [`AGENTS.md`](./AGENTS.md).
|
||||
|
||||
### Requirements
|
||||
|
||||
- `opencode` ≥ 1.4.6
|
||||
- `opencode` >= 1.4.6
|
||||
- A Kimi account with an active **Kimi For Coding** subscription (the same plan that works with kimi-cli)
|
||||
|
||||
### Install
|
||||
@@ -86,7 +85,12 @@ After the plugin is installed and login works, paste this provider entry into `~
|
||||
"models": {
|
||||
"kimi-for-coding": {
|
||||
"name": "Kimi For Coding",
|
||||
"attachment": true,
|
||||
"reasoning": true,
|
||||
"modalities": {
|
||||
"input": ["text", "image"],
|
||||
"output": ["text"]
|
||||
},
|
||||
"options": {},
|
||||
"variants": {
|
||||
"off": { "reasoning_effort": "off" },
|
||||
@@ -102,12 +106,14 @@ After the plugin is installed and login works, paste this provider entry into `~
|
||||
}
|
||||
```
|
||||
|
||||
> **Important:** The `attachment` and `modalities` fields are required for image input to work. Without them, opencode strips image parts before they reach Kimi. If you previously pasted an older config block without these fields, update it.
|
||||
|
||||
This block is for using the model after login. It does **not** register the auth provider by itself. What makes `opencode auth login -p kimi-for-coding-oauth` work is the plugin being loaded via `opencode plugin ...` or the `plugin` array above.
|
||||
|
||||
Use these two ids exactly as written:
|
||||
|
||||
- **provider id** `kimi-for-coding-oauth` — the plugin's `auth` and `chat.params` hooks match on it.
|
||||
- **model id** `kimi-for-coding` — a stable opencode-side alias. At login and on every token refresh the plugin queries `/coding/v1/models` and rewrites the wire `model` field if the server reports a different slug for your account.
|
||||
- **provider id** `kimi-for-coding-oauth` -- the plugin's `auth` and `chat.params` hooks match on it.
|
||||
- **model id** `kimi-for-coding` -- a stable opencode-side alias. At login and on every token refresh the plugin queries `/coding/v1/models` and rewrites the wire `model` field if the server reports a different slug for your account.
|
||||
|
||||
> **Note.** The provider id is intentionally not `kimi-for-coding`. That id is already published by [models.dev](https://models.dev) and points at a static-API-key flow using a different SDK and auth shape. Using a distinct id keeps the two paths from colliding under a single `opencode auth login` entry.
|
||||
|
||||
@@ -123,8 +129,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, display name, context length, and image-input capability your account should send to Kimi
|
||||
- prints a config hint that uses the discovered display name and leaves context backfill to runtime metadata discovery
|
||||
- discovers the exact model slug, display name, context length, and media-input capabilities your account should send to Kimi
|
||||
- prints a config hint that uses the discovered display name and capabilities
|
||||
|
||||
Access tokens refresh automatically while you use the model.
|
||||
|
||||
@@ -148,11 +154,22 @@ Fastest fix:
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Troubleshooting: Images not working / "this model does not support image input"</strong></summary>
|
||||
|
||||
opencode gates image input on model metadata. If your config block is missing `attachment: true` and `modalities`, opencode strips image parts before they reach Kimi.
|
||||
|
||||
Fix: update your config block to match the one in [Configure](#configure) above -- specifically add `"attachment": true` and `"modalities": { "input": ["text", "image"], "output": ["text"] }` to the model entry.
|
||||
|
||||
The plugin also backfills these capabilities at runtime from `/coding/v1/models` discovery, but the static config must be correct for the initial request.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Login and refresh details</strong></summary>
|
||||
|
||||
- The plugin queries `/coding/v1/models` during login so it can discover the current wire model id and context length for your account.
|
||||
- The plugin also uses that discovery response to backfill image-input support into opencode's runtime model metadata, so pasted or dropped images reach Kimi instead of being downgraded into local error text.
|
||||
- The plugin queries `/coding/v1/models` during login so it can discover the current wire model id, context length, and media capabilities for your account.
|
||||
- The plugin uses that discovery response to backfill image and video input support into opencode's runtime model metadata, so pasted or dropped images reach Kimi instead of being downgraded into local error text.
|
||||
- The printed config hint intentionally omits `limit`, because opencode requires both `limit.context` and `limit.output`, while Kimi's models endpoint only exposes `context_length`.
|
||||
- Model discovery runs again on every token refresh, and a fresh loader instance can re-query `/coding/v1/models` on first use if it needs the current wire model id.
|
||||
- On a `401`, the loader refreshes the access token once and retries the request once.
|
||||
@@ -166,9 +183,9 @@ Select `kimi-for-coding-oauth/kimi-for-coding` in opencode.
|
||||
|
||||
The default variant-cycle keybind is **Ctrl+T**. The variants map as follows:
|
||||
|
||||
- `off` → sends `thinking: { "type": "disabled" }`
|
||||
- `auto` → omits both `thinking` and `reasoning_effort`
|
||||
- `low` / `medium` / `high` → send `thinking: { "type": "enabled" }` plus the matching `reasoning_effort`
|
||||
- `off` -- sends `thinking: { "type": "disabled" }`
|
||||
- `auto` -- omits both `thinking` and `reasoning_effort`
|
||||
- `low` / `medium` / `high` -- send `thinking: { "type": "enabled" }` plus the matching `reasoning_effort`
|
||||
|
||||
These variants only affect Kimi's reasoning request fields. They do not switch models or auth paths. In practice:
|
||||
|
||||
@@ -176,10 +193,14 @@ These variants only affect Kimi's reasoning request fields. They do not switch m
|
||||
- `auto` leaves the decision to the server
|
||||
- `low` / `medium` / `high` ask for enabled thinking with the corresponding reasoning effort
|
||||
|
||||
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.
|
||||
Effort levels `xhigh` and `max` are clamped to `high`, matching kimi-cli's behavior (Kimi's backend does not support higher tiers).
|
||||
|
||||
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.
|
||||
|
||||
#### Usage command
|
||||
|
||||
The plugin registers a `/kimi:usage` TUI slash command that shows your Kimi Code subscription usage (weekly and rolling-window limits) in a compact dialog. Run it from the opencode command palette.
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
@@ -189,16 +210,16 @@ Stock opencode can already talk to generic Moonshot and OpenAI-compatible endpoi
|
||||
|
||||
**What it adds over the generic route.**
|
||||
|
||||
- OAuth device flow with `scope: kimi-code`.
|
||||
- OAuth device flow against `https://auth.kimi.com`.
|
||||
- `@ai-sdk/openai-compatible` pointed at `https://api.kimi.com/coding/v1`.
|
||||
- `prompt_cache_key` set to opencode's session id, for session-scoped cache reuse.
|
||||
- Paired `thinking` + `reasoning_effort` fields.
|
||||
- Paired `thinking` + `reasoning_effort` fields, with effort clamping to match kimi-cli.
|
||||
- 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`.
|
||||
- Runtime model discovery from `/coding/v1/models`, including the server-reported wire slug, `display_name`, `context_length`, and media-input capabilities.
|
||||
- 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.
|
||||
- Streaming, `reasoning_content` deltas, and tool-call schemas are handled upstream by `@ai-sdk/openai-compatible` -- not reimplemented here.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -208,20 +229,19 @@ Stock opencode can already talk to generic Moonshot and OpenAI-compatible endpoi
|
||||
| Field | Wire shape | Purpose |
|
||||
|---|---|---|
|
||||
| `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. |
|
||||
| `thinking` + `reasoning_effort` | `thinking: { type: "enabled" \| "disabled" }` with sibling `reasoning_effort: "low" \| "medium" \| "high"` | Sent together, matching kimi-cli. `xhigh`/`max` clamped to `high`. |
|
||||
| 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 `_common_headers()` at the pinned `KIMI_CLI_VERSION`. |
|
||||
| `/coding/v1/models` discovery | `id`, `display_name`, `context_length`, `supports_image_in`, `supports_video_in` | 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:
|
||||
|
||||
| user effort | `reasoning_effort` | `thinking` |
|
||||
|---|---|---|
|
||||
| `auto` | *(omitted)* | *(omitted)* — server picks dynamically |
|
||||
| `auto` | *(omitted)* | *(omitted)* -- server picks dynamically |
|
||||
| `off` | *(omitted)* | `{ type: "disabled" }` |
|
||||
| `low` / `medium` / `high` | same string | `{ type: "enabled" }` |
|
||||
|
||||
`kimi-cli` does not currently surface this as a separate user-facing level selector. The plugin exposes the same wire-level controls as opencode variants so you can choose them explicitly.
|
||||
| `xhigh` / `max` | `"high"` (clamped) | `{ type: "enabled" }` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -241,19 +261,22 @@ No other state is persisted. Credentials are never written to `~/.kimi/credentia
|
||||
<summary><strong>Architecture at a glance</strong></summary>
|
||||
|
||||
```
|
||||
┌────────────── opencode core ─────────────┐
|
||||
│ │
|
||||
│ auth.login ─▶ plugin.auth.authorize() │ device-code flow, poll
|
||||
│ └─▶ oauth.ts │
|
||||
│ │
|
||||
│ 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
|
||||
└──────────────────────────────────────────┘
|
||||
opencode core
|
||||
──────────────────────────────────────────────────
|
||||
auth.login ──> plugin.auth.authorize() device-code flow, poll
|
||||
└──> oauth.ts
|
||||
|
||||
chat ────────> plugin.loader() custom fetch that:
|
||||
├──> ensureFresh() proactive refresh
|
||||
└──> kimiHeaders() 7 X-Msh-* headers
|
||||
/models slug discovery
|
||||
401 -> force-refresh + retry
|
||||
|
||||
chat.params ─> plugin "chat.params" thinking / reasoning_effort /
|
||||
prompt_cache_key
|
||||
|
||||
/kimi:usage ─> tui.tsx subscription usage dialog
|
||||
└──> usage.ts
|
||||
```
|
||||
|
||||
A full description of the invariants that keep this working is in [`AGENTS.md`](./AGENTS.md), under "Architecture" and "Contracts to keep intact".
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencode-kimi-full",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"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": {
|
||||
|
||||
+33
-8
@@ -19,6 +19,7 @@ type ModelDiscovery = {
|
||||
context_length?: number
|
||||
model_display?: string
|
||||
supports_image_in?: boolean
|
||||
supports_video_in?: boolean
|
||||
}
|
||||
|
||||
type ThinkingType = "enabled" | "disabled"
|
||||
@@ -82,6 +83,14 @@ function pickEffort(options: Record<string, unknown> | undefined) {
|
||||
return typeof effort === "string" ? effort : undefined
|
||||
}
|
||||
|
||||
// kimi-cli clamps xhigh/max to "high" (research/kimi-cli/packages/kosong/
|
||||
// src/kosong/chat_provider/kimi.py, Kimi.with_thinking). Other providers
|
||||
// support higher tiers but Kimi's backend does not.
|
||||
function clampEffort(effort: string): string {
|
||||
if (effort === "xhigh" || effort === "max") return "high"
|
||||
return effort
|
||||
}
|
||||
|
||||
function resolveKimiBodyFields(input: KimiHookInput): KimiBodyFields | undefined {
|
||||
if (input.model.providerID !== PROVIDER_ID) return
|
||||
if (input.model.id !== MODEL_ID) return
|
||||
@@ -93,7 +102,8 @@ function resolveKimiBodyFields(input: KimiHookInput): KimiBodyFields | undefined
|
||||
|
||||
const fields: KimiBodyFields = { prompt_cache_key: input.sessionID }
|
||||
const thinking = asThinking(variantOptions?.thinking) ?? asThinking(modelOptions?.thinking)
|
||||
const effort = pickEffort(variantOptions) ?? pickEffort(modelOptions)
|
||||
const rawEffort = pickEffort(variantOptions) ?? pickEffort(modelOptions)
|
||||
const effort = rawEffort ? clampEffort(rawEffort) : undefined
|
||||
|
||||
if (effort === "auto") return fields
|
||||
if (effort === "off") {
|
||||
@@ -148,6 +158,7 @@ function pickModelInfo(models: KimiModelInfo[]): ModelDiscovery {
|
||||
context_length: picked.context_length,
|
||||
model_display: picked.display_name,
|
||||
supports_image_in: picked.supports_image_in,
|
||||
supports_video_in: picked.supports_video_in,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +193,12 @@ function uniqueStrings(values: string[]) {
|
||||
return [...new Set(values)]
|
||||
}
|
||||
|
||||
function withDiscoveredImageInput<T extends ModelWithDiscoveryMetadata>(model: T, supportsImageIn: boolean | undefined): T {
|
||||
if (supportsImageIn === undefined) return model
|
||||
function withDiscoveredMediaInput<T extends ModelWithDiscoveryMetadata>(
|
||||
model: T,
|
||||
supportsImageIn: boolean | undefined,
|
||||
supportsVideoIn: boolean | undefined,
|
||||
): T {
|
||||
if (supportsImageIn === undefined && supportsVideoIn === undefined) return model
|
||||
|
||||
let changed = false
|
||||
let nextAttachment = model.attachment
|
||||
@@ -197,13 +212,19 @@ function withDiscoveredImageInput<T extends ModelWithDiscoveryMetadata>(model: T
|
||||
|
||||
const currentInputModalities = model.modalities?.input
|
||||
const currentOutputModalities = model.modalities?.output
|
||||
const shouldPatchModalities = supportsImageIn || currentInputModalities?.includes("image") === true
|
||||
const shouldPatchModalities =
|
||||
supportsImageIn || supportsVideoIn ||
|
||||
currentInputModalities?.includes("image") === true ||
|
||||
currentInputModalities?.includes("video") === true
|
||||
if (shouldPatchModalities) {
|
||||
const nextInputModalities = uniqueStrings([
|
||||
"text",
|
||||
...(currentInputModalities ?? []),
|
||||
...(supportsImageIn ? ["image"] : []),
|
||||
]).filter((value) => value !== "image" || supportsImageIn)
|
||||
...(supportsVideoIn ? ["video"] : []),
|
||||
])
|
||||
.filter((value) => value !== "image" || supportsImageIn)
|
||||
.filter((value) => value !== "video" || supportsVideoIn)
|
||||
const nextOutputModalities = uniqueStrings(["text", ...(currentOutputModalities ?? [])])
|
||||
if (
|
||||
!sameStrings(currentInputModalities, nextInputModalities) ||
|
||||
@@ -250,9 +271,10 @@ function withDiscoveredImageInput<T extends ModelWithDiscoveryMetadata>(model: T
|
||||
function applyDiscoveryToModels<T extends Record<string, ModelWithDiscoveryMetadata>>(models: T, discovery: ModelDiscovery): T {
|
||||
const current = models[MODEL_ID]
|
||||
if (!current) return models
|
||||
const next = withDiscoveredImageInput(
|
||||
const next = withDiscoveredMediaInput(
|
||||
withDiscoveredContext(withDiscoveredDisplayName(current, discovery.model_display), discovery.context_length),
|
||||
discovery.supports_image_in,
|
||||
discovery.supports_video_in,
|
||||
)
|
||||
if (next === current) return models
|
||||
return {
|
||||
@@ -261,7 +283,7 @@ function applyDiscoveryToModels<T extends Record<string, ModelWithDiscoveryMetad
|
||||
}
|
||||
}
|
||||
|
||||
function buildConfigBlock(info: { model_id: string; display?: string; supports_image_in?: boolean }) {
|
||||
function buildConfigBlock(info: { model_id: string; display?: string; supports_image_in?: boolean; supports_video_in?: boolean }) {
|
||||
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
|
||||
@@ -289,8 +311,10 @@ function buildConfigBlock(info: { model_id: string; display?: string; supports_i
|
||||
// before the request reaches our loader. Mirror Kimi's discovered
|
||||
// capability here so pasted images survive into the upstream SDK.
|
||||
modelConfig.attachment = true
|
||||
const inputModalities = ["text", "image"]
|
||||
if (info.supports_video_in) inputModalities.push("video")
|
||||
modelConfig.modalities = {
|
||||
input: ["text", "image"],
|
||||
input: inputModalities,
|
||||
output: ["text"],
|
||||
}
|
||||
}
|
||||
@@ -605,6 +629,7 @@ const plugin: Plugin = async ({ client }) => {
|
||||
model_id: discovered.model_id,
|
||||
display: discovered.model_display,
|
||||
supports_image_in: discovered.supports_image_in,
|
||||
supports_video_in: discovered.supports_video_in,
|
||||
})
|
||||
console.log(
|
||||
`\n✓ Authorized for Kimi For Coding (model: ${discovered.model_id}${
|
||||
|
||||
@@ -185,6 +185,9 @@ const EFFORT_MATRIX: Array<{
|
||||
{ in: { reasoning_effort: "low" }, effort: "low", thinkingType: "enabled" },
|
||||
{ in: { reasoning_effort: "medium" }, effort: "medium", thinkingType: "enabled" },
|
||||
{ in: { reasoning_effort: "high" }, effort: "high", thinkingType: "enabled" },
|
||||
// kimi-cli clamps xhigh/max to "high" — Kimi's backend does not support them.
|
||||
{ in: { reasoning_effort: "xhigh" }, effort: "high", thinkingType: "enabled" },
|
||||
{ in: { reasoning_effort: "max" }, effort: "high", thinkingType: "enabled" },
|
||||
{ in: {}, effort: undefined, thinkingType: "enabled" },
|
||||
]
|
||||
|
||||
@@ -398,6 +401,32 @@ test("provider.models: surfaces discovered image input capability so opencode do
|
||||
expect(provider.models[MODEL_ID]!.capabilities.input.image).toBe(false)
|
||||
})
|
||||
|
||||
test("provider.models: surfaces discovered video input in modalities", async () => {
|
||||
mock = installFetchMock((call) => {
|
||||
if (call.url.endsWith("/coding/v1/models")) {
|
||||
return {
|
||||
body: {
|
||||
data: [{
|
||||
id: MODEL_ID,
|
||||
display_name: "Kimi Code",
|
||||
context_length: 262144,
|
||||
supports_image_in: true,
|
||||
supports_video_in: true,
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
return { body: { ok: true } }
|
||||
})
|
||||
const { hooks } = await getHooks()
|
||||
const provider = makeProviderState()
|
||||
const next = await hooks.provider!.models!(provider as any, { auth: validAuth() } as any)
|
||||
const model = next[MODEL_ID] as any
|
||||
expect(model.modalities?.input).toContain("video")
|
||||
expect(model.modalities?.input).toContain("image")
|
||||
expect(model.capabilities.input.image).toBe(true)
|
||||
})
|
||||
|
||||
test("provider.models: preserves an explicit user context limit", async () => {
|
||||
mock = installFetchMock((call) => {
|
||||
if (call.url.endsWith("/coding/v1/models")) {
|
||||
|
||||
Reference in New Issue
Block a user