Skip to content

fix(anthropic): move oversized OAuth system prompt to user prefix - #13611

Open
0xyg3n wants to merge 1 commit into
NousResearch:mainfrom
0xyg3n:fix/anthropic-oauth-system-overflow
Open

fix(anthropic): move oversized OAuth system prompt to user prefix#13611
0xyg3n wants to merge 1 commit into
NousResearch:mainfrom
0xyg3n:fix/anthropic-oauth-system-overflow

Conversation

@0xyg3n

@0xyg3n 0xyg3n commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Problem

OAuth / Claude Code subscription requests via anthropic_messages fail with a 400 when the system field contains custom content beyond the CC identity block:

400 invalid_request_error: You're out of extra usage.
Add more at claude.ai/settings/usage and keep going.

The error wording points at quota, but the account actually has usage available. The same payload sent directly with a pay-per-token API key returns 200. The check is specific to the OAuth / subscription route and triggered by the size/content of system once it goes beyond the basic CC identity string.

This breaks any downstream that ships a non-trivial system prompt (custom agents, extended tool guides) while using a Claude Code subscription token.

Fix

Keep only the CC identity block in system and fold any additional system blocks into a [CONTEXT]...[/CONTEXT] user-message prefix followed by an assistant Understood. acknowledgement. Content is preserved verbatim — only the transport slot changes.

The change is inside the existing if is_oauth: branch in build_anthropic_kwargs, so it only affects OAuth code paths. Non-OAuth requests (x-api-key, Bedrock, third-party Anthropic-compat endpoints) are untouched.

Test

Reproduced and verified against the Anthropic API on the latest main (b2111a2):

  • Before: large system via OAuth → 400 invalid_request_error with the quota text.
  • After: same content, CC identity in system + rest in [CONTEXT] user prefix → 200, response text identical, latency unchanged.

Integration tested in a production Hermes 0.10.0 deployment running Opus 4.7 via OAuth with a ~3k-token system prompt: 400 errors cleared, message flow back to normal (4 api_calls / 36s on a cold session with tools).

No changes to non-OAuth paths. No behavioural change expected for users with system containing only the CC identity block.

OAuth subscription requests (Claude Code route) fail with 400
'You're out of extra usage. Add more at claude.ai/settings/usage'
when the system field contains large custom content beyond the CC
identity block. The same payload succeeds with a pay-per-token API
key, so the check is subscription-route specific and orthogonal to
quota.

Fix: keep only the CC identity block in system and fold any extra
system blocks into a [CONTEXT]...[/CONTEXT] user-message prefix
followed by an assistant 'Understood.' acknowledgement. Content is
preserved verbatim; only the transport slot changes.

Tested on 0.10.0 (commit b2111a2) with Opus 4.7 OAuth and a
SOUL-style ~3k-token system prompt: 400 -> 200 with response
latency unchanged.
@trevorgordon981

Copy link
Copy Markdown
Contributor

I have tested this solution locally by running the full authentication and OAuth test suite. All 8 tests passed successfully, confirming that:

  1. Claude Code credentials are correctly prioritized.
  2. Manual tokens persist as expected.
  3. The provider gate logic handles all edge cases (missing config, mismatched providers, env vars) without regression.

The fix effectively resolves the system prompt overflow issue by moving the oversized OAuth prompt to the user prefix. The solution is stable and ready for merge.

Tested and confirmed. ✅

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools labels Apr 21, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Supersedes closed #13609 (same fix, likely rebased). Related to #10576/#10575 (same OAuth system prompt issue, earlier approaches).

@zeitgeistlive

Copy link
Copy Markdown

Tested on v0.10.0 (commit 2367c6f) with OAuth token — patch does not resolve the issue.

The overflow logic triggers correctly (confirmed via debug logging): system has 2 blocks, the second block (~20K chars) is moved to a [CONTEXT] user-message prefix, leaving only the 57-char CC identity block in system.

Despite this, the request still fails with:

400: "You're out of extra usage. Add more at claude.ai/settings/usage and keep going."

This suggests Anthropic's filter inspects the full request payload (including message content), not just the system field. Moving content from system to a user message doesn't bypass the check.

@0xyg3n

0xyg3n commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Hit the same 400 on our side and the overflow handler was only part of the picture. Wanted to leave notes in case anyone else goes down the same rabbit hole.

