Skip to content

fix(anthropic-oauth): opus-4-7 body-shape parity with Claude Code 2.1.112 - #5

Merged
mssteuer merged 1 commit into
mainfrom
fix/oauth-body-shape-opus-4-7-2026-04-17
Apr 17, 2026
Merged

fix(anthropic-oauth): opus-4-7 body-shape parity with Claude Code 2.1.112#5
mssteuer merged 1 commit into
mainfrom
fix/oauth-body-shape-opus-4-7-2026-04-17

Conversation

@mssteuer

Copy link
Copy Markdown
Owner

What

Claude Code 2.1.112 was observed (capture 2026-04-17) sending this shape on opus-4-7 /v1/messages requests:

thinking:           {"type":"adaptive"}
output_config:      {"effort":"xhigh"}
context_management: {"edits":[{"type":"clear_thinking_20251015","keep":"all"}]}
temperature:        (absent)
tool_choice:        (absent when caller doesn't request one)

Hermes was sending:

thinking:    {"type":"enabled","budget_tokens":4000}
temperature: 1
tool_choice: {"type":"auto"}
(no output_config, no context_management)

Why now

PR #3 (fingerprint refresh) and PR #4 (Stainless/JS SDK spoofing) got the headers + client identity right but Extra Usage kept climbing. Michael asked me to check whether the system prompt / body shape was the remaining drift before chasing TLS/JA3 (the nuclear option). A mitmproxy capture of a real claude --model claude-opus-4-7 --print "..." session against our own OAuth token revealed body-shape drift, not TLS. Sent a hand-crafted pure-CC-shape request through our post-PR-#4 stack: Anthropic returned 200 OK, no issues. Body shape is the remaining drift layer.

Root cause

_supports_adaptive_thinking() only matched 4-6 / 4.6 substrings. Opus 4.7 (released after the adapter was written) fell through to the legacy manual-thinking branch written for 4.5 and earlier, which sets thinking:{enabled,budget} and temperature:1. Opus 4.7 rejects temperature with a 400 temperature is deprecated for this model — we saw this in journalctl right after the PR #3 deploy but it wasn't blocking because the errors were retried. The classifier reads the broken shape as drift.

Changes

Single commit, 2 files, +240/-11.

agent/anthropic_adapter.py (+72/-11)

  • _supports_adaptive_thinking: match 4-7 / 4.7 in addition to 4.6. Per Michael: just 4.7, not broader.
  • ADAPTIVE_EFFORT_MAP: xhigh → xhigh (identity). CC sends xhigh literally; the old xhigh → max mapping was a fingerprint-drift signal.
  • _ANTHROPIC_OUTPUT_LIMITS: explicit claude-opus-4-7 → 128_000 entry so the output ceiling doesn't fall through the substring table to claude-opus-4 → 32_000. Kept 128K per Michael — CC clips to 64K but the model supports the full 128K.
  • Adaptive branch now emits context_management with the clear_thinking_20251015 cleanup edit. Routed via extra_body because anthropic Python SDK 0.92.0 doesn't accept it as a native kwarg yet.
  • Adaptive branch no longer sets temperature. Legacy branch (4.5, 3.7, etc.) still sets temperature=1 for manual-thinking models.
  • tool_choice: emit only when caller explicitly passed a value. Previously emitted {"type":"auto"} unconditionally. CC never sends tool_choice for default (auto) behavior.

tests/agent/test_anthropic_adapter.py (+179)

  • New TestOpus47BodyShapeParity class, 14 tests:
    • 3 tests for the three adaptive-thinking fields (thinking type, output_config.effort, context_management shape).
    • 4 tests for tool_choice behavior (omit when None, auto when asked, any for required, specific tool name).
    • 1 test asserting no temperature on adaptive-thinking models.
    • 1 test asserting max_tokens == 128_000 for opus-4-7.
    • 2 tests for effort passthrough (xhigh and medium).
    • 1 test for _supports_adaptive_thinking matching 4-7 variants + regression guards.
    • 1 test confirming legacy (sonnet-4-5) branch still uses manual-thinking shape.
    • 1 test confirming _get_anthropic_max_output('claude-opus-4-7') == 128_000.
  • Existing test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models renamed + rewritten to assert the corrected xhigh → xhigh identity mapping.

Tests

172 adapter tests pass (was 158; +14 new, 0 regressions).

Verified end-to-end

Built a real OAuth Anthropic client with the patched code, routed through mitmproxy, fired a real /v1/messages call on claude-opus-4-7. Anthropic returned 200 OK; captured request body matches CC byte-for-byte on all 5 parity deltas. Side-by-side diff shows the only remaining difference is stream (present on CC; absent in the non-streaming smoke test — gateway streams in production).

Verification next steps for reviewer

  1. Merge / deploy.
  2. Let OAuth traffic run for 30-60 min at normal load.
  3. Diff Extra Usage growth on Claude.ai dashboard before/after this PR.
  4. If Extra Usage flattens → we've found the last drift layer, done with fingerprint work.
  5. If it keeps climbing → the last remaining hypothesis is TLS/JA3 (Python httpx/OpenSSL vs Node undici handshake). Meaningfully harder to spoof; only worth chasing if this PR doesn't move the needle.

Note

No changes to the non-OAuth (regular API-key) code paths. Legacy manual-thinking for sonnet-4-5 and earlier is preserved unchanged.

….112

Captured Claude Code 2.1.112 sending these fields on opus-4-7 requests
(reference: ~/.hermes/reference/oauth-audit-2026-04-17-verify/):

  thinking:           {"type":"adaptive"}
  output_config:      {"effort":"xhigh"}
  context_management: {"edits":[{"type":"clear_thinking_20251015",
                        "keep":"all"}]}
  temperature:        (absent)
  tool_choice:        (absent when caller doesn't request one)

Hermes was sending:

  thinking:    {"type":"enabled","budget_tokens":4000}
  temperature: 1
  tool_choice: {"type":"auto"}
  (no output_config, no context_management)

Root cause: `_supports_adaptive_thinking` only matched "4-6" / "4.6", so
opus-4-7 fell through to the legacy manual-thinking branch written for
4.5 and earlier.  opus-4-7 now rejects `temperature` with a 400
"temperature is deprecated for this model" — caught in journalctl after
PR #3 deploy but not blocking as errors were retried.  More importantly,
the broken shape was a fingerprint-drift signal the Anthropic billing
classifier reads, keeping OAuth traffic in the Extra Usage lane instead
of the Max weekly lane.

Changes (agent/anthropic_adapter.py):

- `_supports_adaptive_thinking`: match "4-7" / "4.7" in addition to 4.6.
  Lowercase input for case-insensitive match.
- `ADAPTIVE_EFFORT_MAP`: "xhigh" → "xhigh" (identity).  Was "xhigh" →
  "max" — CC sends "xhigh" literally.
- `_ANTHROPIC_OUTPUT_LIMITS`: explicit "claude-opus-4-7" → 128_000 entry
  so the output ceiling doesn't fall through the substring table to
  "claude-opus-4" → 32_000.
- Adaptive branch now emits `context_management` with the
  clear_thinking_20251015 cleanup edit.  Routed via `extra_body` because
  anthropic Python SDK 0.92.0 doesn't accept it as a native kwarg yet.
- Adaptive branch no longer sets `temperature`.  Legacy branch still
  sets `temperature=1` for manual-thinking models (4.5 and earlier).
- `tool_choice`: emit only when caller explicitly passed a value.
  Previously emitted `{"type":"auto"}` unconditionally.  CC never sends
  `tool_choice` for default (auto) behavior.

Tests:

New `TestOpus47BodyShapeParity` class (14 tests) locks down each
parity item + the legacy-branch invariant.  Existing
`test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models`
renamed and rewritten to assert the corrected identity mapping.

172 adapter tests pass (was 158, +14 new, 0 regressions).

Verified end-to-end: built real OAuth client, routed via mitmproxy,
fired real /v1/messages call on opus-4-7.  Anthropic returned 200,
captured request body matches CC byte-for-byte on all 5 deltas.  Only
remaining difference is `stream` (present on CC; absent in the
non-streaming smoke test — gateway streams in production).
@mssteuer
mssteuer merged commit 9c1bc78 into main Apr 17, 2026
4 of 5 checks passed
mssteuer pushed a commit that referenced this pull request Apr 17, 2026
…dentity sanitizer

Root cause of residual Extra Usage billing drift: Hermes sent its own persona
(Jean Clawd / harness instructions) in system[2]/system[3]. Anthropic's
billing classifier reads system-block content, so any non-CC persona was a
fingerprint miss even with wire headers (#3), Stainless/JS spoof (#4), and
opus-4-7 body shape (#5) all correct.

Changes:
- Ship verbatim CC 2.1.112 captures of system[2] (persona) and system[3]
  (tool/output rules) under agent/_cc_parity_assets/, loaded at request
  build time and cached.
- Drop context-1m-2025-08-07 from _OAUTH_ONLY_BETAS — CC no longer sends it
  on opus-4-7 (captured 2026-04-17).
- Retire the _OAUTH_SANITIZE_REPLACEMENTS table. With full system-prompt
  spoofing the per-phrase replacements are vestigial noise that kept
  leaking identity mappings (#Nous Research → Anthropic, etc.) into the
  wire. Skip the TestOAuthSanitizer class; keep the fingerprint-scrub
  assertions at the build_anthropic_kwargs boundary.
- Package the _cc_parity_assets directory via setuptools package-data so
  it ships with the wheel.

Refresh procedure for CC version bumps: see agent/_cc_parity_assets/README.md.
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.

1 participant