Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort and vision support#10
Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort and vision support#10Aptul9 wants to merge 5 commits into
Conversation
- Upgrade specificationVersion from v2 to v3, eliminating the compatibility layer that caused the thinking indicator to hang in OpenCode. - Return usage in V3 format (nested objects with total/cacheRead/etc) fixing the 'usage2.inputTokens.total' crash in AI SDK v6. - Filter empty text blocks in message builder to prevent 'cache_control cannot be set for empty text blocks' API errors. - Return empty stream when no user content is available, preventing API errors on follow-up calls with empty messages. - Always use finishReason 'stop' since the CLI executes all tools internally and returns complete results. - Add --effort flag passthrough: reads effort level from OpenCode variant selection and passes it to the Claude CLI. - Upgrade @ai-sdk/provider to 3.0.8 and @ai-sdk/provider-utils to 4.0.23 for V3 type support.
CLI returns input_tokens as non-cached count only, while cacheRead and cacheWrite are separate. OpenCode expects inputTokens.total to include all input tokens (cached + non-cached), then subtracts cache internally. Pass total = input + cacheRead + cacheWrite so OpenCode's subtraction produces the correct non-cached count instead of going negative. Also populate noCache field with the raw non-cached input count.
There was a problem hiding this comment.
Pull request overview
Upgrades this Claude Code CLI provider to the AI SDK v3 specification format, addressing OpenCode v1.4.3 issues (hanging thinking indicator and usage shape crashes) while adding support for passing through an --effort setting to the Claude CLI.
Changes:
- Switch
specificationVersionto"v3"and emit v3-shapedfinishReasonandusageobjects in both generate and stream paths. - Harden message construction by filtering empty text blocks and returning an empty result/stream when there’s no user content to send.
- Add
effortpropagation from model options into spawned Claude CLI args (--effort <level>), and bump AI SDK dependencies.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tsup.config.ts | Disables TS declaration output in the bundler config. |
| src/session-manager.ts | Adds effort to CLI argument builder (--effort). |
| src/message-builder.ts | Filters empty text blocks and returns an empty message when no user content exists. |
| src/claude-code-language-model.ts | Updates provider output to AI SDK v3 shapes, adds empty-message handling, and passes effort through to CLI. |
| package.json | Upgrades AI SDK provider deps and adds ai dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| entry: ["src/index.ts"], | ||
| format: ["esm"], | ||
| dts: true, | ||
| dts: false, |
There was a problem hiding this comment.
dts was changed to false, but the package still declares types: "dist/index.d.ts" (and exports that path). With tsup as the build script, this will stop generating declaration files and ship a broken types entry for consumers. Either re-enable dts generation in tsup, or change the build/publish setup (e.g., run tsc --emitDeclarationOnly into dist and/or remove/update the types/exports.types fields).
| dts: false, | |
| dts: true, |
There was a problem hiding this comment.
Fixed in fd4d0bf — DTS re-enabled. The class no longer declares implements LanguageModelV2 (since it uses specificationVersion: "v3" at runtime), so the type mismatch is resolved via a cast in index.ts. The types field in package.json now correctly points to the generated dist/index.d.ts.
| LanguageModelV2FinishReason, | ||
| LanguageModelV2StreamPart, | ||
| LanguageModelV2Usage, | ||
| LanguageModelV3StreamPart, |
There was a problem hiding this comment.
The file now references LanguageModelV2StreamPart later (e.g., new ReadableStream<LanguageModelV2StreamPart>(...)), but it is no longer imported. At the same time LanguageModelV3StreamPart is imported but unused. This will fail typechecking; either import LanguageModelV2StreamPart (and drop the unused V3 import) or switch the stream typing to LanguageModelV3StreamPart consistently to match specificationVersion = "v3".
| LanguageModelV3StreamPart, | |
| LanguageModelV2StreamPart, |
There was a problem hiding this comment.
Fixed in fd4d0bf — LanguageModelV3StreamPart import removed, stream generics changed to ReadableStream<any>. The class uses V3 format at runtime but keeps V2-compatible method signatures for the type system.
| "@ai-sdk/provider-utils": "^4.0.23", | ||
| "ai": "^6.0.158" |
There was a problem hiding this comment.
ai was added as a runtime dependency, but it isn't imported/used anywhere in this package. If it’s only needed by the host application (e.g., OpenCode), consider removing it or making it a peerDependency to avoid inflating installs and duplicating the host’s dependency graph.
| "@ai-sdk/provider-utils": "^4.0.23", | |
| "ai": "^6.0.158" | |
| "@ai-sdk/provider-utils": "^4.0.23" |
There was a problem hiding this comment.
Fixed in fd4d0bf — ai removed from dependencies. It was only needed for local testing and is not imported anywhere in the package.
- Emit text-start/delta/end per CLI text block instead of one pair for the entire turn. Each text block is saved immediately by OpenCode as a separate part, preventing loss when the stream is aborted before the result event arrives. - Add providerExecuted flag to tool-input-start events so OpenCode does not re-loop looking for unresolved tool calls. - Use last iteration token counts from CLI usage instead of cumulative totals across all internal tool-use iterations — prevents inflated context counters and premature compaction. - Add 5s result fallback timer: if the CLI emits a final text block but never sends a result event, the stream closes gracefully after 5 seconds with accumulated content. - On abort signal, start grace period instead of closing immediately, allowing in-flight CLI messages to be processed. - Fix Copilot review comments: re-enable DTS generation, remove unused LanguageModelV3StreamPart import, remove ai from runtime dependencies. - Remove file-based trace logging.
Convert AI SDK v6 file/image parts into the Anthropic image content
block format expected by the Claude CLI on stream-json input. Accepts
base64 data URIs (the format OpenCode stores on disk), raw base64
strings with mediaType/mime/mimeType, and Uint8Array buffers. Remote
URLs and unsupported media types are skipped with a warning.
Requires the model config in opencode.json to declare
"modalities": { "input": ["text", "image"], "output": ["text"] }
and "attachment": true, otherwise OpenCode filters file parts before
they reach the plugin.
Also ignore the local context.md scratchpad.
Python script that reads ~/.claude/projects/*/\*.jsonl and inserts the conversations into the OpenCode SQLite database so they become visible in the UI. Interactive REPL: type a keyword to filter by title / first prompt / cwd, then a number to import. Handles custom-title events (chats renamed from the Claude Code UI), groups events into turns, remaps tool names to the opencode conventions used by tool-mapping.ts, and uses the per-turn max usage to avoid the double-count bug that would otherwise inflate the context counter. Falls back to the 'global' project when the jsonl cwd does not match any opencode project worktree; session.directory still preserves the original path so the chat appears under the correct folder in the UI.
|
There are too many things in one PR. Please split your PR for easier review. Right now I can't merge your PR as is. |
ok |
|
Closing in favor of three focused PRs (split per maintainer request):
Each PR is independent and builds on top of current |
|
@unixfox done — split into three focused PRs as requested:
Each PR branches from current |
Summary
specificationVersionfromv2tov3, eliminating the V2-to-V3 compatibility layer in AI SDK v6 that caused the thinking indicator to hang indefinitely in OpenCodetotal/cacheRead/cacheWrite/reasoning) — fixes theusage2.inputTokens.totalcrash when running under AI SDK v6 / OpenCode v1.4.3cache_control cannot be set for empty text blocksAPI errorsfinishReason: "stop"since the CLI executes all tools internally and returns complete results — prevents the AI SDK agentic loop from making unnecessary follow-up calls--effortflag passthrough: reads effort level from OpenCode thinking variant selection and passes it to the Claude CLI via--effort <level>@ai-sdk/providerto3.0.8and@ai-sdk/provider-utilsto4.0.23Additional fixes (fd4d0bf)
text-start/text-delta/text-endfor each CLI text block instead of a single pair for the entire turn. Each text block is saved immediately by OpenCode as a separate part, preventing loss of the final response text when the stream is aborted before theresultevent arrives from the CLI.providerExecutedflag totool-input-startevents so OpenCode marks CLI-executed tools correctly and does not re-loop looking for unresolved tool calls.usage.iterations[-1]from CLI result instead of cumulative totals across all internal tool-use iterations — prevents inflated context counters (e.g. 780k instead of 80k) and premature compaction.resultevent, the stream closes gracefully after 5 seconds with accumulated content.LanguageModelV3StreamPartimport, removeaifrom runtime dependencies.Image / vision support (5790c41)
file/imageparts into the Anthropicimagecontent block ({ type: "image", source: { type: "base64", media_type, data } }) expected by the CLI on stream-json input.mediaType/mime/mimeType, andUint8Arraybuffers. Remote URLs and unsupported media types are skipped with a warning.image/png,image/jpeg,image/gif,image/webp."modalities": { "input": ["text", "image"], "output": ["text"] }and"attachment": true, otherwise OpenCode strips file parts before they reach the plugin.Companion tooling (e5e5560)
scripts/import_claude_to_opencode.py: imports existing Claude Code.jsonlsessions (~/.claude/projects/) into the OpenCode SQLite DB so that past conversations are visible in the UI.custom-titleevents from the Claude Code UI; reuses the tool-name mapping fromsrc/tool-mapping.ts; per-turnmax()usage aggregation matching the runtimelastIterationUsage()fix.scripts/README.mdfor commands and examples.OpenCode config example with thinking variants and vision
{ "provider": { "claude-code": { "npm": "opencode-claude-code-plugin", "models": { "sonnet": { "name": "Claude Code Sonnet", "attachment": true, "limit": { "context": 1000000, "output": 64000 }, "capabilities": { "reasoning": true, "toolcall": true }, "modalities": { "input": ["text", "image"], "output": ["text"] }, "variants": { "low": { "thinking": { "type": "adaptive" }, "effort": "low" }, "medium": { "thinking": { "type": "adaptive" }, "effort": "medium" }, "high": { "thinking": { "type": "adaptive" }, "effort": "high" }, "max": { "thinking": { "type": "adaptive" }, "effort": "max" } } } } } } }Test plan
streamText+fullStream(correct event ordering)maxSteps: 5(no unnecessary follow-up steps).jsonlwith verified tokens, tool calls, reasoning, and custom-title