Repository navigation
Description
claude-code-proxy + commandcode-proxy cannot emit client tool calls — and degrade silently (finish_reason:"stop" + prose)
Found by the 2026-11-05 tool-call audit (code inventory: ~/workspace/scratch/toolcall-audit-2026-11-04/code-inventory.md §8/§9). Both providers are currently disabled in the deployed config, so this is latent — it becomes live the moment either door is re-enabled or a route seats one.
The defect
bin/claude-code-proxy.mjs(812 lines): the request'stoolsarray is ignored entirely (no reference tobody.toolsoutside transcript flattening). Responses are text-only (finish_reason:"stop"streaming + non-streaming builders). Claude Code CLItool_useblocks are consumed internally, filtered to text; tool approvals are auto-allowed with the CLI's own input. Client history is flattened to a transcript ([Tool Call: name(args)],Tool Result (<id>): …).bin/commandcode-proxy.mjs(1349 lines): same shape. "Agency" mode: if the request carries tools, the CommandCode CLI runs tool-enabled with its own internal tools; the client's tool list is reduced to a name/description manifest embedded in prompt prose (parameter schemas are dropped), and only the CLI's final text summary returns.- muse-proxy's warm/exec fallback paths (
bin/muse-proxy.mjs) have the same property: when direct mode fails, the conversation is flattened to a prompt for themuseCLI, so a session can flip between real tool calls (direct) and simulated agency (fallback) turn by turn, with nothing surfacing which path served the turn.
Net effect: an agent routed to these doors that requests tools gets plausible prose saying work happened — under the CLI's tool semantics, in the CLI's workdir — instead of OpenCode tool executions. Nothing in any layer reports the substitution.
Doctrine frame (Drew, 2026-11-05)
Direct providers are emulation of the first-party client's wire behavior in our own provider code; the vendor binary is not a runtime dependency beyond login/bootstrap calls. These three doors are the opposite shape (vendor CLI at inference time), which is exactly why they can't carry client tool calls. Their end state is the façade migration (opencode#1411 commandcode, opencode#1412 claude-code, opencode#1155 muse-proxy). Until migration, they must fail closed.
Ask (interim, small)
- Each proxy refuses tool-bearing requests loudly (a clear 4xx naming the limitation) instead of answering with prose — or the config/route layer marks these providers as text-only so the runner never offers them tools and surfaces the constraint at seat time.
- muse-proxy: when a request carries tools, the warm/exec fallback is not eligible (direct-or-error), and the response/session metadata records which path served the turn.
- Regression tests: a tool-bearing request to each proxy produces the loud refusal, not a
stopcompletion. (Today: claude-code-proxy tests have zero tool-call assertions; commandcode-proxy tests enshrine the substitution behavior — update them.)
Long-term resolution remains the façade migrations linked above; this issue is the fail-closed floor under them.
Acceptance criteria
- claude-code-proxy and commandcode-proxy no longer degrade silently: a tools-carrying request either gets real client tool calls or an explicit, surfaced unsupported-tools signal — never
finish_reason:"stop"+ prose that implies work happened. - muse-proxy's warm/exec fallback paths surface which path served a turn when the fallback flattens tool calls to a transcript.
- Tests pin the new behaviour for at least one proxy: tools request in, explicit signal (or real tool calls) out.
- The fix does not re-enable either disabled provider in the deployed config by itself.
Activity
developing-today commentedon Nov 5, 2026
CODE-1763
dezren39 commentedon Nov 5, 2026
Intake review
bin/claude-code-proxy.mjsignoresbody.toolsentirely andbin/commandcode-proxy.mjsreduces the client tool list to a prose manifest; both providers are currently disabled in the deployed config, so this is latent until a door is re-enabled.finish_reason:"stop"+ prose that reads as if client tools ran, under the CLI's own tool semantics in the CLI's workdir. The acceptance therefore centres on an explicit surfaced signal (or real tool calls), not on making agency mode better.dezren39 commentedon Nov 5, 2026
🤖 Agent note · handed off
Merge pull request #6256 from developing-today/fix/muse-proxy-tools-f…
2 remaining items
dezren39 commentedon Nov 5, 2026
Track 2 outcome (2026-11-05): native wire-emulation providers landed — PR developing-today-forks/opencode#1749, merged 8185486080ab
Both CLI doors now have native fork providers in the direct-provider pattern (wire emulation; vendor binary is login/bootstrap-only). The providers emit real client tool calls. Neither provider is re-enabled by this change — the deployed config still seats the disabled proxy blocks; repointing the config at the new packages is the coordinator's deploy step, after which the proxies retire.
claude-code →
packages/ai/src/providers/claude-code.ts(+claude-code-auth.ts,protocols/claude-code-messages.ts)POST https://api.anthropic.com/v1/messages, streamed) with its claude.ai subscription OAuth credential (~/.claude/.credentials.json→claudeAiOauth.accessToken, Bearer, never x-api-key). The new provider reuses the fork's AnthropicMessages protocol through a wrapper that adds the OAuth-gated shape: Claude Code identity as the first system block,anthropic-beta: claude-code-20250219,oauth-2025-04-20,x-app: cli,claude-cli/2.1.306 (external, cli)UA. Tools lower/stream as realtool_use/tool_resultblocks.403 oauth_not_allowed_for_organization— the same org-level OAuth denial the proxy documented forGET /v1/modelson 2026-10-28. The missing piece is an account whose organization allows OAuth API access, i.e. Drew's interactive sign-in. Until then the provider surfaces that 403 as a loud provider error; it never falls back to the CLI.commandcode →
packages/ai/src/providers/commandcode.ts(+commandcode-auth.ts,protocols/commandcode.ts)createModelClient/consumeStream/toWire*/buildServerConfig/buildCommandApiHeaders): the hosted Provider API (/provider/v1) rejects the Go plan (403 upgrade_required, id: F11.10 large-module split has no regression guard — no automated check fails when a production file regrows past 2,500 lines #1411), so the emulation targets the CLI's own endpoint,POST https://api.commandcode.ai/alpha/generate(NDJSON stream), with thecmd loginkey from~/.commandcode/auth.jsonand the CLI's identity headers (x-command-code-version,x-cli-environment: production,User-Agent: cli). Full protocol implemented: envelope incl. the gateway-validated nine-fieldconfig(a bare{}is a live-probed 400), wire messages, atomic streamed tool calls, provider-executed call/result surfacing, finish usage.tool_choicefield, sorequired/named choices error locally instead of silently downgrading to auto (noneis honored by omitting tools); a stream ending before its finish event is a hard error (the bundle's own guard); stream structs tolerate the private vocabulary's extra fields.get_weathertool) returned HTTP 200 with a real client tool call —{"type":"tool-call","toolCallId":"call_00_yebxzt7x4gd59bt3hb506h2k","toolName":"get_weather","input":{"city":"Oslo"}}, finishtool-calls. The captured stream shape is pinned as a unit test.Tests (real runs in the worktree)
packages/aisuite: 2089 pass, 28 skip, 0 fail.packages/coreprovider+variant: 27 pass. Typecheck (both ai tsconfigs) + oxlint clean.What remains on this issue
bin/are superseded by the native providers, and both doors stay disabled until the coordinator repoints the config (the acceptance criterion "the fix does not re-enable either disabled provider" holds). If either proxy is ever re-enabled before that repoint, the silent-degradation defect is live again — the repoint (or the interim 4xx guard, if a re-enable happens first) is the remaining item, sequenced with the shared deploy.dezren39 commentedon Nov 5, 2026
Agent note - handed off
ses_e5e5361a210ffe8yTS22MmT1Fvdezren39 commentedon Nov 5, 2026
🤖 Agent note · done
Issue #6117 is CLOSED with stateReason COMPLETED (closed 2026-11-05T15:15:17Z, label status:done) on a fresh re-read in this shift. Task 10449 is therefore completed as already_done; no new work was started on a closed issue.
Merge pull request #6368 from developing-today/fix/6117-repoint-nativ…
dezren39 commentedon Nov 5, 2026
Repoint complete — claude-code + commandcode now resolve to the native in-fork providers (2026-11-05)
Follow-up deployment for the tool-call-audit batch (opencode#1708), after the first deployment (gen 540) landed only part of the batch:
sha256-AciYcYkuc2ctfZcBOFtBEe67qndw4YLn7XAf85dTPAc=, gotsha256-GDbuDOBJdbqdkL35X9/xxbjeJ1IjM6E7EkyKANxgWLo=). Fork main is now1add72932a4eae0794712c7c1f95a74aad10d03c(ancestry of merges 326493e (openclaw-backup: a missing agent state DB is backed up as success — state/openclaw.sqlite is silently optional, no required-member check #1748) and 8185486 (feat(ci): flake-inputs-update escalates failures to Jules, then Muse tasks #1749) verified)./nix/store/78f5jhv8iz0p28qn1bb42v1q0dsvhjda-opencode-2.0.30+1add729(opencode --version→v2.0.30+1add729)..opencode/opencode.jsonc— theclaude-codedefinition's@ai-sdk/openai-compatiblenpm +:18749baseURL options and thecommandcodedefinition's@ai-sdk/openai-compatiblenpm +:18748baseURL options are replaced with the native providers (@opencode/ai/providers/claude-code,@opencode/ai/providers/commandcode), following the antigravity-direct repoint pattern; model catalogs retained. Both ids remain indisabled_providers— nothing was re-enabled.claude-code → package @opencode/ai/providers/claude-code(no options/baseURL) andcommandcode → package @opencode/ai/providers/commandcode; the strings18748/18749occur 0 times in the whole materialized config; the served model list (GET /api/model, 13 providers) contains neither id — both still disabled/absent.The Drew-gated OAuth org allow for claude-code and the commandcode credit state are unchanged — re-enabling either provider remains a separate deliberate step.
dezren39 commentedon Nov 6, 2026
claude-code provider — enablement result + where the provider is actually used (2026-11-06)
Posted per Drew's order (enable Claude after Max enablement; "tell me where the provider is actually used").
Enablement state
disabled_providersflip landed: config(opencode): re-enable claude-code provider #6471 (merge252d4308), deployed file verified identical at repo /~/.config/opencode/~/codepaths; production server restarted,GET /api/providerreports claude-codeactivation: enabled, 16 models incl. the three config leaves.20ca5a36) — mergedDEFAULT_BASE_URL = "https://api.anthropic.com"+PATH = "/v1/messages"(was/v1+/messages). Not yet in the deployed binary — production still runs2.0.30+1add729(pre-fix) until the flake pin bump + Drew'snixos-rebuild switch.Config usage (
.opencode/opencode.jsonc, routes underexperimental.model_routes)Direct seats — only three, each the LEAD target of its plan route:
haiku-plan(ordered):claude-code/claude-haiku-5-5→opendesign/claude-haiku-5.5→opencode-go-plans/claude-haiku-5-5sonnet-plan(round-robin):claude-code/claude-sonnet-5-5→antigravity-direct/claude-sonnet-5-5opus-plan(ordered):claude-code/claude-opus-5-5→antigravity-direct/claude-opus-5-5Transitive reach:
sonnet-plan#high→ sonnet lead ✓; then argon/spark/sol plans.opus-plan#high→ opus lead ✓; then argon-plan, muse-direct, sol-plan.haiku-plan#high(weight 2) — claude member present as found, unchanged.workhorse-weightedseatshaiku-plan(w2);workhorse-primaryseatssonnet-plan#medium— claude members present as found, unchanged (no route edits were made at all).scout,small,small-100kconsumehaiku-plan.Empirical usage (laptop opencode.db, LIVE tables — session_v2/session_message + route_attempt/route_health; 14-day window unless noted)
model.providerID = claude-codein the last 14 days — but the successes are all old:claude-haiku-5-5475 success, last 2026-10-26 (443 of them via routeworkhorse);claude-opus-5-5[1m]665 success, last 2026-10-25 (legacy model id with[1m]suffix, no route id recorded). All-time: 1,140 success / 301 failure / 3 timeout.[1m]rate-limit windows (Oct 24–25, provider-reported).Where it is NOT used (verified, not assumed)
sonnet-planLIVE attempts (14d): onlyantigravity-direct(1 failure) +antigravity-ls(8 success) — the plan's live door is the direct door, per its config comment ("single-live-door on the direct door, plus claude-code's return").opus-planroute id: zero route_attempt rows in 14 days; summit's 9 attempts went to antigravity-direct / muse-direct / openai / opencode-go — never claude-code.specialist's 153 attempts (14d): antigravity-direct, cline-pass, muse-direct, openai., opencode-go. — zero claude-code.claude-code/claude-haiku-5-5until the 2026-10-27 outage; restoring it is an optional preference, explicitly NOT part of enabling). The pending-decisions itemhaiku-plan-claude-code-reenable-order(haiku-plan lead order on re-enable) is still unanswered — the flip rejoined claude-code as haiku-plan's lead per the 2026-10-28 design; flagged for Drew.Live-probe status at post time
Wire shape verified good (direct replays in the provider's exact shape → 200, fleet + catalog ids, streaming + non-streaming). Two real blockers remain before the direct Haiku#low acceptance probe passes through the server: (1) deployed binary predates the #1813 endpoint fix; (2) Anthropic's third-party classifier rejects OpenCode's full harness system prompt on this account with HTTP 400 "Third-party apps now draw from your extra usage, not your plan limits" while accepting minimal/CLI-shaped prompts on the same credential — full diagnosis in the lane report.
dezren39 commentedon Nov 6, 2026
Correction to the live-probe status above (2026-11-06, later the same night)
The diagnosis is now complete; the paragraph above understated it:
The production server's instant plain-text 404 is Anthropic's own response, reproduced exactly: with the runtime overlay in place, the isolated instance sends to
POST https://api.anthropic.com/messages— no/v1segment (wire capture against a local forwarding proxy). Root cause: the Nov-4 runtime overlay at~/.local/state/opencode/opencode-runtime.jsonccontains no claude-code provider block at all, so the deployed binary falls back to a built-in default endpoint for the provider whose base lacks/v1. Anthropic's edge answers the unversioned path with the plain bodyNot Found(91-byte JSONnot_found_erroronly appears on the versioned path). The fork-side fix (Storage pools: RAID10 HDD tier for RustFS + striped/mirrored NVMe tiers with LINSTOR/DRBD #1813, endpoint constants) is real but is not what unblocks production — the overlay needs the claude-code block (or regeneration throughpkgs/opencode-config-normalize), or the provider needs a code default that carries/v1regardless of config presence. Note the isolated instance with the claude-code config block present routes correctly to/v1/messages.The Anthropic third-party gate is fingerprinted to one line. With the URL corrected, main session requests get HTTP 400 "Third-party apps now draw from your extra usage, not your plan limits." Bisecting the captured session request against the live API: the sole load-bearing trigger is the env instruction
Is directory a git repo: yes|no(packages/core/src/instructions/builtins.ts:43in the fork) inside the harness<env>block. The full captured body with only that line removed returns 200 with the correct reply; flipping its value changes nothing. Filed with full evidence + fix options: claude-code provider: Anthropic third-party gate fingerprints OpenCode's env instruction ('Is directory a git repo') — main sessions billed to extra usage developing-today-forks/opencode#1819. Until that lands (or extra usage is funded account-side), main-session claude-code calls through OpenCode will keep drawing the 400 — small requests without the env block (e.g. title generation) already pass.Acceptance probe status: config enablement ✓ (this issue's flip), wire shape ✓ (200s in the provider's exact shape, fleet + catalog ids, streaming), credential ✓ (below) — the direct Haiku#low leaf call through a session is blocked by (1) and (2) above, both now root-caused with proven fixes.