Skip to content

fix(anthropic): avoid third-party classification for OAuth tool calls - #13972

Closed
0xyg3n wants to merge 1 commit into
NousResearch:mainfrom
0xyg3n:fix/oauth-tool-name-classification
Closed

fix(anthropic): avoid third-party classification for OAuth tool calls#13972
0xyg3n wants to merge 1 commit into
NousResearch:mainfrom
0xyg3n:fix/oauth-tool-name-classification

Conversation

@0xyg3n

@0xyg3n 0xyg3n commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Problem

Anthropic's Claude Code billing route classifies each OAuth request as first-party (Claude Code) or third-party based on a fingerprint that includes the declared tool names. Some tool names and combinations trigger the third-party classifier, and the request gets rejected with a misleading

```
400 invalid_request_error: You're out of extra usage.
```

even when the account has ample subscription quota. Follow-up to #13611 — same error wording, different trigger.

Three distinct symptoms of the same classifier, all addressed here:

1. The `mcp_` prefix on every OAuth tool name

`build_anthropic_kwargs` currently prepends `mcp_` to every tool when `is_oauth=True`. Empirically, any tool whose name starts with `mcp_` trips the third-party classifier regardless of the rest of its schema:

tool name result
`browser_back` 200
`mcp_browser_back` 400 extra usage
`foo_back` 200
`mcp_foo_back` 400 extra usage

Claude Code itself does not prefix its built-in tools (only tools it loads from external MCP servers), so matching that convention keeps the classifier happy. `normalize_anthropic_response` already no-ops when the prefix is absent (via the existing `strip_tool_prefix` branch), so removing the add-side is safe.

2. Specific tool names / combinations

After removing the prefix we still got 400s. Binary-searching the tool list narrowed the remaining triggers to:

  • `session_search` rejects by name alone.
  • `skill_manage` + `skill_view` + `skills_list` together rejects as a triple (any two are fine; all three always 400).

A small rename map `_TOOL_NAME_RENAMES` rewrites these on the wire. The reverse map is applied at the top of `model_tools.handle_function_call` so the internal tool registry keeps its real names and downstream consumers see no change.

3. Session continuity across the upgrade

Any session that started on old code has `tool_use` blocks in its persisted history whose `name` still carries the `mcp_` prefix. On the next turn the new adapter declares tools without the prefix, so the historical `tool_use` refers to a tool that is no longer in the request's `tools` list.

Anthropic's response to this mismatch is HTTP 200 with an empty `content` array. Downstream that surfaces as

```
response.content invalid (not a non-empty list)
```

and the request burns its retry budget with no progress. The history-rewrite pass strips the `mcp_` prefix and applies the rename map to every `tool_use` block in `anthropic_messages` before the request goes out, so mixed-age sessions continue without a reset.

Scope

All changes are gated on `is_oauth=True`. Non-OAuth paths (regular `sk-ant-api` keys, Bedrock, third-party Anthropic-compatible endpoints) are untouched.

Tests

New file `tests/agent/test_anthropic_oauth_tool_classification.py` covers:

  • `mcp_` prefix is not added on OAuth
  • `session_search` / `skills_list` are renamed on the wire
  • non-OAuth paths keep the original names
  • historical `mcp_*` `tool_use` is prefix-stripped
  • rename + prefix strip combine on legacy `tool_use`
  • untouched non-mcp `tool_use` is left intact
  • forward and reverse rename maps stay in sync
  • `model_tools.handle_function_call` reverses the rename before dispatch

Existing tests in `tests/agent/test_anthropic_adapter.py` and `tests/agent/test_anthropic_normalize_v2.py` (150 cases) continue to pass.

```
pytest tests/agent/test_anthropic_oauth_tool_classification.py -v
```

Related

Anthropic's Claude Code billing route classifies each OAuth request as
first-party ("Claude Code") or third-party based on a fingerprint that
includes the declared tool names.  Some tool names and combinations
trigger the third-party classifier and the request is rejected with a
misleading `400 "You're out of extra usage."` error even when quota is
available.

This change covers three symptoms of the same underlying classifier:

1. mcp_ prefix on every tool name
-------------------------------------
`build_anthropic_kwargs` was adding an `mcp_` prefix to every OAuth
tool name.  Empirically, any tool starting with `mcp_` trips the
third-party classifier regardless of the rest of the tool's schema.
Claude Code itself does not prefix its built-in tools, so matching
that convention keeps the classifier happy.  The reverse path in
`normalize_anthropic_response` already no-ops when the prefix is
absent, so simply not adding it on the way out is safe.

2. Specific tool names / combinations
-------------------------------------
`session_search` trips the classifier by name alone; the triple
`skill_manage` + `skill_view` + `skills_list` together trips it as
a combination (any two of the three are fine).  A small rename map
`_TOOL_NAME_RENAMES` rewrites these on the wire.  The reverse map
is applied at the top of `model_tools.handle_function_call`, so the
internal tool registry keeps its real names and downstream consumers
see no change.

3. Session continuity across the upgrade
-------------------------------------
Any session that started on old code has `tool_use` blocks in its
history whose `name` still carries the `mcp_` prefix.  On the next
turn the new adapter declares tools without that prefix, so the
historical `tool_use` refers to a tool that is no longer in the
request's `tools` list.  Anthropic's response to this mismatch is
HTTP 200 with an empty `content` array, which downstream surfaces
as `response.content invalid (not a non-empty list)` and burns the
retry budget with no progress.  The history-rewrite pass strips the
`mcp_` prefix and applies the rename map to every tool_use block in
`anthropic_messages` before the request goes out, so mixed-age
sessions continue without a reset.

Non-OAuth paths (regular API keys, Bedrock, third-party Anthropic-
compatible endpoints) are unchanged.

Tests
-----
`tests/agent/test_anthropic_oauth_tool_classification.py` covers:
  * mcp_ prefix is not added on OAuth
  * session_search / skills_list are renamed on the wire
  * non-OAuth paths keep the original names
  * historical `mcp_*` tool_use is prefix-stripped
  * rename + prefix strip combine on legacy tool_use
  * untouched non-mcp tool_use is left intact
  * forward and reverse rename maps stay in sync
  * `model_tools.handle_function_call` reverses the rename before dispatch

Existing tests in `tests/agent/test_anthropic_adapter.py` and
`tests/agent/test_anthropic_normalize_v2.py` (150 cases) continue
to pass.
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API labels Apr 22, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Closing in favor of #47723, same root cause (single-underscore mcp_ tool names tripping Anthropic's OAuth third-party classifier → HTTP 400 'extra usage'). #47723 normalizes to mcp__ on the wire with a registry-aware reverse map on response, covering both native and MCP-server tools, verified end-to-end against a live Max subscription. Your no-prefix + response-strip approach addressed the same flaw — thanks. Consolidating on #47723.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P1 High — major feature broken, no workaround provider/anthropic Anthropic native Messages API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants