fix(opencode): skip falsy plugin hook returns instead of crashing provider listing#36102
fix(opencode): skip falsy plugin hook returns instead of crashing provider listing#36102CryptArchy wants to merge 3 commits into
Conversation
A plugin entry that resolves to no hooks (a stray helper export on a legacy module, or a v1 server() resolving undefined) was pushed into the hooks array unchecked. Consumers dereference entries directly — hook.provider in the Provider state build — so one undefined entry 500s /config/providers and makes the whole instance unusable (attach and web UI both die). Skip falsy returns with a warning naming the offending plugin spec, guard the internal-plugin push the same way, and pin the behavior with regression tests for both module shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: Related PR FoundPR #35489: Why it's related: This PR addresses a similar issue in plugin loading - skipping invalid plugin exports rather than crashing. While PR #36102 specifically targets falsy hook returns after evaluation, PR #35489 likely handles related plugin validation and export filtering at an earlier stage. Both are part of making the plugin system more resilient to bad plugin exports and helper functions mixed into plugin modules. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
…module
TTL: every chat completion carries LM Studio's idle ttl (default 3600s,
0 = resident) via providerOptions.openaiCompatible — LMS's REST load/chat
endpoints reject the key; the OpenAI-compat path accepts it and resets the
idle countdown per request, so eviction is delegated to LMS with no
client-side unload loop.
Context cap: models load at min(contextLength, max_context_length), default
8192, overridable globally and per-model — the VRAM knob, distinct from the
UI-facing limit.context. Discovery maps limit.context from the smallest
loaded instance (or max for cold models), limit.output as
min(context/4, 8192); tools are always advertised (trained_for_tool_use is
diagnostic-only now). @opencode-ai/plugin exact-pinned to 1.17.15.
Loader safety (the 0.3.0-dev incident): a helper export on the entry module
broke OpenCode provider listing — the legacy loader calls every function
export as a plugin and pushes undefined returns into its hooks array
unchecked, killing /config/providers (500) and with it attach + web UI,
identically on 1.16.2 and 1.17.x. applyCompletionTtl now lives in
src/ttl.ts, the default export is the v1 PluginModule shape ({ id, server })
so named exports are ignored, and two guards pin the contract:
tests/plugin-exports.test.ts and docker/smoke.sh (boots the harness against
latest OpenCode and asserts /config/providers lists lmstudio — doubles as
the version-bump check; needs a reachable LM Studio). README documents
OpenCode ≥1.16.2 (validated on 1.17.15; the plugin pin is types-only).
Upstream hardening: anomalyco/opencode#36102.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vider listing Ported from upstream anomalyco#36102.
…vider listing (#13) Ported from upstream anomalyco#36102.
Issue for this PR
Closes #35772
Type of change
What does this PR do?
A plugin entry that resolves to no hooks gets pushed into the hooks array as-is in
applyPlugin. The Provider state build later doesconst p = hook.provider(provider/provider.ts:1372) one line before its null-guard, so a singleundefinedentry 500sGET /config/providersand takesopencode attachand the web UI down with it — the error in #35772.The easiest way to hit it: a stray helper export on a legacy-shaped plugin module. Every function export is loaded as a plugin, so the helper is called and its
undefinedreturn lands in the hooks array. That's how I hit it with my own plugin (identical on 1.16.2 and 1.17.x). A v1 module whoseserver()resolvesundefineddoes the same. It also explains "happens with plugins removed": file-based plugins are auto-discovered from{plugin,plugins}/*.{ts,js}in every config dir, so a leftover file counts even with an emptypluginarray.Fix: skip falsy hook returns on the v1, legacy, and internal-plugin paths, and log a warning naming the plugin spec. I kept the guard at the source instead of adding
?.at the consumers so the offending plugin gets named instead of silently ignored.How did you verify your code works?
packages/opencode/test/plugin/falsy-hooks.test.tscovers both module shapes; it fails 2/2 without the src change and passes with it.bun test test/plugin/andtest/provider/provider.test.tsare green;tsgo --noEmitand prettier clean.opencode servewith a plugin carrying a stray helper export (/config/providers→ 500 on every poll); removing the export restores 200. The loader guard itself is covered by the new tests.Screenshots / recordings
n/a — no UI change.
Checklist