Skip to content
This repository was archived by the owner on Apr 26, 2026. It is now read-only.

Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort and vision support#10

Closed
Aptul9 wants to merge 5 commits into
unixfox:masterfrom
Aptul9:fix/v3-spec-effort-support
Closed

Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort and vision support#10
Aptul9 wants to merge 5 commits into
unixfox:masterfrom
Aptul9:fix/v3-spec-effort-support

Conversation

@Aptul9

@Aptul9 Aptul9 commented Apr 14, 2026

Copy link
Copy Markdown

Summary

  • Upgrade specificationVersion from v2 to v3, eliminating the V2-to-V3 compatibility layer in AI SDK v6 that caused the thinking indicator to hang indefinitely in OpenCode
  • Return usage in V3 format (nested objects with total/cacheRead/cacheWrite/reasoning) — fixes the usage2.inputTokens.total crash when running under AI SDK v6 / OpenCode v1.4.3
  • 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 (follow-up calls after tool execution), preventing API errors on requests with empty messages
  • Always use finishReason: "stop" since the CLI executes all tools internally and returns complete results — prevents the AI SDK agentic loop from making unnecessary follow-up calls
  • Add --effort flag passthrough: reads effort level from OpenCode thinking variant selection and passes it to the Claude CLI via --effort <level>
  • Upgrade @ai-sdk/provider to 3.0.8 and @ai-sdk/provider-utils to 4.0.23

Additional fixes (fd4d0bf)

  • Per-block text emission: emit text-start/text-delta/text-end for 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 the result event arrives from the CLI.
  • providerExecuted on tool-input-start: add the providerExecuted flag to tool-input-start events so OpenCode marks CLI-executed tools correctly and does not re-loop looking for unresolved tool calls.
  • Last-iteration token counts: use 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.
  • Result fallback timer (5s): if the CLI emits a final text block but never sends a result event, the stream closes gracefully after 5 seconds with accumulated content.
  • Abort grace period: on abort signal, start a 5s grace period instead of closing immediately, allowing in-flight CLI messages to be processed.
  • Copilot review fixes: re-enable DTS generation, remove unused LanguageModelV3StreamPart import, remove ai from runtime dependencies.

Image / vision support (5790c41)

  • Convert AI SDK v6 file / image parts into the Anthropic image content block ({ type: "image", source: { type: "base64", media_type, data } }) expected by the CLI on stream-json input.
  • Accepts base64 data URIs (the format OpenCode stores on disk), raw base64 with mediaType/mime/mimeType, and Uint8Array buffers. Remote URLs and unsupported media types are skipped with a warning.
  • Supported media types: image/png, image/jpeg, image/gif, image/webp.
  • Requires the model config to declare "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 .jsonl sessions (~/.claude/projects/) into the OpenCode SQLite DB so that past conversations are visible in the UI.
  • Interactive REPL with keyword search, pagination, batch selection; honors custom-title events from the Claude Code UI; reuses the tool-name mapping from src/tool-mapping.ts; per-turn max() usage aggregation matching the runtime lastIterationUsage() fix.
  • See scripts/README.md for 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

  • Verified stream events with AI SDK v6 streamText + fullStream (correct event ordering)
  • Verified stream completes with maxSteps: 5 (no unnecessary follow-up steps)
  • E2E test with 28 tool calls: final text (1128 chars) arrives correctly
  • E2E test with exact user prompt (21 tools): final text (2556 chars) arrives correctly
  • Tested in OpenCode v1.4.3: no API errors, no hanging thinking indicator, no truncated responses
  • Tested effort level selection in OpenCode UI
  • Token counts no longer inflated (last iteration only)
  • Image attachment round-trip: PNG uploaded via OpenCode UI, described correctly by Haiku / Sonnet / Opus
  • Session importer: end-to-end import of real .jsonl with verified tokens, tool calls, reasoning, and custom-title

Aptul9 added 2 commits April 14, 2026 10:49
- 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 specificationVersion to "v3" and emit v3-shaped finishReason and usage objects 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 effort propagation 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.

Comment thread tsup.config.ts Outdated
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
dts: false,

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
dts: false,
dts: true,

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/claude-code-language-model.ts Outdated
LanguageModelV2FinishReason,
LanguageModelV2StreamPart,
LanguageModelV2Usage,
LanguageModelV3StreamPart,

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Suggested change
LanguageModelV3StreamPart,
LanguageModelV2StreamPart,

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fd4d0bfLanguageModelV3StreamPart 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.

Comment thread package.json Outdated
Comment on lines +25 to +26
"@ai-sdk/provider-utils": "^4.0.23",
"ai": "^6.0.158"

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"@ai-sdk/provider-utils": "^4.0.23",
"ai": "^6.0.158"
"@ai-sdk/provider-utils": "^4.0.23"

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fd4d0bfai removed from dependencies. It was only needed for local testing and is not imported anywhere in the package.

Aptul9 added 3 commits April 15, 2026 06:31
- 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.
@Aptul9 Aptul9 changed the title Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort support Upgrade to AI SDK v3 spec, fix hanging thinking indicator, add --effort and vision support Apr 17, 2026
@unixfox

unixfox commented Apr 17, 2026

Copy link
Copy Markdown
Owner

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.

@Aptul9

Aptul9 commented Apr 17, 2026

Copy link
Copy Markdown
Author

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

@Aptul9

Aptul9 commented Apr 17, 2026

Copy link
Copy Markdown
Author

Closing in favor of three focused PRs (split per maintainer request):

Each PR is independent and builds on top of current master. Reviewing them separately should be easier. No code is lost — the three branches together reproduce the state of this PR.

@Aptul9 Aptul9 closed this Apr 17, 2026
@Aptul9

Aptul9 commented Apr 17, 2026

Copy link
Copy Markdown
Author

@unixfox done — split into three focused PRs as requested:

Each PR branches from current master and can be reviewed in isolation. Happy to rebase or iterate if any of them still feels too broad.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants