Harden Claude subscription path + restore coding quality through the proxy - #6
Merged
Conversation
The final image re-ran 'npm ci --only=production', which dropped the 'pg' runtime dependency (and the generated Prisma client), breaking the proxy at startup. Reuse the builder's already-installed node_modules instead, and fail the build loudly if pg is missing.
Three intertwined fixes to the Claude subscription (OAuth) provider: 1. OAuth auto-refresh: the proxy now redeems the refresh token itself when the access token is near expiry or on a 401, persisting the rotated credentials atomically with a single-flight mutex. Removes dependence on the external (and here, broken) credential-sync job. 2. Tool-use ID sanitization: a conversation whose earlier turns were served by a fallback provider (GLM/Z.AI) accumulates tool-use IDs that fail Anthropic's strict ^srvtoolu_ / ^toolu_ validation, 400-ing every replay. Rewrite non-conforming IDs, remapping paired tool_use_id references in the same pass (subscription + anthropic paths). 3. Coding quality passthrough: cleanBody was an allow-list that stripped output_config (effort: xhigh, task_budget) and context_management on every request, and cleanMessages stripped ALL historical thinking blocks. For the Anthropic-compatible paths, pass top-level fields through untouched and keep signed thinking blocks (drop only unsigned/contaminated ones). Also fix the OpenRouter Opus->Sonnet downgrade. 102 tests pass (93 prior + 9 new).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three intertwined fixes to the Claude subscription (OAuth) provider path, plus a Docker build fix. Coding requests routed through the proxy were noticeably worse than talking to Claude directly — the root cause was the proxy silently stripping the request settings that drive coding intelligence, plus a couple of correctness bugs on the subscription path.
Background
Live log analysis (
podman logs cc-proxy) showed this on every request:Changes
1. Coding-quality passthrough (
src/index.ts— the headline fix)cleanBodywas a hardcoded allow-list that omittedoutput_config, so every request lost it. Claude Code putseffort(it sendseffort: \"xhigh\"for Opus 4.7/4.8 — the setting Anthropic calls out as best for coding/agentic work),task_budget, andformatinsideoutput_config. Stripped → the API silently fell back tohigh, reducing reasoning/agentic depth on every coding request.cleanBodyis now provider-aware: forsubscription/anthropicit passes the body through untouched (only model-remap + message-clean); the strict allow-list is kept only forzai/openrouter, which genuinely need the translated shape. Future-proofs against new client fields.cleanMessagesno longer strips all historical thinking blocks on the subscription path. It now keeps signed thinking blocks + allredacted_thinking(preserving cross-turn reasoning continuity — what direct Claude does), dropping only unsigned (contaminated) blocks that would 400. Gated bySTRIP_SUBSCRIPTION_THINKING(unsigneddefault |alllegacy |none).mapModelOpenRouter mapping:claude-opus-4-8/4-7/4-6+claude-opusnow map to~anthropic/claude-opus-latestinstead of being silently downgraded to Sonnet on OpenRouter fallback.2. Tool-use ID sanitization (
src/index.ts)A conversation whose earlier turns were served by a fallback provider (GLM/Z.AI) accumulates tool-use IDs that fail Anthropic's strict
^srvtoolu_/^toolu_validation, 400-ing every replay (messages.N.content.M.server_tool_use.id: String should match pattern ...). AddedsanitizeToolUseIds: rewrites non-conforming IDs and remaps every pairedtool_use_idreference in the same pass (subscription + anthropic paths).3. OAuth auto-refresh (
src/index.ts,src/types.ts)The proxy now redeems the refresh token itself when the access token is near expiry or on a 401, persisting rotated credentials atomically (temp file + rename) under a single-flight mutex (Anthropic rotates the refresh token on every use). Removes dependence on the external credential-sync job. Configurable via
CLAUDE_OAUTH_REFRESH_URL/CLAUDE_OAUTH_CLIENT_ID/CLAUDE_OAUTH_REFRESH_SKEW_MS.4. Docker build fix (
Dockerfile)The production image re-ran
npm ci --only=production, which dropped thepgruntime dependency (and the generated Prisma client). Reuse the builder'snode_modulesinstead and fail the build loudly ifpgis missing.Verification
tsc --noEmit+eslintcleanoutput_configis no longer stripped, thinking-strip drops only contaminated blocks, zero new subscription 400s after keeping signed thinking.Operational notes
:rw).STRIP_SUBSCRIPTION_THINKING=alland recreate the container (no rebuild).