The OpenAI Chat proxy decided image support from host and model names:
opencode.ai and DeepSeek hosts replaced every image with a text-only
placeholder unless the model id contained "vision" or was allowlisted.
Each new vision model on OpenCode Go (Kimi K3, Space Bunny, DeepSeek Flash)
therefore told users the endpoint could not read images.
Offer images to every model instead. When the upstream refuses a request
that carried images with a recognizable image-input rejection, resend it
once without images and, only if that succeeds, remember the endpoint and
model as text-only for 30 minutes and record an info diagnostic. Failures
about a particular image (format, MIME type, size, animation, image count)
surface unchanged so one bad image cannot disable images for the model.
The rejection matcher moves to a shared module used by the CLI error mapper
and the proxy, and now also recognizes vLLM and OpenAI wording.
OpenCode Go binds the wire format to the URL path and translates nothing
between formats, so one provider record serves /chat/completions, /messages
and /responses depending on the model, each accepting a different credential
header. A record carries only one apiFormat, so presets can now declare
ordered per-model prefix rules (modelApiFormats) and the proxy resolves the
effective format from the request body. Only the exceptions are listed;
anything unmatched keeps the provider's format, which is the endpoint with the
broadest compatibility. A preset with rules is authoritative for apiFormat,
because a value written by a cc-switch import or the edit form would otherwise
silently disable every rule and point the CLI straight at the upstream.
The gateway also rejects any request without a stable per-conversation
x-opencode-session, so presets can declare upstreamHeaders with $SESSION_ID
and $VERSION placeholders. The id is the one the CLI already sends; it is
redacted in traces the same way the credential is, since it also names the
local transcript files.
Title generation builds its own upstream request and bypassed all of the
above, which left AI titles failing for every provider that needs local
request handling; it now goes through the same proxy the CLI uses.
Verified against the live gateway: glm/kimi reach /chat/completions,
minimax/qwen/union-alpha reach /messages, grok/gpt reach /responses, each
with the credential that endpoint accepts.
Normalize a terminal API version at Anthropic request boundaries while
preserving gateway prefixes and saved provider settings. Cover SDK messages,
token counting, proxy streams, connectivity, titles, files, and recovery CLI.
Fixes#1279
Adapt structured policy-error handling and Astra model metadata from sub2api's recent compatibility updates. Preserve rejection codes through HTTP and SSE, stop retry and non-streaming replay, and add Astra's effective context to the official runtime.
Reference: https://github.com/Wei-Shaw/sub2api/pull/6636 and https://github.com/Wei-Shaw/sub2api/pull/6572. Implementation uses the existing TypeScript runtime and local regression harnesses.
Refs: #896, #953
Make provider network proxy mode explicit so desktop sidecar system proxy env does not affect provider connectivity tests or OpenAI-compatible proxy requests unless System proxy is selected.
Tested: bun test src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx --run
Tested: bun run check:server
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Confidence: high
Scope-risk: moderate
Fixes local Batch A from #809+ triage: configurable scheduled task timeout (#809/#846), OpenAI proxy trace request headers (#836), title generation auth strategy (#854), and unresolved parallel tool card settlement (#889).
Tested: bun test src/server/__tests__/cron-scheduler-launcher.test.ts
Tested: bun test src/server/__tests__/title-service.test.ts
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: bun run verify and coverage were not run because this is a local batch checkpoint, not a PR-ready/push-ready handoff.
Confidence: high
Scope-risk: moderate
Strip the rotating billing attribution line before converting system
prompts to OpenAI Chat/Responses requests, forward a stable
prompt_cache_key derived from the client session, request stream usage
explicitly on Chat streams, and map cached_tokens back to Anthropic
cache_read_input_tokens with exclusive input accounting.
UI rebuild (desktop):
- TraceSession: replace 3-column layout with two panes — turn-grouped
timeline tree (draggable splitter, search/filter, keyboard nav) and a
section-flow detail panel (Response / Messages / System Prompt /
Tools / Parameters / Raw), collapse state persists across spans
- Render LLM requests/responses semantically: messages as role-colored
conversation with tool_use/tool_result pairing instead of raw JSON
dumps; Raw fallback via CodeViewer for legacy truncated records
- TraceList: row-style list with model chips, mono metrics, hover
actions; content-visibility rows (no virtualization, WebKit-safe)
- i18n synced across zh/en/jp/kr/zh-TW (+25/-55 keys)
Data & capture (server):
- Capture full bodies: preview cap 2048 -> 240k chars, stream cap
256KB -> 1MB; list API trims previews to keep polling light; new
GET /api/sessions/:id/trace/calls/:callId returns the full record
- Extract per-call token usage at read time (SSE + JSON + proxy
wrapped); mtime-keyed read cache for the polling path
- Fix sensitive-key regex redacting *_tokens count fields, which made
token stats always report 0
Frontend data layer:
- SSE stream reassembly (Anthropic + OpenAI chat) adapted from
claude-tap (MIT, attribution in THIRD_PARTY_LICENSES.md), request/
response body parsers, shared formatters, on-demand call detail
cache; traceViewModel gains tokenUsage/isLifecycleNoise, drops
fullRaw
Tested:
- bun run check:server (1201 pass)
- bun run check:desktop (lint + 1358 tests + build)
- Chromium walkthrough against real local traces: list, session tree,
LLM semantic detail (new format), legacy fallback, tool detail
Tested:
- cd desktop && bun run test -- --run src/pages/TraceList.test.tsx
- bun test src/server/__tests__/trace-capture.test.ts
- bun run check:desktop
- bun run check:server
Scope-risk: broad
Guard OpenAI-compatible proxy streaming bodies with the configured AI request timeout so a provider that emits partial SSE and then idles cannot leave proxy consumers waiting forever.
This is a proxy-level fix found while investigating #548; it does not claim to close the broader desktop interruption issue.
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:server
Confidence: medium
Scope-risk: narrow
Provider connectivity checks and proxy validation are API boundaries, but they kept the user-facing context suffix in the model id while the chat runtime already strips it. Normalize these paths so MiMo-style models with [1m] test the same model that sessions send.
Constraint: [1m]/[2m] are client-side context markers and upstream APIs may reject them
Rejected: Strip suffixes when saving provider config | would change the user-visible model selection instead of only the API boundary
Confidence: high
Scope-risk: narrow
Directive: Keep all provider API boundaries aligned with normalizeModelStringForAPI before sending requests upstream
Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "normalizes context-window suffixes"
Tested: bun test src/server/__tests__/providers.test.ts
Tested: bun run check:server
Tested: git diff --check
Not-tested: Live MiMo API call; no provider credential was used
Related: #620
DeepSeek's OpenAI-compatible chat schema accepts text content rather than OpenAI vision content parts, so forwarding Anthropic image blocks as image_url makes the upstream reject the request before the model can answer. Keep normal OpenAI Chat vision behavior as the default, but switch known DeepSeek-compatible chat proxy requests to a text-only conversion that preserves prompt text and replaces image payloads with a small omission marker.
Constraint: DeepSeek official Chat Completions docs list user message content as string text, while its Anthropic compatibility table marks image blocks as not supported.
Rejected: Expand unsupported-image error string matching only | it still surfaces the first turn as an API error and depends on provider wording.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/proxy-transform.test.ts
Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "handleProxyRequest|omits image_url"
Tested: bun test src/services/api/errors.test.ts tests/mediaRecoveryAndEstimation.test.ts
Tested: bun run check:server
OpenAI-compatible streaming requests previously used the shared AI request timeout as a full-body AbortSignal. That made long SSE generations stop mid-answer after the configured timeout even when the upstream had already started streaming.
The proxy now applies the configured timeout only while opening streaming upstream requests and keeps non-streaming requests on the existing full-request timeout.
Constraint: The global AI request timeout is documented as covering provider requests and streaming first responses, not total stream duration.
Rejected: Increase the default timeout | long plan-mode answers can still exceed any fixed full-stream limit.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:server
Non-stream OpenAI-compatible proxy requests still kept the previous 300s timeout floor after the unified network settings merge. That made lower General AI timeout settings ineffective for normal proxy traffic, even though the setting is meant to be the final provider request timeout.
Constraint: Unified network settings must remain the final timeout and proxy override for provider protocol paths.
Rejected: Keep the 300s non-stream floor | it makes low timeout settings ineffective for normal proxy requests.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce per-protocol timeout floors without updating the General network settings contract and tests.
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Live provider request against a real upstream proxy.
The worktree implementation adds one General settings surface for AI request timeout and proxy configuration. This merge keeps main's newer attribution-signing and ChatGPT Official provider paths while preserving the unified network override across CLI sessions, provider checks, and OpenAI-compatible proxy forwarding.
Constraint: Main had newer attribution and official-provider changes touching the same server integration points
Rejected: Prefer the worktree proxy body verbatim | it would drop main's signed CCH attribution wrapper
Rejected: Keep provider preset timeouts above General | it would violate the single timeout control requirement
Confidence: high
Scope-risk: moderate
Directive: General network settings must remain the final AI request timeout and proxy override for every provider protocol
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- --run src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Not-tested: Full bun run verify after resolving main merge conflicts
Settings General now owns AI request timeout and proxy policy so Anthropic-native and OpenAI-compatible provider paths share one user-visible control. The desktop UI persists the network settings, the server proxy and provider checks read them directly, and CLI sessions receive the same timeout/proxy environment.
Constraint: Provider request behavior must be consistent across Anthropic, OpenAI Chat, and OpenAI Responses formats
Rejected: Keep preset API_TIMEOUT_MS precedence | it would make the General timeout control unreliable for some providers
Confidence: high
Scope-risk: moderate
Directive: Do not add protocol-specific AI request timeout controls without preserving General as the final override
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/network-settings.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- --run src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Tested: bun run check:desktop
Tested: bun run check:server
Not-tested: socks5 manual proxy support; manual proxy validation currently accepts HTTP/HTTPS URLs only
DeepSeek-compatible OpenAI Chat providers require assistant reasoning_content to be replayed on later tool-call turns when thinking mode is active. The Anthropic-to-OpenAI chat transform now keeps that replay behavior behind an explicit compatibility option, and the proxy only enables it for DeepSeek/OpenCode base URLs so generic OpenAI-compatible providers keep their previous request shape.
Constraint: DeepSeek thinking mode rejects follow-up requests when prior assistant reasoning_content is omitted
Rejected: Trigger compatibility by model name | would affect unrelated OpenAI-compatible providers using DeepSeek model ids
Confidence: high
Scope-risk: narrow
Directive: Do not broaden this compatibility gate without proving other OpenAI-compatible providers accept these non-standard fields
Tested: Fixed-port Web UI harness with copied DeepSeek OpenAI Chat provider reproduced the 400 and passed through local proxy after the fix
Tested: bun test src/server/__tests__/proxy-transform.test.ts
Tested: bun run check:server
Tested: git diff --check
Not-tested: Direct OpenCode Go subscription endpoint, unavailable locally
The desktop model picker now stores a session-scoped provider/model selection instead of relying on the global active provider. That selection is replayed on connect, passed into the CLI startup path, and preserved across turns until the user changes it again.
To make that true end-to-end, the server now restarts the session process when runtime selection changes, injects provider-scoped env for third-party providers, and routes proxy traffic by provider id. The selector UI was also tightened so provider grouping stays visible while the actual model choice remains readable.
Constraint: Different providers can expose the same model id, so chat runtime selection cannot be derived from model id alone
Constraint: A desktop session reuses one CLI subprocess across turns, so runtime changes must restart that process to take effect
Rejected: Keep using Settings active provider as the chat selector | conflates defaults with live session state and breaks overlapping models
Rejected: UI-only runtime switching without server restart | later turns would continue using the old CLI subprocess configuration
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep provider defaults and session runtime overrides separate, and preserve provider-scoped proxy routing when extending model selection surfaces
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test src/stores/chatStore.test.ts
Tested: cd desktop && bun run test src/__tests__/generalSettings.test.tsx
Tested: bun test src/server/__tests__/conversation-service.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts
Tested: bun -e "await import('./src/server/services/titleService.ts'); await import('./src/server/ws/handler.ts')"
Not-tested: Real third-party provider round-trip from the desktop UI against a live upstream account
Add a protocol-translating reverse proxy that allows using OpenAI-compatible
API providers (DeepSeek, OpenRouter, Groq, etc.) with Claude Code.
The proxy intercepts Anthropic Messages API requests from the CLI, transforms
them to OpenAI Chat Completions or Responses API format, forwards to the
upstream provider, and transforms streaming/non-streaming responses back.
Key features:
- Request transform: Anthropic Messages → OpenAI Chat/Responses
- Response transform: OpenAI → Anthropic (streaming SSE + non-streaming)
- Provider-agnostic reasoning support (reasoning_content, thinking_blocks,
reasoning fields from DeepSeek, OpenAI o-series, GLM-5, Groq, etc.)
- Event queue pattern for correct Anthropic SSE event ordering
- Two-step test: ① connectivity check ② full proxy pipeline validation
- Desktop UI: API format selector, two-step test results display
- License attribution for cc-switch (MIT, Jason Young)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>