From d0ba94e0ce5bc5d572eda9934258a2baff457b41 Mon Sep 17 00:00:00 2001 From: Futuri-Risk Date: Tue, 18 Aug 2026 09:58:30 +1000 Subject: [PATCH] fix(plugin): add V1 default export descriptor to stop legacy loader probing every helper opencode's plugin loader prefers a { id, server } default export (readV1Plugin) and invokes only `server`. Without one, the legacy fallback invokes every function-typed export as if it were the plugin; the pure helpers throw on the plugin-init argument and every worker logs "failed to load plugin" (messages.filter / finish.includes / messages.map is not a function) even though the proxy still serves. Fixes #86 --- .gitattributes | 1 + README.md | 6 ++++ dist/llm-proxy.js | 2 ++ index.js | 10 +++++++ index.test.js | 72 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3905704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +dist/** text eol=lf diff --git a/README.md b/README.md index ee08615..7850073 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,12 @@ Streaming uses OpenCode's `client.event.subscribe()` SSE stream. Text deltas are --- +## Compatibility + +The plugin ships a V1 plugin descriptor (`export default { id, server }`). OpenCode detects it (present in every release checked back to 1.15) and loads the plugin cleanly. On older builds the legacy loader probes every exported function, so you may see a harmless `failed to load plugin ... is not a function` line per worker in the startup log — the proxy still starts and serves normally. + +--- + ## Limitations - Media support depends on the selected model's advertised image, audio, video, and PDF/file capabilities diff --git a/dist/llm-proxy.js b/dist/llm-proxy.js index 390c3b0..7df1f51 100644 --- a/dist/llm-proxy.js +++ b/dist/llm-proxy.js @@ -3389,6 +3389,7 @@ var OpenAIProxyPlugin = async ({ client }) => { } }; }; +var index_default = { id: "opencode-llm-proxy", server: OpenAIProxyPlugin }; export { OpenAIProxyPlugin, applyAnthropicToolChoice, @@ -3399,6 +3400,7 @@ export { buildToolsMap, createProxyFetchHandler, createSseQueue, + index_default as default, extractAssistantText, extractGeminiSystemInstruction, mapFinishReason, diff --git a/index.js b/index.js index 9466daf..1561d4d 100644 --- a/index.js +++ b/index.js @@ -2770,3 +2770,13 @@ export const OpenAIProxyPlugin = async ({ client }) => { }, } } + +// V1 plugin descriptor. opencode's plugin loader (readV1Plugin in +// packages/opencode/src/plugin/shared.ts) prefers a default export of this +// shape and invokes only `server`. Without it, the loader falls back to the +// legacy path, which invokes EVERY function-typed export as if it were the +// plugin — the pure helpers (buildPrompt, mapFinishReason, ...) then throw on +// the plugin-init argument and every worker logs "failed to load plugin" +// (see issue #86). Exporting the descriptor keeps all named exports intact +// for direct imports and tests. +export default { id: "opencode-llm-proxy", server: OpenAIProxyPlugin } diff --git a/index.test.js b/index.test.js index 57ada84..c27c36e 100644 --- a/index.test.js +++ b/index.test.js @@ -3,6 +3,7 @@ import assert from "node:assert/strict" import { setTimeout as delay } from "node:timers/promises" import { PassThrough } from "node:stream" +import * as mod from "./index.js" import { createProxyFetchHandler, createSseQueue, @@ -3438,21 +3439,25 @@ describe("mcp-tool-bridge runStdioServer", () => { // OpenAIProxyPlugin (plugin entrypoint / server bootstrap) // --------------------------------------------------------------------------- +// Shared Bun.serve mock: the plugin starts its HTTP server via Bun.serve, so +// tests stub globalThis.Bun and inspect the options it was called with. +const PROXY_STATE_KEY = "__opencodeOpenAIProxyState" + +function withMockedBun(serve, run) { + const savedBun = globalThis.Bun + delete globalThis[PROXY_STATE_KEY] + globalThis.Bun = { serve } + return Promise.resolve() + .then(run) + .finally(() => { + if (savedBun === undefined) delete globalThis.Bun + else globalThis.Bun = savedBun + delete globalThis[PROXY_STATE_KEY] + }) +} + describe("OpenAIProxyPlugin", () => { - const STATE_KEY = "__opencodeOpenAIProxyState" - - function withMockedBun(serve, run) { - const savedBun = globalThis.Bun - delete globalThis[STATE_KEY] - globalThis.Bun = { serve } - return Promise.resolve() - .then(run) - .finally(() => { - if (savedBun === undefined) delete globalThis.Bun - else globalThis.Bun = savedBun - delete globalThis[STATE_KEY] - }) - } + const STATE_KEY = PROXY_STATE_KEY it("starts a Bun server and records it in global state", async () => { const calls = [] @@ -3587,3 +3592,42 @@ describe("OpenAIProxyPlugin", () => { ) }) }) + +// --------------------------------------------------------------------------- +// V1 plugin descriptor (default export) +// --------------------------------------------------------------------------- + +describe("V1 plugin default export", () => { + it("exposes a { id, server } descriptor so opencode's loader skips the legacy fallback", async () => { + assert.ok(mod.default, "index.js must have a default export") + assert.equal(typeof mod.default, "object", "default export must be a V1 descriptor object") + assert.equal(mod.default.id, "opencode-llm-proxy") + assert.equal(typeof mod.default.server, "function", "descriptor.server must be the plugin factory") + }) + + it("descriptor.server starts the proxy when invoked, like the loader would", async () => { + const calls = [] + + await withMockedBun( + (opts) => { + calls.push(opts) + return { stopped: false } + }, + async () => { + process.env.OPENCODE_LLM_PROXY_HOST = "127.0.0.1" + process.env.OPENCODE_LLM_PROXY_PORT = "4997" + try { + const result = await mod.default.server({ client: createClient() }) + + assert.equal(typeof result["chat.params"], "function") + assert.equal(calls.length, 1) + assert.equal(calls[0].port, 4997) + } finally { + delete process.env.OPENCODE_LLM_PROXY_HOST + delete process.env.OPENCODE_LLM_PROXY_PORT + } + }, + ) + }) +}) +