Skip to content

feat: auto-route image turns to Anthropic Max OAuth (env-gated)#19

Closed
BenSheridanEdwards wants to merge 4 commits into
aattaran:mainfrom
BenSheridanEdwards:image
Closed

feat: auto-route image turns to Anthropic Max OAuth (env-gated)#19
BenSheridanEdwards wants to merge 4 commits into
aattaran:mainfrom
BenSheridanEdwards:image

Conversation

@BenSheridanEdwards
Copy link
Copy Markdown

DeepSeek's anthropic-compat endpoint returns 400 on requests containing image content blocks; OpenRouter and Fireworks reject them similarly or silently ignore the pixels. Pre-translating images to text loses the detail that vision is actually for (UI screenshots, diagrams, OCR).

Detect image content in the outbound request body and re-route that single request to api.anthropic.com, leaving Claude Code's OAuth bearer intact so it consumes the user's Max subscription quota rather than a backend API key.

Behavior:

  • containsImageBlock walks tool_result.content[] recursively because Claude Code's Read tool wraps image bytes in a tool_result rather than at the top of the message.
  • Routing is now decided after the body is read (previously decided pre-body) so image detection can flip a single request.
  • On reroute: drop thinking and context_management from the body — Anthropic 400s with "clear_thinking_20251015 strategy requires thinking to be enabled" otherwise.
  • Cost tracking uses effectiveMode so image turns are bucketed under anthropic rather than the resident backend.
  • Strip outbound accept-encoding and inbound content-encoding on proxy-mutated paths (UsageNormalizer / normalizeJsonBody). Without this, Anthropic gzipped responses break the client with Decompression error: ZlibError. Loopback compression buys nothing.

Gate:

  • DEEPCLAUDE_IMAGE_FALLBACK=anthropic (default) — image turns reroute.
  • DEEPCLAUDE_IMAGE_FALLBACK=off — no reroute; image turns will 400 from the backend as before.

Closes #12.

BenSheridanEdwards and others added 4 commits May 5, 2026 21:13
DeepSeek's anthropic-compat endpoint returns 400 on requests containing
image content blocks; OpenRouter and Fireworks reject them similarly or
silently ignore the pixels. Pre-translating images to text loses the
detail that vision is actually for (UI screenshots, diagrams, OCR).

Detect image content in the outbound request body and re-route that
single request to api.anthropic.com, leaving Claude Code's OAuth bearer
intact so it consumes the user's Max subscription quota rather than a
backend API key.

Behavior:
- `containsImageBlock` walks `tool_result.content[]` recursively because
  Claude Code's Read tool wraps image bytes in a tool_result rather than
  at the top of the message.
- Routing is now decided after the body is read (previously decided
  pre-body) so image detection can flip a single request.
- On reroute: drop `thinking` and `context_management` from the body —
  Anthropic 400s with "clear_thinking_20251015 strategy requires thinking
  to be enabled" otherwise.
- Cost tracking uses `effectiveMode` so image turns are bucketed under
  `anthropic` rather than the resident backend.
- Strip outbound `accept-encoding` and inbound `content-encoding` on
  proxy-mutated paths (UsageNormalizer / normalizeJsonBody). Without
  this, Anthropic gzipped responses break the client with
  `Decompression error: ZlibError`. Loopback compression buys nothing.

Gate:
- `DEEPCLAUDE_IMAGE_FALLBACK=anthropic` (default) — image turns reroute.
- `DEEPCLAUDE_IMAGE_FALLBACK=off` — no reroute; image turns will 400
  from the backend as before.

Closes aattaran#12.
Five JSON.parse(body) passes in the request end-handler collapse to one
parse + one stringify; all mutations (image-reroute thinking/context_management
drop, model remap, thinking-block strip) operate on the same parsed object.

Image-rerouted turns get a dedicated `anthropic_max` pricing bucket with
cost=0 (Max consumes subscription quota, not per-token). total_cost no
longer overstates spend on image turns; anthropic_equivalent still flows
into savings via PRICING_PER_M.anthropic.

DEEPCLAUDE_IMAGE_FALLBACK is read once at module load
(IMAGE_FALLBACK_ENABLED) instead of per-request. trackUsage is declared
near the routing decisions so the upstream-routing log reuses it instead
of recomputing isModelCall || forceAnthropicForImage. The OAuth-bearer
passthrough comment now flags the x-api-key 401 failure mode.

No behavior change for the routing logic itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the 13-line block comment above the body-mutation block with
two short inline comments above the phases whose `why` is non-obvious
(image-reroute thinking/context_management drop, thinking-block strip
rationale). Model-remap needs no comment — MODEL_REMAP is the doc.

Add blank lines between the three mutation phases for visual separation.

Switch `parsed != null` to `parsed` to match the file's idiom
(body?.messages elsewhere).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BenSheridanEdwards added a commit to BenSheridanEdwards/DeepClaude that referenced this pull request May 7, 2026
Plain `deepclaude` (without --remote) previously pointed Claude Code
straight at api.deepseek.com, bypassing the proxy entirely — which made
PR aattaran#19's image fallback unreachable from the default launch path.

Wire launch_claude through the proxy:

- Symlink-resolving SCRIPT_DIR so $SCRIPT_DIR/proxy/start-proxy.js works
  when deepclaude is invoked via ~/.local/bin/deepclaude symlink.
- New `start_proxy` helper that spawns proxy/start-proxy.js, polls a
  per-instance log file (PROXY_LOG=/tmp/deepclaude-proxy.$$.log) for the
  bare-numeric port line, and sets PROXY_PID/PROXY_PORT/PROXY_LOG as
  script globals so the EXIT trap can clean up. Must be called without
  $() — comment makes that load-bearing.
- launch_claude calls start_proxy, sets ANTHROPIC_BASE_URL to the local
  proxy port, unsets ANTHROPIC_AUTH_TOKEN so Claude Code's OAuth bearer
  flows through to the proxy (and onward to api.anthropic.com on
  image-reroute), and drops `exec` so the EXIT trap actually fires.

Switch ANTHROPIC_DEFAULT_*_MODEL to canonical claude-* names. Required
because Claude Code v2.1.132 rejects non-Claude model names client-side
when ANTHROPIC_AUTH_TOKEN is unset (OAuth path). The proxy translates
canonical names to backend-specific names on the wire via MODEL_REMAP.

Pass backend long-name as third argv to start-proxy.js so the proxy
starts in named mode (e.g. `deepseek`) rather than `_single` —
MODEL_REMAP['_single'] is undefined, so without this the canonical names
would be forwarded as-is and DeepSeek would 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

deepseek image support implementation

1 participant