Skip to content

Fix shared tool I/O mappings between Claude Code and OpenCode#11

Merged
dotCipher merged 1 commit intomainfrom
fix/tool-io-mappings
Apr 13, 2026
Merged

Fix shared tool I/O mappings between Claude Code and OpenCode#11
dotCipher merged 1 commit intomainfrom
fix/tool-io-mappings

Conversation

@dotCipher
Copy link
Copy Markdown
Owner

Summary

  • fix Agent/task bridging by requiring and defaulting subagent_type, mapping general correctly, and dropping OpenCode-only agent history fields
  • add missing AskUserQuestion/question and Skill name/argument mappings, plus best-effort WebFetch bridging from OpenCode format to Claude prompt
  • strip Claude-only tool arguments that OpenCode does not accept for Agent, Bash, Read, and Grep, with regression tests covering the mapping paths

Details

  • Agent.subagent_type is now required in the advertised schema because OpenCode task validation requires it
  • inbound Claude general-purpose now maps to OpenCode general instead of build
  • inbound Agent calls seed a default subagent_type: \"general\" when Claude omits it
  • AskUserQuestion.questions[].multiSelect maps to OpenCode multiple and back
  • Skill.skill maps to OpenCode name and back; unsupported Claude args are dropped inbound
  • WebFetch is bridged best-effort:
    • inbound Claude calls become OpenCode webfetch with format: \"markdown\"
    • outbound OpenCode format is converted into a synthetic Claude prompt
  • unsupported Claude-only fields are stripped inbound for:
    • Agent: model, run_in_background, isolation
    • Bash: run_in_background, dangerouslyDisableSandbox
    • Read: pages
    • Grep: output_mode, context flags, type, head_limit, offset, multiline

Verification

  • updated local Claude Code from 2.1.101 to 2.1.104
  • npm test

@dotCipher dotCipher merged commit 73afa33 into main Apr 13, 2026
1 check passed
BNasraoui added a commit to BNasraoui/opencode-claude-bridge that referenced this pull request Apr 15, 2026
This PR was previously based on pre-v1.10.2 main. Upstream has since
landed PRs dotCipher#9 (OAuth), dotCipher#10 (Windows compat), dotCipher#11 (shared tool mappings),
and dotCipher#12 (CI). Rebased onto current main and reworked our improvements
to layer cleanly on top of upstream's tool-mapping work.

## What changed from upstream v1.10.2

Upstream dotCipher#11 uses regex-on-raw-bytes to translate tool arguments as
they stream in. That works when the whole JSON fits in one chunk, but
fails silently when Anthropic splits a tool argument across a TCP
boundary — e.g. "file_" in one chunk and "path" in the next. The regex
never matches a split key, so the consumer receives untranslated
snake_case and produces tool validation errors.

This PR replaces that approach with three structural changes:

1. **SSE event framing (src/stream.ts).** New module with a stateful
   processor that parses SSE frames on \n\n boundaries, buffers
   input_json_delta fragments per content-block index, and emits a
   single translated input_json_delta at content_block_stop.
   Chunk-boundary corruption becomes structurally impossible. Uses
   start() + async loop instead of pull() — Bun's ReadableStream
   doesn't reliably re-invoke pull() when the handler resolves
   without enqueuing (which happens while we're buffering deltas).

2. **JSON.parse for tool arg translation (src/claude-tools.ts).**
   translateToolArgsJsonString parses the assembled JSON, walks the
   object, and serializes back. Key renames on parsed objects can't
   corrupt string values that happen to contain key names (e.g. a
   TodoWrite item whose content literally says "activeForm", or a Bash
   command with "file_path=" inside a heredoc). Replaces every regex
   substitution from dotCipher#11.

3. **Consolidated outbound translation (translateArgsOpencodeToClaude).**
   The inbound path is now a single function; the outbound path in
   index.ts body transform was a wall of nested if-blocks. Extracted
   to a sibling function so inbound/outbound stay in lockstep — next
   bug fix touches one place.

## What was carried forward from dotCipher#11

All of upstream's tool-mapping fixes are preserved:

- Agent: subagent_type required; general-purpose ↔ general; strip
  model/run_in_background/isolation inbound; strip task_id/command
  outbound; default subagent_type=general inbound
- AskUserQuestion: bidirectional multiSelect ↔ multiple per question
- Skill: skill ↔ name; strip args inbound
- WebFetch: strip prompt inbound, inject default format=markdown;
  synthesize prompt from format outbound, strip timeout
- Bash: strip run_in_background/dangerouslyDisableSandbox inbound
- Read: strip pages inbound
- Grep: strip output_mode/-B/-A/-C/context/-n/-i/type/head_limit/
  offset/multiline inbound
- TodoWrite: bidirectional activeForm ↔ priority; cancelled → completed
  outbound

## Other changes

- context_management and output_config.effort only injected for
  thinking-capable models (opus, sonnet-4-6). Previously sent to
  haiku too, which 400s.
- Silent try/catch {} blocks now log via debugLog() gated by
  OPENCODE_CLAUDE_BRIDGE_DEBUG=1. Diagnosable without source changes.
- flush() logs a debug warning if a tool_use block is abandoned
  mid-stream (upstream disconnect between start and stop).

## Tests

81 unit tests (up from 11). Test file imports actual production
modules (createSseProcessor from ./stream, translateToolArgsJsonString
and translateArgsOpencodeToClaude from ./claude-tools) — not local
reimplementations. Assertions parse SSE events and use assert.deepEqual
on object shape rather than substring matches. Covers:

- Every INBOUND_TOOL_NAME_MAP entry (name mapping)
- Every tool's argument translation (including the corruption cases)
- Chunk boundary splits (fragmented args, events across chunks)
- Interleaved tool_use blocks (per-block state isolation)
- Error paths (malformed JSON, translator throws, abandoned blocks)
- Byte-exact pass-through for ping/message_start/text_delta
- Round-trip symmetry between inbound/outbound translation
- All inbound field stripping (Agent/Bash/Read/Grep/Skill)
- All outbound field handling (Agent/AskUserQuestion/WebFetch/TodoWrite)

## Verified end-to-end via opencode run

Read, Edit, Write, Grep, Glob, Bash, Agent, WebFetch, Skill — all
working. WebSearch remains a stub (not in OpenCode's AVAILABLE_TOOLS).
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