First: the plugin works fine. This is cosmetic log noise, not an outage — I'm filing it because it looks alarming in logs and a one-line change silences it cleanly. Deciding priority is entirely up to you.
What happened
On opencode 1.18.4, every opencode worker logs one failed to load plugin error at startup:
level=ERROR message="failed to load plugin" path=opencode-llm-proxy
error="messages.filter is not a function. (In 'messages.filter((message) => message.role !== \"system\" && message.role !== \"developer\")', 'messages.filter' is undefined)"
The proxy still starts and serves correctly. Functionality is unaffected.
Root cause (verified against opencode's source)
This package exports ~27 named functions (buildPrompt, buildSystemPrompt, OpenAIProxyPlugin, …) with no export default.
opencode's plugin loader (packages/opencode/src/plugin/index.ts, getLegacyPlugins) falls back to the legacy path when there's no V1 default export, and calls every function-typed export as if it were the plugin:
function getLegacyPlugins(mod) {
for (const entry of Object.values(mod)) {
const plugin = getServerPlugin(entry) // typeof === "function" → treated as plugin
if (!plugin) throw new TypeError("Plugin export is not a function")
result.push(plugin)
}
return result
}
// then: for (const server of getLegacyPlugins(mod)) { await server(input, options) }
So buildPrompt() gets invoked with the plugin-init object { client, project, … } as its messages arg → {client}.filter is undefined → throws → logged as failed to load plugin.
Why it's non-fatal: OpenAIProxyPlugin is reached first in iteration order, starts Bun.serve as a side effect, then buildPrompt (5th) throws. The server is already up by the time the error fires. I confirmed this ordering by simulating applyPlugin against the real module:
OK OpenAIProxyPlugin -> started the server (side effect)
OK applyAnthropicToolChoice / applyGeminiToolChoice / applyOpenAIToolChoice
THROW buildPrompt -> "messages.filter is not a function"
Steps to reproduce
- opencode 1.18.4 (Bun runtime), plugin installed via npm and listed in
opencode.json "plugin": ["opencode-llm-proxy"]
- Start opencode
- Observe one
failed to load plugin error per worker in the startup log
GET http://127.0.0.1:4010/health → still returns {"healthy":true} (proxy works)
Expected behaviour
No spurious failed to load plugin errors at startup; the plugin loads as a proper V1 plugin.
Suggested fix (tested)
Add a V1 default export so opencode's readV1Plugin (shared.ts) detects it and calls only server(), skipping the legacy fallback entirely:
// at the end of index.js, after OpenAIProxyPlugin is defined
export default { id: "opencode-llm-proxy", server: OpenAIProxyPlugin }
I verified readV1Plugin detects this shape (mod.default is a record with id + server), and that with the export present the loader takes the V1 branch and only invokes OpenAIProxyPlugin. The same shape is used by other working plugins.
Notes
- The docs show
export const MyPlugin: Plugin = async (...) (named export) as a valid pattern, which is presumably why the plugin was structured this way — that pattern only loads cleanly when it's the only export. With 27 helpers exported, the legacy fallback calls all of them. A one-line note in the docs ("if your plugin exports helpers, add a V1 default to avoid the legacy fallback") would also help.
- The deeper fix could land on opencode's side too (the legacy loader could prefer a single obvious entrypoint rather than every function), but the plugin-side change is trivial and immediately effective.
- Happy to open a PR if useful.
Environment
- opencode-llm-proxy: 1.10.0 (also reproduced in 1.9.0 — same export shape)
- opencode: 1.18.4
- Runtime/OS: Bun, Windows 11
First: the plugin works fine. This is cosmetic log noise, not an outage — I'm filing it because it looks alarming in logs and a one-line change silences it cleanly. Deciding priority is entirely up to you.
What happened
On opencode 1.18.4, every opencode worker logs one
failed to load pluginerror at startup:The proxy still starts and serves correctly. Functionality is unaffected.
Root cause (verified against opencode's source)
This package exports ~27 named functions (
buildPrompt,buildSystemPrompt,OpenAIProxyPlugin, …) with noexport default.opencode's plugin loader (
packages/opencode/src/plugin/index.ts,getLegacyPlugins) falls back to the legacy path when there's no V1 default export, and calls every function-typed export as if it were the plugin:So
buildPrompt()gets invoked with the plugin-init object{ client, project, … }as itsmessagesarg →{client}.filteris undefined → throws → logged asfailed to load plugin.Why it's non-fatal:
OpenAIProxyPluginis reached first in iteration order, startsBun.serveas a side effect, thenbuildPrompt(5th) throws. The server is already up by the time the error fires. I confirmed this ordering by simulatingapplyPluginagainst the real module:Steps to reproduce
opencode.json"plugin": ["opencode-llm-proxy"]failed to load pluginerror per worker in the startup logGET http://127.0.0.1:4010/health→ still returns{"healthy":true}(proxy works)Expected behaviour
No spurious
failed to load pluginerrors at startup; the plugin loads as a proper V1 plugin.Suggested fix (tested)
Add a V1 default export so opencode's
readV1Plugin(shared.ts) detects it and calls onlyserver(), skipping the legacy fallback entirely:I verified
readV1Plugindetects this shape (mod.defaultis a record withid+server), and that with the export present the loader takes the V1 branch and only invokesOpenAIProxyPlugin. The same shape is used by other working plugins.Notes
export const MyPlugin: Plugin = async (...)(named export) as a valid pattern, which is presumably why the plugin was structured this way — that pattern only loads cleanly when it's the only export. With 27 helpers exported, the legacy fallback calls all of them. A one-line note in the docs ("if your plugin exports helpers, add a V1 default to avoid the legacy fallback") would also help.Environment