2 Commits

Author SHA1 Message Date
sususu98
f1b0431c77 feat(claude): add fingerprint-profile=claude-code-cli for API keys and delegated providers (#5047)
* 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
2026-08-18 17:29:26 +08:00
sususu
189776aab1 fix(claude): validate legacy-model system turns before sending
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.
2026-08-11 22:56:41 +08:00