Skip to content

fix(anthropic): keep OAuth requests on included subscription billing - #72173

Open
mguttmann wants to merge 2 commits into
NousResearch:mainfrom
mguttmann:fix/anthropic-oauth-system-budget
Open

fix(anthropic): keep OAuth requests on included subscription billing#72173
mguttmann wants to merge 2 commits into
NousResearch:mainfrom
mguttmann:fix/anthropic-oauth-system-budget

Conversation

@mguttmann

Copy link
Copy Markdown
Contributor

Fixes #72171
Addresses #65564

Problem

Anthropic's OAuth billing classifier inspects the request payload and decides from its shape which billing lane applies. Hermes already handles two facets of this in build_anthropic_kwargs():

  • step 1 — prepend the Claude Code identity block

  • step 3 — normalize mcp_mcp__ tool names, with this comment already in the tree:

    […] treats a single-underscore mcp_ tool name as a third-party-app fingerprint and rejects the request with HTTP 400 "Third-party apps now draw from extra usage, not plan limits" (verified empirically […])

The system prompt body is the unguarded third facet. Everything Hermes injects (skill_manage, session_search, Computer Use guidance, delegation rules, mid-turn steering, …) goes out verbatim — 17,000+ chars on a real deployment. The classifier fingerprints that as raw API usage and routes the request to the pay-per-token "extra usage" pool instead of the subscription's included quota, so users see:

HTTP 400  You're out of extra usage. Add more at claude.ai/settings/usage
HTTP 429  monthly spend limit

…while claude.ai shows the subscription barely used. Full analysis, bisection table and impact assessment in #72171.

Key evidence — same length, opposite outcome:

System prompt Length Result
identity + generic filler ~17,000 ✅ subscription
identity + real Hermes instructions ~17,000 400 out of extra usage
identity + Hermes instructions, trimmed ~3,000 ✅ subscription

Not a token-count limit — content fingerprinting.

Change

Extends the existing "make the OAuth payload Claude-Code-shaped" logic to the system prompt body: cap the Hermes-specific portion; the Claude Code identity block (index 0) is never touched.

providers:
  anthropic:
    oauth_system_budget_chars: 3000   # 0 disables

Two files, +320 lines, no deletions:

  • agent/anthropic_adapter.py_resolve_oauth_system_budget() + step 2b in build_anthropic_kwargs()
  • tests/agent/test_anthropic_oauth_system_budget.py — new suite