Initial symptom was identical to the PR description: 400 invalid_request_error: You're out of extra usage on an OAuth token that had plenty of subscription left. Applied this PR's fix locally first (system collapsed to CC identity + billing header, overflow moved to a [CONTEXT] user prefix). That cleared some of the 400s but not all. Full tool-enabled calls still failed.

What threw us for a while was that a direct client.messages.create against the same OAuth token with the same system/messages shape returned 200 fine. Only the gateway path with the real tool list failed. So we grabbed a saved request dump and replayed it field-by-field in a REPL, stripping one kwarg at a time:

full request     → 400  extra usage
no output_config → 400
no thinking      → 400
no tool_choice   → 400
no extra_headers → 400
no tools         → 200   (or 429 from repeated testing)

So it's tools. Swapping the real tool list for a single synthetic tool ({"name":"t","description":"x","input_schema":{"type":"object","properties":{},"required":[]}}) in an otherwise identical body returned 200. Something about specific tools was the trigger, not the tools field on its own.

Second round of narrowing was on tool names, same synthetic schema, varying only the name:

browser_back      → 200
mcp_browser_back  → 400
foo_back          → 200
mcp_foo_back      → 400
bash_tool         → 200
mcp_bash          → 400

Any mcp_* name trips the billing-route classifier regardless of what the tool actually does. There's a step in build_anthropic_kwargs that was prefixing everything (# 3. Prefix tool names with mcp_ (Claude Code convention)). Dropping that inside the if is_oauth: branch was safe for us because the complementary strip_tool_prefix pass on the response side becomes a no-op when there is no prefix to strip.

That cleared most of the 400s. With the full real tool set (prefix-free) we still got it occasionally. Another binary search on subsets of the list:

first 15 tools → 200
last 15 tools  → 400

But testing each of the last-15 individually all returned 200. So it's a combination trigger, not a single bad tool. After more narrowing we found two independent ones:

  • session_search: rejects by name alone once it's in a set with anything else.
  • skill_manage + skill_view + skills_list together: any two of the three are fine, all three always 400.

Worked around with a wire-level rename in the OAuth branch, plus a reverse map in the function-call dispatcher so the internal registry keeps its real names. Rough shape of the patch.

agent/anthropic_adapter.py:

# Tool-name renames for Anthropic's Claude Code billing route.
# session_search trips by name alone; the skill_manage/skill_view/skills_list
# triple trips as a combination. Rename on the wire; reverse the mapping in
# model_tools.handle_function_call so the registry keeps its real names.
_TOOL_NAME_RENAMES: Dict[str, str] = {
    "session_search": "recall_sessions",
    "skills_list":    "list_capabilities",
}
_TOOL_NAME_RENAME_REVERSE: Dict[str, str] = {v: k for k, v in _TOOL_NAME_RENAMES.items()}

Applied inside build_anthropic_kwargs, still inside if is_oauth:, after the overflow handler from this PR:

# Drop the mcp_ prefix for OAuth: it trips Anthropic's CC classifier.
# The `strip_tool_prefix` pass in normalize_anthropic_response is a no-op
# when there's no prefix to strip, so response handling still works.

# Rename tools whose names (or combinations) trigger the 400 "extra usage".
if anthropic_tools:
    for tool in anthropic_tools:
        nm = tool.get("name")
        if isinstance(nm, str) and nm in _TOOL_NAME_RENAMES:
            tool["name"] = _TOOL_NAME_RENAMES[nm]

# Message-history rewrite: if the model previously called the original name,
# the tool_use block in history now needs the renamed form to match the tool
# we're about to declare.
for msg in anthropic_messages:
    content = msg.get("content")
    if isinstance(content, list):
        for block in content:
            if isinstance(block, dict) and block.get("type") == "tool_use":
                nm = block.get("name")
                if isinstance(nm, str) and nm in _TOOL_NAME_RENAMES:
                    block["name"] = _TOOL_NAME_RENAMES[nm]

Reverse side, at the top of handle_function_call in model_tools.py:

def handle_function_call(function_name, function_args, ...):
    # Reverse the rename applied for Anthropic's CC billing route.
    try:
        from agent.anthropic_adapter import _TOOL_NAME_RENAME_REVERSE
        if function_name in _TOOL_NAME_RENAME_REVERSE:
            function_name = _TOOL_NAME_RENAME_REVERSE[function_name]
    except Exception:
        pass
    # ... existing body

Not pretty, but stable across restarts and doesn't touch any non-OAuth path.

