fix(anthropic): keep OAuth requests on included subscription billing - #72173
fix(anthropic): keep OAuth requests on included subscription billing#72173mguttmann wants to merge 2 commits into
Conversation
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
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.
|
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 ( Problems
Suggested changes
Automated hermes-sweeper review. |
SummaryTwenty-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 consolidationKeep #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 graphflowchart 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"
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. |
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: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:…while claude.ai shows the subscription barely used. Full analysis, bisection table and impact assessment in #72171.
Key evidence — same length, opposite outcome:
400 out of extra usageNot 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.
Two files, +320 lines, no deletions:
agent/anthropic_adapter.py—_resolve_oauth_system_budget()+ step 2b inbuild_anthropic_kwargs()tests/agent/test_anthropic_oauth_system_budget.py— new suiteDesign notes
config.yaml, not an env var. This is behavioral configuration; perAGENTS.md.envis for secrets only. My earlier attempt (fix(anthropic): keep subscription OAuth requests on included billing + auxiliary OAuth fallback #53213) was correctly closed for introducingHERMES_OAUTH_SYSTEM_BUDGET— this follows the documented path viaload_config_readonly()(the same read-only fast pathget_provider_request_timeoutuses, so no per-turn cost).oauth_system_budget_chars: 0restores current behavior byte for byte.cache_controlis 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.rfind("\n")before the budget, hard cut only if that would drop more than half), so instructions are not truncated mid-sentence.Testing
assert 16800 <= 3000— verified by stashing the adapter change and re-running, so the suite is not a no-op).test_anthropic_adapter.py,test_anthropic_mcp_prefix_strip.py,test_auxiliary_client.py: 572 passed.TestRunOauthSetupTokenare present identically on unmodifiedmain(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-115currently 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.