Design notes

  • config.yaml, not an env var. This is behavioral configuration; per AGENTS.md .env is for secrets only. My earlier attempt (fix(anthropic): keep subscription OAuth requests on included billing + auxiliary OAuth fallback #53213) was correctly closed for introducing HERMES_OAUTH_SYSTEM_BUDGET — this follows the documented path via load_config_readonly() (the same read-only fast path get_provider_request_timeout uses, so no per-turn cost).
  • Opt-out preserved. oauth_system_budget_chars: 0 restores current behavior byte for byte.
  • API-key requests untouched. They bill per token and legitimately keep the full prompt — covered by a test.
  • Prompt caching preserved. A trimmed-to-empty block must not stay on the wire: Anthropic rejects empty text blocks, and an empty block carrying cache_control is always invalid (cache_control cannot be set for empty text blocks). If a dropped block held the cache marker it migrates to the last surviving text block — otherwise every request would silently lose its cached prefix. Two dedicated tests guard this.
  • Cut on a line boundary (rfind("\n") before the budget, hard cut only if that would drop more than half), so instructions are not truncated mid-sentence.
  • Never raises. A malformed or unreadable config falls back to the default rather than breaking request building.

Testing

tests/agent/test_anthropic_oauth_system_budget.py ..............  14 passed
  • 14/14 pass with the fix; 7 fail without it (assert 16800 <= 3000 — verified by stashing the adapter change and re-running, so the suite is not a no-op).
  • Neighbouring suites — test_anthropic_adapter.py, test_anthropic_mcp_prefix_strip.py, test_auxiliary_client.py: 572 passed.
  • 3 pre-existing failures in TestRunOauthSetupToken are present identically on unmodified main (verified by stash/compare) and are unrelated to this change.

Coverage: config resolution (set / unset / zero / malformed / unreadable), trimming (long / short / custom budget / identity preserved / opt-out), wire safety (no empty block, no empty block with cache_control, line-boundary cut), and the non-OAuth path.

Scope / trade-off

This is a mitigation, not a cure. It trims instructions the agent would otherwise receive. The cure would be Anthropic classifying subscription clients by credential rather than payload shape, or a first-class subscription-OAuth provider (#25267). Until then the practical choice is a slightly shorter system prompt versus an agent that does not work at all on a subscription. In production here (long-running helpdesk deployment) a 3,000-char cap produced no observable behavior regression while the 400/429 failures stopped entirely.

A strictly better follow-up, deliberately not in this PR: relocate the Hermes-specific instructions out of the system prompt into the first user message or a tool result, where the classifier does not appear to look — preserving the full instruction set. Larger change, separate discussion.

Also worth a separate look: website/docs/integrations/providers.md:112-115 currently documents Anthropic OAuth as requiring Max extra-usage credits. Per the bisection in #72171 that describes this bug's symptom rather than an Anthropic policy — happy to send a docs PR if maintainers agree with the diagnosis.

Happy to iterate on the shape of the mitigation, including dropping truncation entirely in favour of relocation, if you prefer a different direction.

Anthropic's OAuth billing classifier inspects the system prompt. A large
block of Hermes-specific instructions (skill_manage, session_search,
Computer Use guidance, mid-turn steering, ...) is fingerprinted as raw API
usage, so the request is billed against the pay-per-token "extra usage"
pool instead of the subscription's included quota. Once that pool is empty
the API returns HTTP 400 "You're out of extra usage" / HTTP 429 "monthly
spend limit" -- while the Claude Pro/Max plan still has headroom.

Established by bisection: generic filler text of identical length passes;
only the Hermes-specific instruction text flips the billing lane. Same
class of problem as the mcp_ -> mcp__ tool-name normalization (NousResearchGH-25255)
already handled a few lines below -- payload shape, not actual usage,
decides how a request is billed.

Trim the Hermes portion of the system prompt on the OAuth wire to a budget
from config.yaml (providers.anthropic.oauth_system_budget_chars, default
3000, 0 restores previous behavior). Per AGENTS.md this is a behavioral
setting, so it lives in config.yaml rather than an env var. The Claude Code
identity block is never touched, and the cut lands on a line boundary so
instructions are not truncated mid-sentence.

Empty text blocks are dropped rather than sent: Anthropic rejects them, and
an empty block carrying cache_control is always invalid ("cache_control
cannot be set for empty text blocks"). A dropped block's cache marker
migrates to the last surviving text block, so prompt caching is preserved.

API-key requests are unaffected -- they bill per token and keep the full
prompt.

Fixes NousResearch#65564
@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/billing Account usage, credit usage, billing (cross-cutting) P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #68839: both address Anthropic OAuth subscription billing classification, but this patch caps Hermes-specific system text while #68839 relocates the full prompt to the first user message. Maintainer selection is needed; neither is marked duplicate.

The contributor-attribution check flagged this PR's commit author email as
unmapped. Add the mapping via scripts/add_contributor.py so the attribution
job resolves the author instead of failing the required-checks gate.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating the OAuth request-shape path. Current main still places the full extracted system prompt beside the Claude Code identity block on OAuth requests (agent/anthropic_adapter.py:2761-2769), so the targeted payload shape remains present.

Problems

  • The new default budget removes all instruction text after the retained prefix. That changes agent behavior for every OAuth user with a system prompt over 3,000 characters, rather than preserving the prompt. The distinct relocation implementation in ab7b4edcc6 keeps the complete prompt while moving it out of system[].
  • The added tests do not cover the production cache-decorated system shape. apply_anthropic_cache_control() creates two cache-marked system text blocks when a stable prefix is available (agent/prompt_caching.py:100-116), but the PR helper supplies only a string system prompt. The cache-marker migration and breakpoint-cap claims therefore remain untested.

Suggested changes

  • Re-scope to a prompt-preserving relocation strategy, or demonstrate why truncation is preferable.
  • Add an end-to-end adapter test through apply_anthropic_cache_control() and assert valid blocks and the four-breakpoint limit.

Automated hermes-sweeper review.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Twenty-two PRs address or reference this Anthropic OAuth extra-usage complex, spanning error classification, endpoint and credential routing, MCP normalization, billing attribution, subprocess transports, system-prompt fingerprints, and retry identity. For #72173, the cause-matched diff caps the bisected OAuth system-prompt content, while narrower aliases preserve more prompt semantics and the remaining PRs address separate causes or recovery layers.

Related pull requests

Duplicates

#6498, #21019, #40020, and #40073 overlap on extra-usage classification and guidance now implemented through #56128; #17681, #28872, and #46687 form the MCP-prefix chain superseded by #47723; #48177, #48202, and #69844 share the billing-marker mechanism; #76669 was salvaged into and superseded by #76807; #72263 overlaps provider-neutral #62008.

Suggested consolidation

Keep #72173 open with a salvage path: replace default prompt truncation with the prompt-preserving relocation identified by its keep_open review, then add an end-to-end test through apply_anthropic_cache_control() covering the production two-block shape, cache-marker migration, and four-breakpoint limit. Keep the closed sanitizer #10576 closed under its contributor policy block, keep #76807 separate as the narrower alias approach, and do not consolidate the policy-blocked billing-marker chain #48177/#48202/#69844 into #72173.

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
    I47260(["issue #47260 (open)"])
    I53212(["issue #53212 (closed)"])
    I65365(["issue #65365 (open)"])
    I65564(["issue #65564 (open)"])
    I72171(["issue #72171 (open)"])
    P72173["PR #72173 (open)"]
    P72173 -->|best fix| I47260
    P72173 -->|best fix| I53212
    P72173 -.->|partial| I65365
    P72173 -->|best fix| I65564
    P72173 -->|best fix| I72171
    class I47260 open
    class I53212 closed
    class I65365 open
    class I65564 open
    class I72171 open
    class P72173 open
    class P72173 best
    class P72173 best
    class P72173 best
    class P72173 best
    class P72173 target
    click I47260 "https://github.com/NousResearch/hermes-agent/issues/47260"
    click I53212 "https://github.com/NousResearch/hermes-agent/issues/53212"
    click I65365 "https://github.com/NousResearch/hermes-agent/issues/65365"
    click I65564 "https://github.com/NousResearch/hermes-agent/issues/65564"
    click I72171 "https://github.com/NousResearch/hermes-agent/issues/72171"
    click P72173 "https://github.com/NousResearch/hermes-agent/pull/72173"
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 22 pull requests and 22 issues in this complex. Each diff was read against this issue; Assessment working set: 244 kB of PR diffs, 253 kB of issue/PR text, 86 kB of discussion (170 comments), 112 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

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

Labels

area/billing Account usage, credit usage, billing (cross-cutting) area/usage-cost Token accounting, usage reporting, billing, cost tracking 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) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic OAuth: system-prompt shape routes subscription requests into the metered "extra usage" lane (root cause + bisection for #65564)

4 participants