mirror of
https://github.com/lemon07r/opencode-kimi-full.git
synced 2026-07-18 08:05:52 +02:00
Use v1 PluginModule format to fix Windows plugin loading
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.
This commit is contained in:
@@ -33,7 +33,7 @@ Three files, 1 job each. Do not add a fourth unless the existing three genuinely
|
||||
| `src/constants.ts` | Pinned strings that must mirror upstream kimi-cli (version, endpoints, client id, scope). |
|
||||
| `src/headers.ts` | The seven `X-Msh-*` / UA headers + the persistent `~/.kimi/device_id` file. |
|
||||
| `src/oauth.ts` | Device-code start, device-code poll, refresh-token exchange, and `GET /coding/v1/models` discovery. |
|
||||
| `src/index.ts` | Plugin entry. Wires `auth` (login + loader) plus the Kimi chat hooks/body rewrite. |
|
||||
| `src/index.ts` | Plugin entry (v1 `PluginModule` format). Wires `auth` (login + loader) plus the Kimi chat hooks/body rewrite. |
|
||||
|
||||
Data flow on a chat request:
|
||||
|
||||
@@ -66,7 +66,7 @@ These are the invariants that, if broken, silently route requests onto the wrong
|
||||
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` 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.
|
||||
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.
|
||||
|
||||
@@ -88,7 +88,7 @@ These are the invariants that, if broken, silently route requests onto the wrong
|
||||
- ❌ Don't call out to other files to "share" the kimi-cli credentials. Different OAuth consumers must have independent refresh-token chains or one will invalidate the other.
|
||||
- ❌ Don't introduce a build step. The plugin ships as `.ts` and opencode's bun-based loader handles it.
|
||||
- ❌ Don't add tests that require real Kimi credentials and check them in. If you add offline unit tests, put them under `test/` and mock `fetch`.
|
||||
- ❌ Don't add named exports to `src/index.ts`. See rule 9.
|
||||
- ❌ Don't add named exports to `src/index.ts` or change the default export away from the `{ id, server }` PluginModule shape. See rule 9.
|
||||
|
||||
### How to verify a change
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencode-kimi-full",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.8",
|
||||
"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": {
|
||||
@@ -18,7 +18,8 @@
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": "./src/index.ts",
|
||||
"./server": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
+16
-7
@@ -1,16 +1,19 @@
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
import type { Plugin, PluginModule } from "@opencode-ai/plugin"
|
||||
import { API_BASE_URL, MODEL_ID, PROVIDER_ID, REFRESH_SAFETY_WINDOW_MS } from "./constants.ts"
|
||||
import { kimiHeaders } from "./headers.ts"
|
||||
import { type KimiModelInfo, listModels, pollDeviceToken, refreshToken, startDeviceAuth } from "./oauth.ts"
|
||||
|
||||
// IMPORTANT: this module must have exactly ONE export — the default plugin
|
||||
// function. opencode's plugin loader (packages/opencode/src/plugin/index.ts →
|
||||
// getLegacyPlugins) iterates every export and throws "Plugin export is not a
|
||||
// function" if any named export is not a function. Keep constants in
|
||||
// constants.ts and import them here.
|
||||
// IMPORTANT: this module must have exactly ONE export — the default
|
||||
// PluginModule object. opencode's plugin loader detects the v1 format
|
||||
// ({ id, server }) via readV1Plugin *before* falling back to
|
||||
// getLegacyPlugins — which iterates every export and throws "Plugin export
|
||||
// is not a function" on any non-callable value. The v1 path is more
|
||||
// reliable on Windows where Bun standalone dynamic imports can produce
|
||||
// module namespace objects with unexpected non-function metadata.
|
||||
// Keep constants in constants.ts and import them here.
|
||||
|
||||
type OAuthAuth = {
|
||||
type: "oauth"
|
||||
@@ -688,4 +691,10 @@ const plugin: Plugin = async ({ client }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default plugin
|
||||
// v1 PluginModule format — bypasses getLegacyPlugins entirely.
|
||||
// For npm-sourced plugins, id is optional (falls back to package.json name),
|
||||
// but we set it explicitly for clarity.
|
||||
export default {
|
||||
id: "opencode-kimi-full",
|
||||
server: plugin,
|
||||
} satisfies PluginModule
|
||||
|
||||
+16
-10
@@ -1,17 +1,23 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import * as mod from "../src/index.ts"
|
||||
|
||||
// Regression guard for the 1.0.0 bug:
|
||||
// opencode's plugin loader (packages/opencode/src/plugin/index.ts →
|
||||
// getLegacyPlugins) iterates every export of the plugin module and throws
|
||||
// "Plugin export is not a function" if any export is not callable. The
|
||||
// published v1.0.0 re-exported PROVIDER_ID (a string), which broke loading
|
||||
// silently — `opencode auth login` just did not list the provider.
|
||||
// Regression guard for the 1.0.0 bug + the Windows loading fix:
|
||||
// opencode's plugin loader first tries readV1Plugin (detect mode) on the
|
||||
// default export. If it finds { id?, server } it uses the v1 path and
|
||||
// never touches getLegacyPlugins. The legacy path iterates every export and
|
||||
// throws "Plugin export is not a function" on any non-callable value — a
|
||||
// problem that surfaced on Windows where Bun standalone dynamic imports can
|
||||
// produce module namespaces with extra non-function metadata.
|
||||
//
|
||||
// Keep this file cheap and dependency-free; do not import anything that
|
||||
// makes network calls.
|
||||
test("src/index.ts exports exactly one default export which is a function", () => {
|
||||
// This test ensures the module exports exactly one default PluginModule
|
||||
// object with a callable `server` and no named exports.
|
||||
test("src/index.ts exports exactly one default PluginModule object", () => {
|
||||
const keys = Object.keys(mod)
|
||||
expect(keys).toEqual(["default"])
|
||||
expect(typeof (mod as { default: unknown }).default).toBe("function")
|
||||
const plugin = (mod as { default: unknown }).default
|
||||
expect(typeof plugin).toBe("object")
|
||||
expect(plugin).not.toBeNull()
|
||||
const obj = plugin as Record<string, unknown>
|
||||
expect(typeof obj.server).toBe("function")
|
||||
expect("id" in obj).toBe(true)
|
||||
})
|
||||
|
||||
+3
-1
@@ -2,7 +2,9 @@ import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { test, expect, afterEach } from "bun:test"
|
||||
import plugin from "../src/index.ts"
|
||||
import pluginModule from "../src/index.ts"
|
||||
|
||||
const plugin = pluginModule.server
|
||||
import { MODEL_ID, PROVIDER_ID, REFRESH_SAFETY_WINDOW_MS } from "../src/constants.ts"
|
||||
import { installFetchMock } from "./_util/fetchMock.ts"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user