Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2329,14 +2329,33 @@ def build_anthropic_kwargs(

# ── OAuth: Claude Code identity ──────────────────────────────────
if is_oauth:
# 1. Prepend Claude Code system prompt identity
# 0. Prepend the billing-attribution block that the genuine Claude Code
# / Agent-SDK CLI sends as its FIRST system block:
# x-anthropic-billing-header: cc_version=<ver>; cc_entrypoint=sdk-cli;
# Anthropic's billing gate uses this to route OAuth/subscription
# requests to the plan's usage limits. Without it, tool-bearing
# requests are classified as a generic third-party app and rejected
# with HTTP 400 "Third-party apps now draw from extra usage, not plan
# limits" — even when every tool name is already on the double-
# underscore mcp__ wire form normalized below (step 3). Verified on a
# live Team plan: mcp__ normalization alone still 400s; adding this
# block returns 200. Captured from `claude -p` (v2.1.181).
billing_block = {
"type": "text",
"text": (
f"x-anthropic-billing-header: "
f"cc_version={_get_claude_code_version()}; "
f"cc_entrypoint=sdk-cli;"
),
}
# 1. Prepend Claude Code system prompt identity (after billing header)
cc_block = {"type": "text", "text": _CLAUDE_CODE_SYSTEM_PREFIX}
if isinstance(system, list):
system = [cc_block] + system
system = [billing_block, cc_block] + system
elif isinstance(system, str) and system:
system = [cc_block, {"type": "text", "text": system}]
system = [billing_block, cc_block, {"type": "text", "text": system}]
else:
system = [cc_block]
system = [billing_block, cc_block]

# 2. Sanitize system prompt — replace product name references
# to avoid Anthropic's server-side content filters.
Expand Down
65 changes: 65 additions & 0 deletions tests/agent/test_anthropic_oauth_billing_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Verify the OAuth billing-attribution system block.

The genuine Claude Code / Agent-SDK CLI sends an ``x-anthropic-billing-header``
text block as the FIRST entry of the system array
(``cc_version=...; cc_entrypoint=sdk-cli;``). Anthropic's billing gate uses it
to route OAuth/subscription requests to the plan's usage limits. Without it,
tool-bearing requests are classified as a generic third-party app and rejected
with HTTP 400 "Third-party apps now draw from extra usage, not plan limits."
"""


def _build(is_oauth, system=None, tools=None):
from agent.anthropic_adapter import build_anthropic_kwargs

messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": "Hi"})
return build_anthropic_kwargs(
model="claude-sonnet-4-6",
messages=messages,
tools=tools,
max_tokens=4096,
reasoning_config=None,
is_oauth=is_oauth,
)


def _is_billing_block(block):
return (
isinstance(block, dict)
and block.get("type") == "text"
and "x-anthropic-billing-header" in block.get("text", "")
and "cc_entrypoint=sdk-cli" in block.get("text", "")
)


class TestAnthropicOAuthBillingHeader:
def test_billing_block_is_first_on_oauth(self):
kwargs = _build(is_oauth=True, system="You are a helpful assistant.")
system = kwargs["system"]
assert isinstance(system, list)
assert _is_billing_block(system[0])
# Claude Code identity prefix follows the billing header.
assert system[1]["text"].startswith("You are Claude Code")

def test_billing_block_present_with_no_user_system(self):
kwargs = _build(is_oauth=True)
assert _is_billing_block(kwargs["system"][0])

def test_billing_block_present_with_tools(self):
kwargs = _build(
is_oauth=True,
tools=[{
"type": "function",
"function": {"name": "read_file", "description": "x", "parameters": {}},
}],
)
assert _is_billing_block(kwargs["system"][0])

def test_no_billing_block_on_non_oauth(self):
kwargs = _build(is_oauth=False, system="You are a helpful assistant.")
system = kwargs.get("system")
blocks = system if isinstance(system, list) else ([system] if system else [])
assert not any(_is_billing_block(b) for b in blocks)