Skip to content

Opt-in Claude Code identity injection for OAuth subscription tokens (#74) - #75

Merged
doramirdor merged 1 commit into
mainfrom
fix/oauth-claude-code-identity
Jun 26, 2026
Merged

Opt-in Claude Code identity injection for OAuth subscription tokens (#74)#75
doramirdor merged 1 commit into
mainfrom
fix/oauth-claude-code-identity

Conversation

@doramirdor

@doramirdor doramirdor commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the opt-in Claude Code identity injection flag floated in #74. After v0.21.0 fixed the credential path, the reporter (@ThunderEagle) retested and confirmed: Haiku works, but Sonnet/Opus still return an immediate rate_limit_error through both nadirclaw test and a direct POST /v1/messages. Haiku-OK-but-premium-gated on the same subscription token is the classic signature of Anthropic gating premium models behind the official Claude Code identity — the real client always leads its requests with a fixed identity system block; raw API/SDK callers omit it.

What this does

New env flag NADIRCLAW_CLAUDE_CODE_IDENTITY=1 (default off). When enabled, NadirClaw prepends

You are Claude Code, Anthropic's official CLI for Claude.

as the first system block on outgoing Anthropic requests, on both surfaces:

  • /v1/messages (the Anthropic-native path Claude Code + the reporter use)
  • the direct Anthropic OAuth path inside /v1/chat/completions

Guard rails:

  • OAuth only — injected only for Bearer / sk-ant-oat* tokens. No effect on sk-ant-api* API keys (those use x-api-key).
  • Idempotent — no-op if the request already leads with the identity block (so real Claude Code traffic is untouched).
  • Non-destructive — any caller-supplied system prompt is preserved, appended after the identity block; system is normalized to Anthropic's block-array form.
  • Observable — the decision is recorded as claude_code_identity on the request log.
  • Default off, since it changes the system prompt the model sees.

Drive-by fix

The direct Anthropic OAuth path in /v1/chat/completions was forwarding role:"system" messages inside the messages array, which Anthropic's /v1/messages API rejects (system must be a top-level field). System/developer turns are now collected into the top-level system field before forwarding.

Testing

  • New TestClaudeCodeIdentityInjection unit tests for the helper (no-system / string / block-array / already-present / noop cases).
  • New endpoint tests asserting injection happens only for OAuth + flag-on, and not for flag-off or API-key tokens.
  • Full test_server.py, test_oauth.py, test_tool_calling.py, test_streaming_fallback.py, test_thinking_passthrough.py pass (91 tests).

For the reporter to try

pip install -U nadirclaw        # once released, or install from this branch
export NADIRCLAW_CLAUDE_CODE_IDENTITY=1
nadirclaw serve
# then retest claude-sonnet-4-6 / opus through /v1/messages

If Sonnet/Opus start returning completions with the flag on, this was the cause. If they still rate_limit_error, that points to plan-tier model-access gating (Pro vs Max) on the subscription itself rather than anything actionable in NadirClaw.

Refs #74 — keeping the issue open until the reporter confirms the flag resolves the gating (or rules it out as plan-tier).

🤖 Generated with Claude Code

Anthropic gates premium models (Sonnet/Opus) behind subscription/OAuth
tokens (sk-ant-oat*) unless the request leads with the official Claude
Code identity system block. The real client always sends it; raw API/SDK
callers omit it and get a bare rate_limit_error on those models while
Haiku works.

Add NADIRCLAW_CLAUDE_CODE_IDENTITY (default off). When enabled,
/v1/messages and the OAuth completion path prepend
"You are Claude Code, Anthropic's official CLI for Claude." as the first
system block — only for Bearer/OAuth tokens, only when not already
present, preserving any caller-supplied system prompt after it. Recorded
as claude_code_identity on the request log.

Also fix the OAuth completion path forwarding role:"system" messages
inside the messages array (Anthropic requires system as a top-level
field); system/developer turns are now collected into the top-level
system field.

Co-Authored-By: Claude Opus 4.8 (1M context) <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

Development

Successfully merging this pull request may close these issues.

1 participant