chore(llm): replace vendored lib/llm with the published @opencode-ai/ai - #626
Merged
Conversation
`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).
🍁 Maple PR previewWarning Preview cleanup could not be confirmed. The Alchemy teardown outcome was Final commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
lib/llmwas a vendored copy of opencode'spackages/llm, pinned by SHA, kept in sync byscripts/sync-llm-upstream.tsand guarded by a drift workflow. The justification inMAPLE.mdwas that upstream isprivate: 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 usesSchema.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/llmall go away. Onlyapps/apiever depended on it.What a reviewer should know
Version channels are
dev/beta/nextwith no semver, so the version is pinned exactly (0.0.0-beta-18050). Notebunfig.tomlsetsminimumReleaseAge = 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:
Model→LanguageModel;LLMError→AIError, tag"LLM.Error"→"AI.Error".LLM.updateRequest→LLMRequest.update, whose patch is canonical, so tool choice goes throughToolChoice.make("none")/ToolChoice.named(name).finishReasononLLMResponseand on thefinishevent is now{ normalized, raw }, not a bare string.retryableis gone fromAIErrorand from theprovider-errorevent. Classification moves to the Maple seam asRETRYABLE_REASONS = {RateLimit, ProviderInternal}inplatform/Llm.ts— exactly what the old per-reason getters returned. The in-band provider-error retry branch inturn.tsis deleted: only the Bedrock protocol ever set that flag, andLlm.tsdeliberately 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.limitsis gone. Maple's context/output windows now live in aWeakMapside table keyed by the modelwithLimitsreturns.providerOptionsis not usable for this — everything in it is sent to the provider.providerOptionsis no longer namespaced per provider. The adapter lowers the whole record into the request body, so our{ openrouter: { reasoning, usage } }was shipping a literalopenrouterbody 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/generatenow honestly declareR = LLMClient.Service— the vendored copy erased that requirement with anascast. It now threads throughrunChatTurn, andbuildTaskTooltakes the resolved client as a value andprovideServices it, because a tool handler's effect must require nothing.Two things left alone deliberately: the wire-visible error tag
"@maple/llm/LlmCallError"inpackages/domain/src/llm.ts(renaming it changes error identifiers), andapps/api'stypecheck:test, which is red in ~15 pre-existing files unrelated to this change.Verification
bun typecheck— 39/39 tasksbun run lintbun run --cwd apps/api test— 2439 passed, 277 skippedwrangler deploy --dry-run— 13.6 MB raw / 2.7 MB gzip, with nogoogle-auth-libraryand no node builtins pulled into the Worker bundle🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.