fix(anthropic): alias session_search/memory OAuth billing-classifier triggers (fixes #65365, #82154) - #78025
Conversation
…triggers Anthropic subscription OAuth (claude_code credential) misroutes Hermes sessions carrying the session_search or memory toolset into the extra-usage lane, surfacing as HTTP 400 "You're out of extra usage" on a valid subscription. Live-verified via the anthropic-ratelimit-unified-representative-claim response header (deterministic lane oracle, no dependency on the laggy usage counter): tool schemas are innocent, the trigger is three specific system-prompt sentences (session_search recall + two skill_manage sentences), required jointly — breaking any one clears the classifier. Two independent layers: 1. OAuth wire alias (anthropic_adapter.py, transports/anthropic.py): session_search -> chat_history_lookup, memory -> context_notes in tool name, description, and (session_search only) system-prompt prose, with wire-collision guarding and a normalize_response reverse-map that keeps NousResearchGH-25255 registered-tool precedence. Also routes named tool_choice through the same normalizer, closing a gap where a forced tool_choice would leak the raw trigger string and stop matching tools[]. 2. Prompt-preserving reword (prompt_builder.py): rewords the two triggering SKILLS_GUIDANCE sentences while keeping the same meaning, still naming skill_manage, and leaving the Skill Safety Rule section untouched. Applies to all auth paths since it's a prompt-copy change, not a wire-level transform. Two layers rather than one because the three-sentence AND-condition means a classifier tightening could start firing on either remaining leg alone. Fixes NousResearch#65365
Also fixes #82154Independent bisection in #82154 landed on the same root cause with a narrower reproduction: the first sentence alone of The
Net effect: merging this PR closes both the 3-sentence combined trigger (#65365) and the single-sentence trigger (#82154) with one reworded prompt block, so no separate PR is needed for #82154 — opening a second one would just re-touch the same constant with a subset of this fix. Flagging the two secondary observations from #82154 that are genuinely out of scope here and worth separate follow-ups:
@Cloud-Ops-Dev — could you confirm on #82154 whether the reworded prose in this PR clears your live bisection harness too? Happy to adjust wording further if it doesn't. |
Fixes #65365
Summary
On Anthropic subscription OAuth (
claude_codecredential), Hermes sessions carrying thesession_searchormemorytoolset are misrouted by Anthropic's OAuth billing classifier into the (usually empty) extra-usage lane, surfacing asHTTP 400 "You're out of extra usage"on a valid subscription — or silently billing the metered lane on accounts where extra usage is enabled.Related to #76807 (open) — that PR aliases the
session_search/memoryschema names, descriptions, and prose. This PR implements the same class of fix independently, and additionally closes a gap the repo's own triage bot flagged in #76807: namedtool_choiceis not routed through the alias, so a forcedtool_choice="session_search"would (a) still leak the literal trigger string onto the wire and (b) reference a tool name that no longer matches any entry intools[]once the schema itself is aliased — a guaranteed 400 on top of the billing misroute. Happy to consolidate with #76807 if the maintainers prefer — opening this as a complete, independently-tested alternative so there's a mergeable option either way.Root cause (live-verified, not speculative)
Per the issue thread's own deterministic A/B repros (replayed byte-exact request bodies against
/v1/messageswith the real OAuth client, read viaanthropic-ratelimit-unified-representative-claim/-overage-in-useresponse headers as a lane oracle — no dependency on the laggy usage counter):session_search/memoryschemas succeeds when the system prompt is minimal.SESSION_SEARCH_GUIDANCErecall sentence, and twoSKILLS_GUIDANCEsentences ("save the approach as a skill withskill_manage" / "patch it immediately withskill_manage(action='patch')"). Breaking any one of the three clears the classifier.1ecfe6867) — Anthropic's classifier changed, not Hermes. Per one reporter: "any fix here is chasing a moving server-side target, and other prose could start tripping it next."Fix — two independent layers
1. OAuth wire alias (
agent/anthropic_adapter.py,agent/transports/anthropic.py) — breaks thesession_searchleg:session_search→chat_history_lookup,memory→context_noteson the OAuth wire only (tool name + description).session_searchalso aliased in system-prompt prose via word-boundary regex (memoryis deliberately not prose-aliased — it's ordinary English throughout the prompt and inside the memory tool's own parameter docs; rewriting it there would corrupt guidance the model must follow verbatim).tool_choicegap (new vs. fix(anthropic): alias the session_search and memory schemas on the OAuth wire #76807): a forced tool choice is now routed through the same normalizer astools[], so it always matches the corresponding entry instead of leaking the raw trigger string or a stale name.normalize_responsereverse-maps the alias last, after the existing [Bug]: Anthropic OAuth stripsmcp_prefix from Hermes-native MCP tool names, breaking registry lookup #25255 registry lookups, so a genuinely registered tool under that wire name always wins.2. Prompt-preserving reword (
agent/prompt_builder.py) — breaks theskill_manageleg, as defense-in-depth against the classifier's demonstrated instability:SKILLS_GUIDANCEsentences while keeping the same meaning, still namingskill_manage, and leaving the## Skill Safety Rulesection byte-identical (existingtest_ghost_skill_pruning.pyassertions cover that section and still pass unmodified).Two independent layers rather than one because the three-sentence AND-condition means a future classifier tightening could start firing on either remaining leg alone; breaking both removes the fragility instead of chasing the classifier one leg at a time.
Test plan
tests/agent/test_anthropic_mcp_prefix_strip.py: name/description/prose aliasing, word-boundary safety (session_search_tool.pypath survives), tool_choice alias + non-aliased-name prefix, wire-collision avoidance, API-key passthrough, response-side round-trip (both directions), registered-tool-wins precedencetests/agent/test_ghost_skill_pruning.py: trigger phrasing removed,skill_managestill named, Skill Safety Rule untouchedtests/agent/full suite: no regressionstests/agent/transports/: 228 passed, no regressions%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#00f0ff', 'mainBkg': '#0a0a16', 'primaryTextColor': '#ffffff', 'primaryBorderColor': '#ff007f', 'lineColor': '#00f0ff'}}}%% graph TD A[🔒 OAuth Request Built] -->|3-Sentence AND-Trigger| B{Classifier Fingerprint?} B -->|session_search prose + name| C[⚡ Wire Alias: chat_history_lookup] B -->|skill_manage x2 prose| D[⚡ Prompt Reword: Layer 2] C --> E[🧬 tool_choice Routed Through Same Alias] D --> F[🛰️ Skill Safety Rule Untouched] E --> G[🚀 Subscription Lane: five_hour] F --> GInfographic :