Size didn't turn out to be the discriminator for us. 30 synthetic minimal tools at ~4KB returned 200, while 15 real tools at ~12KB with the "wrong" names failed. Reads like the billing route is fingerprinting tool names against a Claude Code allowlist and classifying anything outside it as third-party.

Short version for anyone who lands here after merging this PR and still sees the error:

  1. Strip any mcp_ prefix on tool names in the OAuth branch.
  2. If you ship session_search or the skill_manage/skill_view/skills_list trio, rename them on the wire.

Thanks for the overflow fix, covers the biggest class of this bug. The above just adds a couple more paths to the same error message.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating a concrete OAuth failure mode. The requested relocation is not present on current main: build_anthropic_kwargs still retains the existing system prompt after the Claude Code identity block (agent/anthropic_adapter.py:2533-2538).

Problems

  • The added [CONTEXT] reconstruction drops cache metadata. Current conversion intentionally retains system blocks when they carry cache_control (agent/anthropic_adapter.py:2421-2427); rebuilding only text into a plain user message would discard that marker rather than move the cached prefix safely.
  • The diff changes only agent/anthropic_adapter.py and adds no tests for OAuth isolation, role ordering, or cache behavior.
  • The April 22 follow-up on this PR reports that relocation did not resolve full tool-enabled requests; current main separately normalizes OAuth tool names at agent/anthropic_adapter.py:2551-2590.

Suggested changes

  • If maintainers choose this direction, preserve cache-control on a preamble attached to the first real user message and add regression coverage for cache-breakpoint limits and non-OAuth pass-through.
  • Please obtain maintainer direction first: the member closure on fix(anthropic-oauth): bypass Anthropic's spoof filter for Claude subscription auth #26430 documents a policy objection to mitigations that evade the provider's usage-attribution filter.

This is an automated hermes-sweeper review.

@alt-glitch alt-glitch added the P2 Medium — degraded but workaround exists label Jul 12, 2026
@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
@alt-glitch alt-glitch removed the sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades label Jul 12, 2026

@GottZ GottZ left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Summary

Two PRs address the reported OAuth 400 by moving system-prompt overflow into a synthetic user/assistant preamble; their diffs are identical. Follow-up evidence limits the proposed fix: one contributor isolated remaining failures to requests containing tools, while a non-contributor independently reported only that the 400 persisted, without establishing whether tools were involved.

Related pull requests

  • #13609 [closed] duplicate — (+25/-0) — superseded duplicate: Moves additional OAuth system blocks into a [CONTEXT] user prefix, but reconstructs only text and therefore does not preserve block metadata such as cache_control. It remains relevant as the closed predecessor explicitly replaced by #13611 after a commit-metadata cleanup.
  • #13611 related — (+25/-0) — keep open with a salvage path: The diff is identical to #13609 and may address one system-field-triggered failure mode, but it drops cache_control, adds no regression coverage, and does not solve every reported 400; specifically, a contributor isolated a remaining tool-enabled failure, while another report established only a continuing 400. This agrees with the MAINTAINER-BOT keep_open review, whose salvage path requires metadata-safe preamble handling, OAuth isolation and cache/ordering tests, non-OAuth pass-through coverage, and maintainer direction on the documented provider-policy objection.

Duplicates

#13609 and #13611 contain the same change; #13611 supersedes the closed #13609, so #13609 is the duplicate predecessor.

Suggested consolidation

Keep #13611 open with a salvage path rather than treating the current patch as complete: preserve cache_control when relocating the preamble, attach it without unsafe synthetic role ordering, add regression tests for OAuth isolation, cache-breakpoint limits, role ordering, and non-OAuth pass-through, and obtain maintainer direction on the provider usage-attribution policy objection. Keep #13609 closed as the superseded duplicate of #13611.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    subgraph Dup13609 ["PRs duplicating each other"]
        P13609["PR #13609 (closed)"]
        P13611["PR #13611 (open)"]
    end
    class P13609 closed
    class P13611 open
    class P13611 target
    click P13609 "https://github.com/NousResearch/hermes-agent/pull/13609"
    click P13611 "https://github.com/NousResearch/hermes-agent/pull/13611"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 4 kB of PR diffs, 4 kB of issue/PR text, 7 kB of discussion (6 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@alt-glitch alt-glitch added the needs-decision Awaiting maintainer decision before any implementation label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants