Skip to content

fix: port critical fixes from opencode-claude-auth for billing parity - #48

Open
JJValentin wants to merge 1 commit into
zacdcook:masterfrom
JJValentin:fix/opencode-claude-auth-parity
Open

fix: port critical fixes from opencode-claude-auth for billing parity#48
JJValentin wants to merge 1 commit into
zacdcook:masterfrom
JJValentin:fix/opencode-claude-auth-parity

Conversation

@JJValentin

Copy link
Copy Markdown

Summary

Ports 10 fixes discovered through a code-level comparison with opencode-claude-auth v1.4.10, which solved the same billing-parity problems for OpenCode. All changes are verified with 38 unit tests and live-tested on an OpenClaw Max subscription (Opus 4.6 calls working through the proxy).

What Changed

🔴 Critical Fixes (requests failing without these)

Fix Problem Solution
mcp_ tool prefix Anthropic's billing validator rejects bare PascalCase tool names (e.g. Bash) — must be mcp_Bash All 31 DEFAULT_TOOL_RENAMES entries now include mcp_ prefix
CCH computation Was hardcoded cch=00000 — Anthropic may validate this hash Now computes real SHA256(text).hex().slice(0,5) from original (pre-transform) user message
Model-aware beta flags interleaved-thinking sent to Haiku (400 error). effort beta sent to 4.5 models. Two fake betas (advanced-tool-use-2025-11-20, fast-mode-2026-02-01) never existed in Claude Code. New getModelBetas(modelId) filters by model. Fake betas removed.

🟡 Reliability Fixes (things that break over time)

Fix Problem Solution
Effort param stripping Haiku rejects effort parameter with 400 Detects Haiku model, strips effort from output_config and thinking objects
OAuth token refresh Token loaded once at startup, dies after ~10h Direct refresh via POST claude.ai/v1/oauth/token with pre-expiry check, 401 retry, race-condition dedup, and disk persistence
System prompt boundary Boundary detection only matched C:\, D:\, E:\ drives and could match outside system[] 6 identity markers, generic [A-Z]:\ drive pattern, search bounded to system[] via findMatchingBracket()

🟢 Cleanup

