Skip to content

Spurious 'failed to load plugin' error on opencode 1.18.x (cosmetic — proxy still works); one-line V1 default export silences it #86

Description

@Futuri-Risk

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

  1. opencode 1.18.4 (Bun runtime), plugin installed via npm and listed in opencode.json "plugin": ["opencode-llm-proxy"]
  2. Start opencode
  3. Observe one failed to load plugin error per worker in the startup log
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions