Claude Code 2.1.220 through 2.1.234 emit the cch attribution only for
firstParty on api.anthropic.com and for vertex; every other backend sends
the billing header unsigned. CPA had dropped its endpoint check, so an
opted-in API key signed a per-request hash on any gateway and could bust
that gateway's prompt cache.
- Restore the endpoint gate in claudeCCHSigningEnabled: a real Claude OAuth
credential still signs on every upstream, because a downstream Claude Code
pointed at CPA cannot produce that value itself, while a claude-code-cli
API key signs only on api.anthropic.com or Vertex
- Drop the unused origin parameter from Claude fingerprint policy resolution
and restore the original resolveClaudeWirePolicy signature; the wire profile
follows the credential and only CCH follows the origin
- Add config.NormalizeClaudeFingerprintProfile / ValidateClaudeFingerprintProfile
as the single source of truth for fingerprint-profile values
- Reject unknown fingerprint-profile values in the Management API, and warn
once per distinct value at request time instead of on every resolution,
which previously logged about four warnings per request for one typo
- Preserve unrecognized values through config sanitization so rewriting a
config file never discards operator input
- Update config.example.yaml and tests for the origin-scoped CCH behavior
* feat(config): add fingerprint-profile to Claude keys and auth JSON
- Add FingerprintProfile to ClaudeKey configuration struct and normalizer
- Track fingerprint-profile in config diff
- Map fingerprint-profile / fingerprint_profile to auth attributes in file and config synthesizers
- Support fingerprint-profile in Management API PatchClaudeKey and normalization
- Add Claude billing attribution string manipulation utilities in internal/util
- Document fingerprint-profile options in config.example.yaml
* feat(claude): add fingerprint policy and request-local CLI identity
- Centralize Claude fingerprint policy resolution in claude_fingerprint_policy.go
- Support stable Claude CLI identity synthesis (UUIDv5 account_uuid and SHA-256 device_id)
seeded from API keys or stable OAuth IDs, keeping access tokens isolated
- Warn on unrecognized fingerprint-profile values
* feat(claude): apply CLI fingerprint to Messages and keep API keys caller-owned
- Wire centralized fingerprint policy into Claude and Kimi executors
- Keep first-party Anthropic API keys and delegated providers caller-owned by default
- Apply Claude Code CLI wire profile (betas, metadata, diagnostics, MCP aliases)
when fingerprint-profile=claude-code-cli is configured
- Strictly align CCH signing with native Claude Code 2.1.220: only first-party
api.anthropic.com and Vertex sign dynamic CCH; third-party gateways and Kimi
receive billing header without cch= to avoid prompt cache busting
- Respect caller-owned count_tokens bodies by default while aligning CLI shape on opt-in
- Fall back to CLIProxyAPI/<version> User-Agent when caller sends no UA in caller-owned mode
- Scope custom operator header overrides accurately in caller-owned mode
- Add comprehensive test coverage for policy resolution, gateway opt-in, Kimi, and token counting
injectClaudeCodeCurrentDate inserted the reminder at index 0 of the first user message. Anthropic requires the message after an assistant tool_use turn to lead with its tool_result blocks, so the request was rejected with 400. Advance the insert index past leading tool_result blocks, matching the existing guard in prependClaudeSystemRemindersToFirstUserMessage. Every other content shape keeps the current index-0 placement.
A caller can put a {"role":"system"} turn inside messages. Models older than
the role=system turn reject it outright, verified against api.anthropic.com on
both /v1/messages and /v1/messages/count_tokens:
400 role 'system' is not supported on this model
claude-haiku-4-5, claude-sonnet-4-5, claude-sonnet-4-6 and claude-opus-4-6
answer that way, while claude-sonnet-5 and claude-opus-5 accept the turn.
claudeLegacySystemReminderModels already enumerates that boundary, which is why
claudeCodeCLIBetas withholds mid-conversation-system-2026-04-07 for those
models, but caller-provided turns were forwarded unchanged and always spent an
upstream call on a guaranteed rejection.
The native client does not produce the pairing either: it gates the turn on the
model. In 314 captured native requests the turn appears only on
claude-opus-5 and claude-sonnet-5, and on none of the 43 requests addressed to
a model in that set. That is an observation about the captures rather than a
proof about the upstream, so it only corroborates the measured rejection.
Validate the finished body, and only inside the evidence that produced the
rule:
- The check runs after the body is finalized and before
http.NewRequestWithContext. Payload rules can rewrite model and messages long
after translation, so an earlier check would not describe what is sent.
- Only Anthropic's first-party origin is covered, matching the reasoning
shouldUseClaudeUpstreamTokenCount already applies to count_tokens. A
third-party gateway may map these model IDs onto something that accepts the
turn and therefore decides for itself.
- A confirmed native caller keeps the passthrough. It gates the turn itself, so
its body is forwarded untouched and the upstream error reaches it unchanged.
- Unknown and future model IDs stay optimistic and are forwarded, matching how
checkSystemInstructions treats them.
- rebuild_mid_system_message still folds caller turns into the system slot; the
final check therefore preserves the explicit escape hatch.
Cloaking adds one extra ordering case: it can place a caller's top-level system
prompt into a role=system turn for a modern model before a payload rule changes
the model to legacy. Track only the exact contiguous turns that CPA inserted,
using both their position and the corresponding message-count increase as
provenance. After payload rules settle the model, replay those turns through the
existing legacy <system-reminder> path. Pre-existing caller turns, even if they
have identical content, remain caller-owned and are still rejected. If payload
rules also rewrite the tracked messages, reconciliation fails closed and final
validation returns 400 rather than guessing provenance.
The 400 is request-scoped like claudeCallerSystemBlockError: the invalid
body/model pairing is independent of first-party credential health, so no
credential is cooled or retried.
Translated requests never carry the pairing, because every non-Claude source
format hoists system content into the top-level system field. Tests pin that for
OpenAI, Gemini, Responses and Interactions, and separately drive Execute,
ExecuteStream and CountTokens through an injected first-party transport.
Anthropic rejects the injected clear_thinking_20251015 strategy unless
thinking is enabled or adaptive:
`clear_thinking_20251015` strategy requires `thinking` to be enabled or
adaptive
Both sides of the automatic injection only special-cased the literal
{"type":"disabled"}, so an absent thinking field slipped through
gjson's empty string and the request was sent with a strategy the API
refuses. Two reachable paths produced it:
- a cloaked caller that never sent thinking at all;
- a caller whose thinking was enabled at injection time and then removed
by disableThinkingIfToolChoiceForced, which runs between injection and
reconciliation.
Both now test the accepted values instead of excluding the disabled one,
via a shared claudeThinkingAcceptsClearThinking helper, so reconciliation
also withdraws an object CPA injected itself once thinking disappears.
Caller-owned and payload-rule-owned objects keep their existing
precedence and are never withdrawn.
Verified against api.anthropic.com: context_management with thinking
absent or disabled returns 400, with thinking enabled returns 200. The
existing expectation that forced tool choice retains the automatic object
pinned the rejected shape and is inverted accordingly.
Cloaked Responses/Chat/Gemini→Claude traffic was skipping ensureCacheControl
once the first-user cloak marker existed, freezing multi-turn cache hits.
Always ensure section-independent system and rolling-message breakpoints for
non-native callers, keep confirmed Claude Code placement intact, and leave
tools unstamped when a usable system prompt already covers the prefix.
Align the wire shape with the Claude Code 2.1.220/2.1.221/2.1.227 cache-control
constructor: the default is {"type":"ephemeral"} with no ttl. A 1h body ttl is
applied only after placement, only for OAuth credentials, and only onto blocks
that already carry cache_control, so it stays strictly paired with
extended-cache-ttl-2025-04-11. Explicit caller-owned 5m choices survive.
Match the native rolling selector (skip thinking-like assistant tails; final
system string special case), fall back to a tools breakpoint when system is
absent or blank, and leave empty/whitespace string systems unconverted so they
cannot double-stamp tools plus a useless system host.
OpenAI callers can raise instructions above user content in three ways
and Claude has one system slot for all of them, but the translators
disagreed on where each one landed. Responses instructions became a
leading user turn, a role=system item was only demoted for the first
item, role=developer silently became user text, and the Chat translator
dropped developer messages outright. An operator instruction could
therefore lose its authority or disappear without any signal.
All five sources now become separate top-level Claude system blocks in
source order, so the executor cloak decides the final placement on its
own: a mid-conversation role=system message on models that accept one,
an individual <system-reminder> block on legacy models. Blocks are never
merged, trimmed, reordered or demoted.
Anthropic only accepts text in a system slot. Verified against
api.anthropic.com: the top-level system field rejects anything else with
"system.<i>.type: Input should be 'text'", and a role=system message
accepts text, tool_addition and tool_removal only. A non-text system part
is therefore kept as a typed marker without its payload and rejected by
the cloak with a request-scoped 400 that names the offending type. That
keeps the failure local, spends no upstream call on a request that cannot
succeed, and stops caller content from ever reaching the top-level system
field where it would break the Claude Code fingerprint shape.
A cloaked direct-Anthropic count_tokens request skipped applyCloaking entirely
while still reporting cloaked=true, so two guarantees that hold on the Messages
path were silently dropped on this endpoint:
- configured sensitive words reached Anthropic verbatim, even though the same
words are obfuscated on the Messages request
- a third-party caller's system prompt was forwarded in the system slot, which
measured Claude Code 2.1.220 count_tokens traffic never carries
Skipping the full Messages cloaking is still correct here, because native
count_tokens sends only model, messages and tools and must not gain the Claude
Code system blocks. Apply the two parts that do have to hold instead: relocate
the caller's system prompt into messages with the same positional mapping the
Messages path uses, so its tokens stay counted without leaking it as a system
prompt, and obfuscate sensitive words. Strict mode keeps dropping caller
prompts, matching the Messages path.
Align the remaining measured OAuth wire profiles, including the ordered
connection writer in internal/httpwire that reproduces the observed header
sequence, and the refresh/profile response shapes in internal/auth/claude.
Replay the measured Fast path and keep diagnostic continuity across cloaked and
native requests.
Preserve the native direct token-counting shape so a caller that reaches
count_tokens itself is not reshaped into the cloaked form.
Scope cloak dates to the credential's timezone rather than the host's, so
currentDate matches what the real client would have sent for that account.
Claude Code builds Anthropic-Beta per request instead of sending a fixed list.
Captured from an isolated 2.1.220 profile pointed at api.anthropic.com through
a local proxy, over two rounds covering 11 model IDs and the [1m] variants:
constant claude-code, interleaved-thinking, redact-thinking,
thinking-token-count, context-management, prompt-caching-scope
tools advanced-tool-use-2025-11-20 only when tools are declared
model mid-conversation-system-2026-04-07 only on models that accept a
role=system turn
[1m] context-1m-2025-08-07, directly after claude-code-20250219 rather
than at the end
trailing effort-2025-11-24, then server-side-fallback-2026-06-01
claude-sonnet-5 emits mid-conversation-system-2026-04-07, so it accepts a
role=system turn and must not sit in the legacy reminder whitelist.
count_tokens does not reuse the inference fingerprint. Running /context in an
interactive session issues 37 identical calls, which made the endpoint
observable for the first time: four betas only, and 21 headers rather than 22
because X-Stainless-Timeout is absent. The profile is selected from the request
path so no call site has to thread another flag.
Preserve a cloaked caller's own system prompt instead of discarding it, place
it as a mid-conversation system turn on models that accept one, and route the
remaining legacy models through system reminders.
Scope the legacy reminder whitelist to official model IDs. claude-opus-4-6-thinking
was dropped: Anthropic publishes no -thinking IDs, that one belongs to the
antigravity provider in models.json and is served by a different executor, so it
can never reach ClaudeExecutor cloaking. Keeping it implied that synthetic
suffixes are normalized here, which they are not, since thinking.ParseSuffix only
strips parenthesis suffixes. The map is anchored to the "claude" provider block
plus Anthropic's bare and "-latest" aliases, and now covers claude-opus-4-7.
Detect confirmed CLI, sdk-cli and VSCode callers before mutation so native
software, system, tool, cache and beta shapes pass through, while unconfirmed
OAuth clients receive a coherent minimum CLI identity.
Persist each Claude OAuth credential's upstream account metadata and one stable
device ID, derive one stable session per agent conversation, and keep body and
header identity synchronized across Messages, streaming and count_tokens.
Alias every cloaked third-party custom tool through caller-stable opaque MCP
names and restore declarations, choices, history, references, non-stream
responses and SSE events without changing tool ownership.
Implement the Claude Code 2.1.220 CCH algorithm over the final serialized
request bytes, align currentDate and first-user cache layout, update the
official beta/header baseline, and use upstream count_tokens for OAuth and
first-party Anthropic credentials.
Match the 2.1.220 TLS ClientHello so the transport fingerprint agrees with the
identity the request now claims, and document the CLI defaults and automatic
OAuth signing / tool alias behaviour in config.example.yaml.
- Removed deprecated interceptor and executor-related methods, including `callRequestInterceptor`, `callResponseInterceptor`, and `callStreamChunkInterceptor`.
- Consolidated unused logic and pruned redundant imports to streamline `adapters.go`.
- No functional changes.