Fix Problem Solution
CC tool stubs default OFF Injected fake tools cause "tool not found" loops (#43) injectCCStubs defaults to false. Claude Code doesn't inject fake tools.
Orphaned tool pair repair Mismatched tool_use/tool_result blocks cause API errors New repairToolPairs() function removes orphans with adjacent same-role turn guard
Remove fake betas advanced-tool-use-2025-11-20 and fast-mode-2026-02-01 don't exist in Claude Code Removed from REQUIRED_BETAS
Stainless SDK version Hardcoded 0.81.0 is stale Updated to 0.90.0

Testing

  • 38 unit tests added in test-functions.js (run with node test-functions.js, zero dependencies)
  • Syntax verified: node -c proxy.js passes
  • Live tested: Deployed to running OpenClaw instance with Max subscription — Opus 4.6 calls succeed through the proxy

Issues Addressed

Reference

All fixes were derived from opencode-claude-auth v1.4.10 (specifically: src/transforms.ts, src/signing.ts, src/credentials.ts, src/model-config.ts, src/betas.ts). That project solved the same billing-parity problems for OpenCode and has been iterating on Anthropic's detection changes since March 2026.

Ports 10 fixes discovered through code-level comparison with opencode-claude-auth
(griffinmartin/opencode-claude-auth v1.4.10), verified with 38 unit tests and
live-tested on an OpenClaw Max subscription.

Critical fixes:
- Add mcp_ prefix to all tool renames (Anthropic rejects bare PascalCase)
- Compute real CCH hash (was hardcoded cch=00000)
- Model-aware beta flags: exclude interleaved-thinking for Haiku,
  add effort only for 4.6 models, remove fake betas

Reliability fixes:
- Strip effort parameter for Haiku models (prevents 400 errors)
- Direct OAuth token refresh via POST to claude.ai/v1/oauth/token
  with pre-expiry check, 401 retry, and race-condition dedup
- Improved system prompt boundary detection (6 identity markers,
  generic Windows drive letter patterns, bounded to system[] array)

Cleanup fixes:
- Disable CC tool stubs by default (fixes zacdcook#43 tool-not-found loops)
- Add orphaned tool_use/tool_result pair repair
- Remove non-existent beta flags (advanced-tool-use, fast-mode)
- Update Stainless SDK version to 0.90.0

Also adds test-functions.js with 38 unit tests covering all pure
functions (no credentials or network required to run).

Fixes zacdcook#41, zacdcook#43, zacdcook#46. Partially addresses zacdcook#47 (token refresh).
@DBostik

DBostik commented Apr 15, 2026

Copy link
Copy Markdown

You were able to get token refresh to work with these changes? Mine failed when I implemented this.

kongkong7777 pushed a commit to kongkong7777/openclaw-billing-proxy that referenced this pull request Apr 15, 2026
…Code

'advanced-tool-use-2025-11-20' and 'fast-mode-2026-02-01' were added
speculatively in v2.0.0 but neither flag ever shipped in real Claude
Code. Anthropic's density-based classifier scores the full anthropic-beta
header, and unknown flags are a strong signal that the client is not
genuine CC, contributing to extra-usage billing (zacdcook#41) and possibly to
account revocation (zacdcook#37).

Identified via PR zacdcook#48 code comparison against opencode-claude-auth v1.4.10
which verified the real CC beta flag set.
@DBostik

DBostik commented Apr 16, 2026

Copy link
Copy Markdown

Not so sure about this PR. Here is what Opus has to say primarily regarding the MCP change:

Let me be straightforward about my confidence level on each:
mcp_ prefix change -- moderate confidence (maybe 60-65%)
I can see clearly what the change is and it's mechanically simple in your proxy. But I have a few reservations:
First, not all Claude Code tools actually use the mcp_ prefix. The built-in tools (Bash, Read, Write, Edit, Grep, Glob, Agent, TodoRead, etc.) are bare PascalCase in real Claude Code. The mcp_ prefix is specifically for MCP server tools (like mcp__slack__post_message). So blindly prefixing everything with mcp_ could actually make things less authentic, not more. Your current rename from exec to Bash is arguably closer to what real Claude Code sends for a built-in tool than mcp_Bash would be.
Second, the PR author tested on OpenClaw but the detection logic Anthropic uses could differ across subscription tiers, regions, or could change at any time. What worked for them might not be the same check your traffic hits.
Third, your proxy has a bidirectional reverse mapping (lines 254-290) that translates names back in responses. Adding mcp_ means you'd need to update the reverse map too, and make sure LCM/OpenClaw handles receiving mcp_-prefixed tool names in the response stream without choking.
I wouldn't make this change without more information. It would be worth asking the PR author whether they prefixed ALL tools with mcp_ or only certain ones, and whether they have evidence that bare PascalCase names (which match real Claude Code built-ins) are actually being rejected.

@JJValentin

Copy link
Copy Markdown
Author

Hey @DBostik, The Opus analysis was wrong on the mcp_ point.

Built-ins included (mcp_Bash, mcp_Read, mcp_Write, mcp_Edit, mcp_Grep, mcp_Glob, etc.). There are no bare PascalCase names. You can verify this yourself by inspecting tool definitions in any active CC session.

The fixes were derived from comparing against opencode-claude-auth line by line. They solved the same billing issues; that's where I found the mcp_ prefix, CCH hash, and model-aware betas. I have it working live on both my Openclaw and Opencode set-ups.

On token refresh, it works. Direct POST to claude.ai/v1/oauth/token with the refresh token.

Pre-expiry check so it refreshes before dying, 401 retry if it does expire, and dedup so concurrent requests don't race.

What error were you hitting?

@DBostik

DBostik commented Apr 16, 2026

Copy link
Copy Markdown

Hey @DBostik, The Opus analysis was wrong on the mcp_ point.

Built-ins included (mcp_Bash, mcp_Read, mcp_Write, mcp_Edit, mcp_Grep, mcp_Glob, etc.). There are no bare PascalCase names. You can verify this yourself by inspecting tool definitions in any active CC session.

The fixes were derived from comparing against opencode-claude-auth line by line. They solved the same billing issues; that's where I found the mcp_ prefix, CCH hash, and model-aware betas. I have it working live on both my Openclaw and Opencode set-ups.

On token refresh, it works. Direct POST to claude.ai/v1/oauth/token with the refresh token.

Pre-expiry check so it refreshes before dying, 401 retry if it does expire, and dedup so concurrent requests don't race.

What error were you hitting?

I actually was able to get the token refresh to work thankfully.

Haven't implemented the MCP or CCH changes yet though since it seems to be working as is.

I'm definitely having cache issues causing extreme token usage though but I believe that is a LCM issue.

@JJValentin

Copy link
Copy Markdown
Author

@DBostik The CCH fix might actually be what addresses the cache issue. The hardcoded key means cache keys never match, so every turn bills as fresh tokens. Computing the real hash is what makes prompt caching work.

@DBostik

DBostik commented Apr 16, 2026

Copy link
Copy Markdown

@DBostik The CCH fix might actually be what addresses the cache issue. The hardcoded key means cache keys never match, so every turn bills as fresh tokens. Computing the real hash is what makes prompt caching work.

Had to ask Opus of course and this is what I got(also confirmation from LCM team that they are aware of my issue and have a fix):

The claim that hardcoded cch=00000 breaks caching is not accurate for your setup. In fact, the opposite is true -- changing it to a computed per-request hash would make things worse.
Here's the reasoning:
Why hardcoded cch=00000 is actually fine for caching:
The billing header is injected as the first text block in the system prompt. Anthropic's prompt cache works by exact prefix matching -- every byte from the start of the request must be identical for a cache hit. Since cch=00000 is the same string on every single call, the system prompt prefix stays stable. Your batch data already proves this: 20 of 24 calls cached the system prompt at exactly 13,260 tokens. If cch=00000 were breaking the cache, the system prompt wouldn't cache at all -- you'd see 0 cache reads across the board.
Why the proposed "fix" would actually break caching:
Computing SHA256(user_text).hex().slice(0,5) means the hash changes every time the user message changes, which is every call. That would make the first block of the system prompt different on every request, destroying the 13,260-token system prompt cache that currently works. You'd go from 20/24 calls caching the system prompt to 0/24.
The real CCH bug exists, but it's in actual Claude Code, not your proxy:
There's a documented issue (anthropics/claude-code #40652) where real Claude Code generates a new cch hash per request and then does a global find-and-replace across the entire message body to update all previous occurrences. If any tool result in the conversation history happens to contain a cch= string (from grepping logs, for example), that historical content gets mutated, which changes the prefix and permanently breaks caching for that session. Your proxy doesn't do this -- it uses a constant value and doesn't do any global find-and-replace of previous cch values. So you're actually immune to the bug that affects real Claude Code users.
Bottom line: The person who responded to you was likely describing the real Claude Code bug but applying it incorrectly to your situation. In your proxy, the hardcoded cch=00000 is doing you a favor by keeping the system prompt prefix stable. Don't change it.
The cache failures you're experiencing are in the messages array, not the system prompt. That's the issue the Lossless Claw developer is working on with the cold-cache signal bug he found. The CCH field has nothing to do with it.

kongkong7777 pushed a commit to kongkong7777/openclaw-billing-proxy that referenced this pull request Apr 17, 2026
- Remove web_search/web_fetch from Layer 3 tool renames (same class of
  bug as the image rename, issue zacdcook#14). The rename corrupts the "type"
  field of built-in tools, causing Anthropic to reject with:
    Input tag WebSearch found using type does not match expected tags

- Add Layer 0: auto-normalize shorthand built-in tool types to their
  latest versioned names (e.g. "web_search" -> "web_search_20260209").
  Covers all 8 built-in tool types: web_search, web_fetch, text_editor,
  code_execution, bash, memory, tool_search_tool_bm25, tool_search_tool_regex

- Update REQUIRED_BETAS: remove density-classifier signals
  (advanced-tool-use-2025-11-20, fast-mode-2026-02-01) identified via
  PR zacdcook#48 comparison against opencode-claude-auth

- Add per-model beta filtering (getModelBetas): skip
  interleaved-thinking for Haiku, skip effort for non-4.6 models

- Update Layer 2 replacements with improved obfuscation patterns
kongkong7777 added a commit to kongkong7777/openclaw-billing-proxy that referenced this pull request Apr 21, 2026
Ports 8 substitutions derived from upstream PR zacdcook#48 by JJValentin, which
itself ports 10 fixes from opencode-claude-auth v1.4.10 (verified in
production on OpenClaw Max subscription).

Our fork applies only the BODY-LEVEL changes. HTTP-header-level changes
in PR zacdcook#48 (beta header, Stainless version in transit) are either dead
weight (we don't send Anthropic-Beta) or overridden by downstream
CLIProxyAPI applyClaudeHeaders(). See README §三 for details.

Applied changes:
 - CC_TOOL_STUBS: mcp_ prefix on all 5 stubs (mcp_Glob, mcp_Grep,
   mcp_Agent, mcp_NotebookEdit, mcp_TodoRead)
 - DEFAULT_TOOL_RENAMES: mcp_ prefix on 13 non-CC tools (pdf,
   image_generate, music_generate, video_generate, memory_search,
   memory_get, lcm_expand_query, lcm_grep, lcm_describe, lcm_expand,
   yield_task, task_store, task_yield_interrupt). CC-native tools
   (Read/Write/Bash/etc) stay PascalCase without mcp_.
 - CCH: add computeCch() helper; buildBillingBlock now emits real
   SHA256(firstUserText)[:5] instead of hardcoded cch=00000.
 - buildBillingBlock: accepts optional preExtractedText so CCH is
   computed from the ORIGINAL user text (pre-Layer-2 transformation).
 - Layer 1 call site: passes originalFirstUserText saved at the top
   of processBody().
 - Layer 2.5: Haiku effort stripping (detects haiku in model name,
   strips effort from output_config and thinking objects — Haiku
   returns 400 if effort is present).
 - New helpers: findMatchingBrace (string-aware {/} matcher) and
   stripEffortFromObject.
 - repairToolPairs(): removes orphaned tool_use/tool_result blocks
   before any transforms run. Prevents Anthropic 400 errors on
   conversation histories with unmatched tool blocks (issue zacdcook#34).
   Includes adjacent-same-role guard.
 - x-stainless-package-version: bumped 0.81.0 -> 0.90.0 (this field
   is stripped/replaced by CLIProxyAPI in Plan B; kept for upstream
   parity and standalone deployments).

Verified on production:
 - node -c proxy.js passes
 - 3 smoke tests: Sonnet 4.6 (Messages), Opus 4.6 (OpenAI-compat),
   Haiku with effort all returned 200
 - [EFFORT] log line confirms Layer 2.5 triggered for Haiku
 - 28 real requests in 5 minutes post-deploy: 0 errors
kongkong7777 added a commit to kongkong7777/openclaw-billing-proxy that referenced this pull request Apr 21, 2026
Rewrites README around this fork's actual deployment pattern: chained
through CLIProxyAPI :18801, not direct to api.anthropic.com. The
original README was written for standalone use and misrepresented what
the fork does in production.

Main additions:
 - §零 three-line fork summary
 - §一 full Plan B architecture diagram (client -> nginx -> logger /
   billing-proxy :18804 -> CLIProxyAPI :18801 -> upstream)
 - §二 comprehensive body-level transform table (all 10+ layers
   including PR zacdcook#48 additions)
 - §三 ZOMBIE CODE section — documents that REQUIRED_BETAS,
   getModelBetas, getStainlessHeaders, and UA injection are RETAINED
   but NOT EFFECTIVE in Plan B because CLIProxyAPI's
   applyClaudeHeaders() scrubs and rebuilds all HTTP headers.
   Includes pointer: "want to change HTTP-level spoofing? edit
   CLIProxyAPI, not this repo."
 - §四 complete deployment guide: prerequisites, CLIProxyAPI config
   snippets, billing-proxy config.json, systemd unit (with
   Requires=cliproxyapi.service), nginx route examples, client
   config examples
 - §五 updated tool rename tables split into "CC-native (no prefix)"
   vs "non-CC (mcp_ prefix)"
 - §八 troubleshooting matrix for the full chain
 - §九 version history entry for PR zacdcook#48 port
 - §十 pointers to sidecar tools (billing-logger, claude-watchdog)
   that live outside this repo but complete the Plan B picture

Rationale for the body/header split emphasis: investigation confirmed
that Anthropic-Beta, X-Stainless-*, User-Agent, X-Claude-Code-Session-
Id, x-client-request-id, and CCH signing are all handled exclusively
by CLIProxyAPI in the Plan B path. Our proxy's remaining value is
purely body-level (tools, messages, system content, billing text
block, path protection, prompt caching). Future contributors should
not attempt to fix HTTP-header-level spoofing here.
kongkong7777 added a commit to kongkong7777/openclaw-billing-proxy that referenced this pull request Apr 21, 2026
Previously the rename table had an inconsistent mix: 7 entries mapping
to REAL CC tools (Bash/Read/Write/Edit/Grep/Glob/LS — no prefix) and
14 entries mapping to PascalCase names that LOOK like CC tools but
aren't (BashSession/BrowserControl/TaskCreate/etc — also no prefix).

PR zacdcook#48 added mcp_ prefix only to 13 of the most obviously non-CC tools
(pdf/music_generate/lcm_*/yield_task etc). The 14 middle-ground fake-CC
names were left bare, creating an inconsistent state where Anthropic's
CC-tool-set density classifier would see names that neither match real
CC nor mark themselves as MCP-user-tools.

This commit applies mcp_ prefix to all 14 remaining non-CC entries:
 - process → BashSession          → mcp_BashSession
 - browser → BrowserControl       → mcp_BrowserControl
 - canvas → CanvasView            → mcp_CanvasView
 - cron → Scheduler               → mcp_Scheduler
 - message → SendMessage          → mcp_SendMessage
 - tts → Speech                   → mcp_Speech
 - gateway → SystemCtl            → mcp_SystemCtl
 - agents_list → AgentList        → mcp_AgentList
 - create_task → TaskCreate       → mcp_TaskCreate
 - list_tasks → TaskList          → mcp_TaskList
 - get_history → TaskHistory      → mcp_TaskHistory
 - send_to_task → TaskSend        → mcp_TaskSend
 - subagents → AgentControl       → mcp_AgentControl
 - session_status → StatusCheck   → mcp_StatusCheck

Result: the rename table now has a clean binary split:
 - 7 entries → real CC tool names (no prefix, Anthropic sees CC)
 - 14 entries → mcp_ prefixed names (Anthropic sees user MCP tools)

REVERSE MAPPING: no changes needed. reverseMap() iterates config
.toolRenames in swapped direction, so forward update is symmetric.
DEFAULT_REVERSE_MAP handles only Layer-2 sanitize reverse and didn't
reference these names.

Verified end-to-end: sent a request with "process" tool defined, model
responded with tool_use name="process" (not mcp_BashSession), proving
the full chain process -> mcp_BashSession -> Anthropic -> mcp_BashSession
-> process -> client works correctly.

README §二 Layer 3 row updated, §五 split into table 2 (real CC, 7
entries) and table 3 (mcp_ prefix, 28 entries), §九 version history
adds this change.
tata-meow added a commit to tata-meow/openclaw-billing-proxy that referenced this pull request May 19, 2026
…t strip, multi-marker identity, tests

- OAuth token refresh: async getTokenAsync with race-condition dedup,
  disk persistence, and 401 cache invalidation
- Layer 2.5: stripEffortFromObject removes effort params for Haiku
  models (prevents 400 errors)
- Layer 4: multi-marker identity detection (6 variants) with improved
  end-boundary patterns (UNC paths, generic Windows drives)
- injectCCStubs defaults to false (issue zacdcook#43 — prevents tool-not-found
  loops)
- Stainless SDK version bump 0.81.0 → 0.90.0
- Async request handler for OAuth await support
- 51 unit tests in test-functions.js covering all pure functions

Co-Authored-By: Claude Opus 4.6 <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

2 participants