Skip to content

chore(llm): replace vendored lib/llm with the published @opencode-ai/ai - #626

Merged
Makisuo merged 1 commit into
mainfrom
chore/unvendor-llm-to-opencode-ai
Aug 25, 2026
Merged

chore(llm): replace vendored lib/llm with the published @opencode-ai/ai#626
Makisuo merged 1 commit into
mainfrom
chore/unvendor-llm-to-opencode-ai

Conversation

@Makisuo

@Makisuo Makisuo commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Why

lib/llm was a vendored copy of opencode's packages/llm, pinned by SHA, kept in sync by scripts/sync-llm-upstream.ts and guarded by a drift workflow. The justification in MAPLE.md was that upstream is private: true — "no npm release to depend on and no semver contract".

That is no longer true. opencode v2 publishes the same core as @opencode-ai/ai: identical dependency set (@smithy/eventstream-codec, @smithy/util-utf8, aws4fetch, effect@4.0.0-rc.111 — our pinned line), the same export tree, and a superset of features. Both of our local deltas also dissolved upstream: it now uses Schema.TaggedError, and it maps the SSE decoder failure into its public error itself.

So the vendor, the SHA pin, the sync script, the drift workflow, and the oxfmt/oxlint/knip ignore entries that existed only for lib/llm all go away. Only apps/api ever depended on it.

What a reviewer should know

Version channels are dev/beta/next with no semver, so the version is pinned exactly (0.0.0-beta-18050). Note bunfig.toml sets minimumReleaseAge = 86400, so a bump has to pick a build older than 24h.

Upstream had moved on in ways that are more than renames. Each of these is a real behavioural surface:

  • ModelLanguageModel; LLMErrorAIError, tag "LLM.Error""AI.Error".
  • LLM.updateRequestLLMRequest.update, whose patch is canonical, so tool choice goes through ToolChoice.make("none") / ToolChoice.named(name).
  • finishReason on LLMResponse and on the finish event is now { normalized, raw }, not a bare string.
  • retryable is gone from AIError and from the provider-error event. Classification moves to the Maple seam as RETRYABLE_REASONS = {RateLimit, ProviderInternal} in platform/Llm.ts — exactly what the old per-reason getters returned. The in-band provider-error retry branch in turn.ts is deleted: only the Bedrock protocol ever set that flag, and Llm.ts deliberately does not import Bedrock, so the branch was already dead for both live providers. Its test is rewritten to pin what remains (terminal, and the provider's message never reaches the client).
  • defaults.limits is gone. Maple's context/output windows now live in a WeakMap side table keyed by the model withLimits returns. providerOptions is not usable for this — everything in it is sent to the provider.
  • providerOptions is no longer namespaced per provider. The adapter lowers the whole record into the request body, so our { openrouter: { reasoning, usage } } was shipping a literal openrouter body key and the reasoning-effort default never reached OpenRouter. This one typechecked cleanly; only the tests caught it. Worth a second pair of eyes.
  • LLM.stream/generate now honestly declare R = LLMClient.Service — the vendored copy erased that requirement with an as cast. It now threads through runChatTurn, and buildTaskTool takes the resolved client as a value and provideServices it, because a tool handler's effect must require nothing.

Two things left alone deliberately: the wire-visible error tag "@maple/llm/LlmCallError" in packages/domain/src/llm.ts (renaming it changes error identifiers), and apps/api's typecheck:test, which is red in ~15 pre-existing files unrelated to this change.

Verification

  • bun typecheck — 39/39 tasks
  • bun run lint
  • bun run --cwd apps/api test — 2439 passed, 277 skipped
  • wrangler deploy --dry-run — 13.6 MB raw / 2.7 MB gzip, with no google-auth-library and no node builtins pulled into the Worker bundle

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

`lib/llm` was vendored because opencode's `packages/llm` was `private: true`
with no npm release to depend on. That premise no longer holds: opencode v2
publishes the same core as `@opencode-ai/ai` — identical dependency set,
superset of the exports — so the vendor, the SHA pin, the sync script and the
drift workflow all go away. Only `apps/api` ever depended on it.

Channels are `dev`/`beta`/`next` with no semver, so the version is pinned
exactly. Effect is `4.0.0-rc.111`, our pinned line.

Upstream had moved on in ways that are not just renames:

- `Model` -> `LanguageModel`; `LLMError` -> `AIError` (`"AI.Error"`).
- `LLM.updateRequest` -> `LLMRequest.update`, whose patch is canonical, so
  tool choice goes through `ToolChoice.make`/`ToolChoice.named`.
- `finishReason` is now `{ normalized, raw }` rather than a bare string.
- `retryable` is gone from `AIError` and from the `provider-error` event.
  Classification moves to the Maple seam as `RETRYABLE_REASONS` in
  `platform/Llm.ts`, matching the old getters exactly. The in-band
  provider-error retry branch is deleted: only the Bedrock protocol ever set
  that flag, and Maple deliberately does not import Bedrock, so it was
  already dead for both live providers.
- `defaults.limits` is gone. Context and output windows now live in a WeakMap
  side table keyed by the model `withLimits` returns; `providerOptions` is not
  usable for this because everything in it is sent to the provider.
- `providerOptions` is no longer namespaced per provider — the adapter lowers
  the whole record into the body, so `{ openrouter: { reasoning, usage } }`
  was shipping a literal `openrouter` body key and the reasoning-effort
  default never reached OpenRouter. This one typechecked; the tests caught it.
- `LLM.stream`/`generate` now honestly declare `R = LLMClient.Service`; the
  vendored copy erased it with a cast. The requirement threads through
  `runChatTurn`, and `buildTaskTool` takes the resolved client as a value and
  provides it, because a tool handler's effect must require nothing.

Verified with `bun typecheck`, `bun run lint`, the full `apps/api` suite, and
`wrangler deploy --dry-run` (2.7 MB gzipped, no `google-auth-library` and no
node builtins in the bundle).
@Makisuo
Makisuo merged commit aecc55b into main Aug 25, 2026
74 of 76 checks passed
@Makisuo
Makisuo deleted the chore/unvendor-llm-to-opencode-ai branch August 25, 2026 12:51
@github-actions

Copy link
Copy Markdown

🍁 Maple PR preview

Warning

Preview cleanup could not be confirmed. The Alchemy teardown outcome was skipped.

Final commit fe80377 · View workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant