feat(v1.1.0): PascalCase mcp_ tools + Claude Code 2.1.112 fingerprint

Anthropic tightened OAuth validation between 2026-04-14 and 2026-04-22.
Requests are now classified as third-party and routed to extra-usage
credits (or rejected with 'Third-party apps now draw from your extra
usage, not your plan limits') unless the fingerprint matches real
Claude Code. Ports the two upstream opencode-claude-auth fixes that
address this:

- PR #191 (merged, v1.4.10): PascalCase `mcp_` tool names. Hermes
  prefixes tools with lowercase `mcp_bash`; the validator now flags
  that as non-Claude-Code. Real CC uses `mcp_Bash` / `mcp_Read` /
  `mcp_Background_output`. Adds a request-side rewrite hook plus a
  response-side unhook that lowercases the first char after hermes
  strips the prefix, so the tool dispatcher continues to receive the
  original lowercase names.

- PR #207 (open, attempted fix for Claude Code 2.1.112): updates the
  billing fingerprint. Changes the `cc_entrypoint` in the signed
  billing header from `cli` to `sdk-cli`, adds the
  `advisor-tool-2026-03-01` beta flag, injects the Stainless SDK
  headers that real CC sends (`x-stainless-*`) plus
  `anthropic-dangerous-direct-browser-access: true`, and appends
  `?beta=true` to the /v1/messages query string. All non-body signals
  ride the Anthropic Python SDK's per-request `extra_headers` and
  `extra_query` kwargs so we don't need to rewrap the HTTP client.

Other changes:
- Bump __version__ to 1.1.0 and document the rationale above in the
  module docstring.
- Extend `_EXTRA_OAUTH_BETAS` so both prompt-caching and advisor-tool
  betas are appended to hermes's `_OAUTH_ONLY_BETAS` list.
- 10 new unit tests covering PascalCase rewrite (request + response),
  sdk-cli entrypoint, Stainless headers, extra_query=beta:true, and
  preservation of pre-existing extra_headers.

Smoke-tested on Linux with hermes-agent v0.10.0 against claude-sonnet-4-6,
claude-opus-4-6, and claude-opus-4-7 \u2014 all return 200 and complete
responses with the new patch loaded. Billing-tier routing (plan vs
extra-usage) cannot be verified from the client side; this tracks the
best-known upstream bypass as of 2026-04-22 but Anthropic may tighten
validation again at any time.

Refs: #6
Upstream: https://github.com/griffinmartin/opencode-claude-auth/pull/191
Upstream: https://github.com/griffinmartin/opencode-claude-auth/pull/207
This commit is contained in:
Kristian Vastveit 2026-04-22 11:56:25 +02:00
parent 6857168c19
commit b430a8a78f
2 changed files with 398 additions and 13 deletions

View File

@ -3,8 +3,9 @@ Claude Code OAuth bypass for hermes-agent.
========================================== ==========================================
Monkey-patches hermes-agent's ``agent.anthropic_adapter.build_anthropic_kwargs`` Monkey-patches hermes-agent's ``agent.anthropic_adapter.build_anthropic_kwargs``
at import time via a sitecustomize.py hook so that OAuth-authenticated requests and ``normalize_anthropic_response`` at import time via a sitecustomize.py hook
pass Anthropic's server-side content validation. so that OAuth-authenticated requests pass Anthropic's server-side content
validation and still route to the Claude Max/Pro subscription tier.
Background Background
---------- ----------
@ -23,10 +24,37 @@ opencode-claude-auth v1.4.8 (PR #148) worked around it by:
message wrapped in ``<system-reminder>`` blocks. message wrapped in ``<system-reminder>`` blocks.
3. Adding the ``prompt-caching-scope-2026-01-05`` beta flag. 3. Adding the ``prompt-caching-scope-2026-01-05`` beta flag.
Between 2026-04-14 and 2026-04-16, Anthropic tightened the validator further.
Two additional signals matter:
- Tool names are now inspected: real Claude Code uses PascalCase after the
``mcp_`` prefix (``mcp_Bash``, ``mcp_Read``, ``mcp_Background_output``).
Requests with lowercase names (``mcp_bash``) are classified as third-party
and the response says "Third-party apps now draw from your extra usage,
not your plan limits." This was fixed in opencode-claude-auth PR #191.
- The request fingerprint was updated in Claude Code 2.1.112 (upstream PR
#207, currently unmerged): the billing entrypoint changed from ``cli`` to
``sdk-cli``, the ``advisor-tool-2026-03-01`` beta flag was added, the SDK
now sends ``x-stainless-*`` headers and ``anthropic-dangerous-direct-
browser-access: true``, and ``/v1/messages`` is called with ``?beta=true``.
hermes-agent already implements the Claude Code identity prefix, user-agent hermes-agent already implements the Claude Code identity prefix, user-agent
spoofing, ``x-app: cli``, tool name ``mcp_`` prefixing, and the spoofing, ``x-app: cli``, lowercase tool name ``mcp_`` prefixing, HermesClaude
``oauth-2025-04-20`` / ``claude-code-20250219`` beta flags. This patch adds Code product-name scrubbing, dynamic Claude CLI version detection, and the
only the three items above plus a temperature fix for Opus 4.6 adaptive thinking. ``oauth-2025-04-20`` / ``claude-code-20250219`` beta flags.
This patch fills the remaining gaps:
- Signed billing header (system[0]) with the ``sdk-cli`` entrypoint.
- System prompt relocation to first user message.
- ``prompt-caching-scope-2026-01-05`` + ``advisor-tool-2026-03-01`` beta flags.
- PascalCase rewrite of hermes's lowercase ``mcp_`` prefixed tool names in
both the outgoing request and the response normalization path (so the tool
dispatcher continues to receive the original lowercase names).
- Stainless SDK spoof headers + ``anthropic-dangerous-direct-browser-access``
+ ``?beta=true`` query param injected via the Anthropic SDK's per-request
``extra_headers`` / ``extra_query`` kwargs.
- Temperature fix for Opus 4.6 adaptive thinking (HTTP 400 otherwise).
Installation Installation
------------ ------------
@ -45,21 +73,30 @@ site-packages and restart hermes-gateway.
References References
---------- ----------
- https://github.com/griffinmartin/opencode-claude-auth - https://github.com/griffinmartin/opencode-claude-auth
- https://github.com/griffinmartin/opencode-claude-auth/pull/148 - https://github.com/griffinmartin/opencode-claude-auth/pull/148 (billing header)
- https://github.com/griffinmartin/opencode-claude-auth/pull/191 (PascalCase tools)
- https://github.com/griffinmartin/opencode-claude-auth/pull/207 (Claude Code 2.1.112 fingerprint)
Version history Version history
--------------- ---------------
- 1.0.0 (2026-04-09): Initial billing header, system prompt relocation, - 1.0.0 (2026-04-09): Initial billing header, system prompt relocation,
prompt-caching beta flag, aux-client temperature hook for Opus 4.6. prompt-caching beta flag, aux-client temperature hook for Opus 4.6.
- 1.1.0 (2026-04-22): PascalCase ``mcp_`` tool prefix (request + response),
``sdk-cli`` billing entrypoint, ``advisor-tool-2026-03-01`` beta flag,
Stainless SDK spoof headers, ``anthropic-dangerous-direct-browser-access``
header, ``?beta=true`` query param on ``/v1/messages``. Addresses the
"Third-party apps now draw from your extra usage, not your plan limits"
400 error introduced by Anthropic's 2026-04-14+ validator tightening.
""" """
from __future__ import annotations from __future__ import annotations
__version__ = "1.0.0" __version__ = "1.1.0"
import hashlib import hashlib
import inspect import inspect
import logging import logging
import platform
import sys import sys
import traceback import traceback
from typing import Any, Dict, List from typing import Any, Dict, List
@ -74,11 +111,36 @@ logger = logging.getLogger("anthropic_billing_bypass")
# uses this salt to verify billing-header signatures. # uses this salt to verify billing-header signatures.
_BILLING_SALT = "59cf53e54c78" _BILLING_SALT = "59cf53e54c78"
# Billing entrypoint — Claude Code 2.1.112+ reports ``sdk-cli`` instead of the
# legacy ``cli`` value. Anthropic's validator matches this against the
# x-stainless-* headers; a mismatch routes the request to third-party billing.
_BILLING_ENTRYPOINT = "sdk-cli"
# Sentinel strings — entries in system[] starting with these are kept; # Sentinel strings — entries in system[] starting with these are kept;
# everything else is relocated to the first user message. # everything else is relocated to the first user message.
_BILLING_PREFIX = "x-anthropic-billing-header" _BILLING_PREFIX = "x-anthropic-billing-header"
_SYSTEM_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude." _SYSTEM_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
# Tool-name prefix used by hermes-agent's existing OAuth path. We rewrite
# hermes's lowercase ``mcp_foo`` to Claude Code's PascalCase ``mcp_Foo``.
_MCP_PREFIX = "mcp_"
# Stainless SDK version the Anthropic JS SDK reports. Real Claude Code ships
# @anthropic-ai/sdk@0.81.0 as of 2.1.112 — we spoof the same value.
_STAINLESS_PACKAGE_VERSION = "0.81.0"
# Node runtime version Claude Code 2.1.112 runs under. We send a recent LTS
# value rather than our actual Python version (which would give us away).
_STAINLESS_NODE_VERSION = "v22.11.0"
# Additional beta flags the OAuth path needs on top of hermes-agent's built-in
# ``claude-code-20250219`` and ``oauth-2025-04-20``. These are appended to
# ``_OAUTH_ONLY_BETAS`` in ``apply_patches``.
_EXTRA_OAUTH_BETAS = [
"prompt-caching-scope-2026-01-05",
"advisor-tool-2026-03-01",
]
def _extract_first_user_message_text(messages: List[Dict[str, Any]]) -> str: def _extract_first_user_message_text(messages: List[Dict[str, Any]]) -> str:
"""Return the text of the first user message's first text block. """Return the text of the first user message's first text block.
@ -137,6 +199,114 @@ def _build_billing_header_value(
) )
def _stainless_arch() -> str:
machine = (platform.machine() or "").lower()
if machine in ("x86_64", "amd64"):
return "x64"
if machine in ("arm64", "aarch64"):
return "arm64"
if machine in ("i386", "i686"):
return "ia32"
return machine or "unknown"
def _stainless_os() -> str:
mapping = {"Darwin": "MacOS", "Linux": "Linux", "Windows": "Windows"}
return mapping.get(platform.system(), platform.system() or "Unknown")
def _build_spoof_headers() -> Dict[str, str]:
"""Headers real Claude Code 2.1.112 sends that hermes-agent does not.
The Anthropic SDK (Stainless-generated) automatically attaches
``x-stainless-*`` identifying headers. The validator cross-references these
with the billing header's ``cc_entrypoint``; absent or mismatched values
flag the request as third-party. ``anthropic-dangerous-direct-browser-
access: true`` is a separate Claude Code CLI behavior.
"""
return {
"anthropic-dangerous-direct-browser-access": "true",
"x-stainless-arch": _stainless_arch(),
"x-stainless-lang": "js",
"x-stainless-os": _stainless_os(),
"x-stainless-package-version": _STAINLESS_PACKAGE_VERSION,
"x-stainless-retry-count": "0",
"x-stainless-runtime": "node",
"x-stainless-runtime-version": _STAINLESS_NODE_VERSION,
"x-stainless-timeout": "600",
}
def _pascalcase_mcp_name(name: str) -> str:
"""Rewrite ``mcp_foo_bar`` → ``mcp_Foo_bar``.
Matches opencode-claude-auth PR #191 exactly: only the character
immediately following the ``mcp_`` prefix is uppercased. Names already in
PascalCase are returned unchanged.
"""
if not isinstance(name, str) or not name.startswith(_MCP_PREFIX):
return name
rest = name[len(_MCP_PREFIX):]
if not rest or not rest[0].islower():
return name
return _MCP_PREFIX + rest[0].upper() + rest[1:]
def _rewrite_tool_names_pascalcase(api_kwargs: Dict[str, Any]) -> None:
"""Convert hermes-agent's lowercase ``mcp_`` tool names to PascalCase.
Hermes prefixes tools with ``mcp_`` at line 1365 of ``anthropic_adapter``
using the literal ``_MCP_TOOL_PREFIX + tool["name"]``, which produces
lowercase values like ``mcp_bash``. Anthropic's billing validator
blacklists specific lowercase names (e.g. ``background_output``), flagging
the client as non-Claude-Code. Real Claude Code uses PascalCase after the
prefix; we rewrite the request in-place.
"""
tools = api_kwargs.get("tools")
if isinstance(tools, list):
for tool in tools:
if isinstance(tool, dict) and "name" in tool:
tool["name"] = _pascalcase_mcp_name(tool.get("name") or "")
messages = api_kwargs.get("messages")
if isinstance(messages, list):
for msg in messages:
if not isinstance(msg, dict):
continue
content = msg.get("content")
if not isinstance(content, list):
continue
for block in content:
if not isinstance(block, dict):
continue
if block.get("type") == "tool_use" and "name" in block:
block["name"] = _pascalcase_mcp_name(block.get("name") or "")
def _merge_spoof_extras(api_kwargs: Dict[str, Any]) -> None:
"""Inject Claude Code 2.1.112 request fingerprint via extra_headers / extra_query.
The Anthropic Python SDK forwards both to the underlying HTTP request:
``extra_headers`` becomes request headers (merged with client defaults),
``extra_query`` becomes URL query parameters. We avoid overwriting values
already set by hermes-agent (e.g. its fast-mode ``anthropic-beta`` header)
so our spoof is additive.
"""
existing_headers = api_kwargs.get("extra_headers")
merged_headers: Dict[str, str] = dict(_build_spoof_headers())
if isinstance(existing_headers, dict):
for key, value in existing_headers.items():
merged_headers[key] = value
api_kwargs["extra_headers"] = merged_headers
existing_query = api_kwargs.get("extra_query")
merged_query: Dict[str, str] = {"beta": "true"}
if isinstance(existing_query, dict):
for key, value in existing_query.items():
merged_query[key] = value
api_kwargs["extra_query"] = merged_query
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Bypass logic (ported from opencode-claude-auth/src/transforms.ts) # Bypass logic (ported from opencode-claude-auth/src/transforms.ts)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -243,7 +413,9 @@ def apply_claude_code_bypass(api_kwargs: Dict[str, Any], version: str) -> None:
# Compute billing header using ORIGINAL messages (before relocation). # Compute billing header using ORIGINAL messages (before relocation).
try: try:
billing_value = _build_billing_header_value(messages, version, "cli") billing_value = _build_billing_header_value(
messages, version, _BILLING_ENTRYPOINT
)
except Exception as exc: except Exception as exc:
logger.warning("Failed to build billing header: %s", exc) logger.warning("Failed to build billing header: %s", exc)
return return
@ -287,6 +459,8 @@ def apply_claude_code_bypass(api_kwargs: Dict[str, Any], version: str) -> None:
if moved_texts: if moved_texts:
_prepend_to_first_user_message(messages, moved_texts) _prepend_to_first_user_message(messages, moved_texts)
_rewrite_tool_names_pascalcase(api_kwargs)
_merge_spoof_extras(api_kwargs)
_fix_temperature_for_oauth_adaptive(api_kwargs, site="build_kwargs") _fix_temperature_for_oauth_adaptive(api_kwargs, site="build_kwargs")
@ -311,6 +485,71 @@ def _get_version_safely(aa_module: Any) -> str:
return "2.1.90" return "2.1.90"
def _lowercase_first(name: str) -> str:
if not name:
return name
return name[0].lower() + name[1:]
def _install_response_pascalcase_unhook(aa_module: Any, force: bool = False) -> bool:
"""Post-process ``normalize_anthropic_response`` to restore lowercase tool names.
We rewrote outgoing tool names from ``mcp_bash`` to ``mcp_Bash`` to pass
Anthropic's validator. The response comes back referencing ``mcp_Bash``
too. Hermes strips the ``mcp_`` prefix (line 1488-1489 of
``anthropic_adapter``), leaving ``Bash`` which hermes's tool dispatcher
cannot find because the registered name is ``bash``. We wrap
``normalize_anthropic_response`` to lowercase the first character of each
tool call name after hermes's strip runs.
"""
if getattr(aa_module, "_CLAUDE_CODE_RESPONSE_UNHOOK_APPLIED", False) and not force:
logger.debug("response PascalCase unhook already installed")
return True
original = getattr(aa_module, "normalize_anthropic_response", None)
if not callable(original):
logger.warning("normalize_anthropic_response not found; skipping response unhook")
return False
def patched_normalize(response: Any, strip_tool_prefix: bool = False, **kwargs: Any) -> Any:
result = original(response, strip_tool_prefix=strip_tool_prefix, **kwargs)
if not strip_tool_prefix:
return result
try:
assistant_message, _finish = result
except (TypeError, ValueError):
return result
tool_calls = getattr(assistant_message, "tool_calls", None)
if not tool_calls:
return result
for tc in tool_calls:
fn = getattr(tc, "function", None)
if fn is None:
continue
name = getattr(fn, "name", None)
if isinstance(name, str) and name and name[0].isupper():
try:
fn.name = _lowercase_first(name)
except Exception:
pass
return result
patched_normalize.__name__ = original.__name__
patched_normalize.__qualname__ = getattr(
original, "__qualname__", original.__name__
)
patched_normalize.__doc__ = original.__doc__
patched_normalize.__wrapped__ = original # type: ignore[attr-defined]
aa_module.normalize_anthropic_response = patched_normalize
aa_module._CLAUDE_CODE_RESPONSE_UNHOOK_APPLIED = True # type: ignore[attr-defined]
logger.info("Response PascalCase unhook installed on normalize_anthropic_response")
sys.stderr.write(
"[anthropic_billing_bypass] Response PascalCase unhook installed\n"
)
return True
def _install_aux_client_hook(force: bool = False) -> bool: def _install_aux_client_hook(force: bool = False) -> bool:
"""Patch the auxiliary client to strip temperature on OAuth adaptive models.""" """Patch the auxiliary client to strip temperature on OAuth adaptive models."""
try: try:
@ -426,12 +665,13 @@ def apply_patches(anthropic_adapter_module: Any = None) -> bool:
logger.debug("Claude Code bypass already installed") logger.debug("Claude Code bypass already installed")
return True return True
# 1. Add the missing beta flag. # 1. Add the missing beta flags (prompt-caching + advisor-tool).
new_beta = "prompt-caching-scope-2026-01-05"
oauth_betas = getattr(aa, "_OAUTH_ONLY_BETAS", None) oauth_betas = getattr(aa, "_OAUTH_ONLY_BETAS", None)
if isinstance(oauth_betas, list) and new_beta not in oauth_betas: if isinstance(oauth_betas, list):
oauth_betas.append(new_beta) for new_beta in _EXTRA_OAUTH_BETAS:
logger.info("Appended beta flag: %s", new_beta) if new_beta not in oauth_betas:
oauth_betas.append(new_beta)
logger.info("Appended beta flag: %s", new_beta)
# 2. Verify the target function exists with the expected signature. # 2. Verify the target function exists with the expected signature.
original_build = getattr(aa, "build_anthropic_kwargs", None) original_build = getattr(aa, "build_anthropic_kwargs", None)
@ -492,6 +732,7 @@ def apply_patches(anthropic_adapter_module: Any = None) -> bool:
logger.info("Claude Code OAuth bypass installed (build_anthropic_kwargs)") logger.info("Claude Code OAuth bypass installed (build_anthropic_kwargs)")
sys.stderr.write("[anthropic_billing_bypass] Claude Code OAuth bypass installed\n") sys.stderr.write("[anthropic_billing_bypass] Claude Code OAuth bypass installed\n")
_install_response_pascalcase_unhook(aa)
_install_aux_client_hook() _install_aux_client_hook()
return True return True

View File

@ -1,10 +1,14 @@
# pyright: reportPrivateUsage=false, reportUnknownParameterType=false, reportMissingParameterType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportArgumentType=false # pyright: reportPrivateUsage=false, reportUnknownParameterType=false, reportMissingParameterType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportArgumentType=false
import copy import copy
from types import SimpleNamespace
from anthropic_billing_bypass import ( from anthropic_billing_bypass import (
_BILLING_ENTRYPOINT,
_SYSTEM_IDENTITY, _SYSTEM_IDENTITY,
_fix_temperature_for_oauth_adaptive, _fix_temperature_for_oauth_adaptive,
_install_response_pascalcase_unhook,
_pascalcase_mcp_name,
apply_claude_code_bypass, apply_claude_code_bypass,
) )
@ -19,6 +23,14 @@ def test_apply_claude_code_bypass_injects_billing_header_and_preserves_identity(
assert system[1]["text"] == _SYSTEM_IDENTITY assert system[1]["text"] == _SYSTEM_IDENTITY
def test_apply_claude_code_bypass_uses_sdk_cli_entrypoint(basic_api_kwargs):
apply_claude_code_bypass(basic_api_kwargs, "2.1.112")
billing_text = basic_api_kwargs["system"][0]["text"]
assert "cc_entrypoint=sdk-cli;" in billing_text
assert _BILLING_ENTRYPOINT == "sdk-cli"
def test_apply_claude_code_bypass_relocates_non_identity_system_text_to_first_user_message( def test_apply_claude_code_bypass_relocates_non_identity_system_text_to_first_user_message(
basic_api_kwargs, basic_api_kwargs,
): ):
@ -93,3 +105,135 @@ def test_fix_temperature_for_oauth_adaptive_without_temperature_is_noop():
api_kwargs = {"model": "claude-opus-4-6-20260101"} api_kwargs = {"model": "claude-opus-4-6-20260101"}
_fix_temperature_for_oauth_adaptive(api_kwargs, site="test") _fix_temperature_for_oauth_adaptive(api_kwargs, site="test")
assert api_kwargs == {"model": "claude-opus-4-6-20260101"} assert api_kwargs == {"model": "claude-opus-4-6-20260101"}
def test_pascalcase_mcp_name_uppercases_first_char_after_prefix():
assert _pascalcase_mcp_name("mcp_bash") == "mcp_Bash"
assert _pascalcase_mcp_name("mcp_read") == "mcp_Read"
assert _pascalcase_mcp_name("mcp_background_output") == "mcp_Background_output"
def test_pascalcase_mcp_name_leaves_already_pascalcase_unchanged():
assert _pascalcase_mcp_name("mcp_Bash") == "mcp_Bash"
assert _pascalcase_mcp_name("mcp_Background_output") == "mcp_Background_output"
def test_pascalcase_mcp_name_ignores_unprefixed_names():
assert _pascalcase_mcp_name("bash") == "bash"
assert _pascalcase_mcp_name("not_mcp_bash") == "not_mcp_bash"
assert _pascalcase_mcp_name("") == ""
def test_apply_claude_code_bypass_rewrites_tool_names_to_pascalcase(basic_api_kwargs):
basic_api_kwargs["tools"] = [
{"name": "mcp_bash"},
{"name": "mcp_background_output"},
{"name": "mcp_Already_pascal"},
]
basic_api_kwargs["messages"] = [
{
"role": "user",
"content": [
{"type": "text", "text": "hello world"},
{"type": "tool_use", "name": "mcp_bash", "id": "tool_1", "input": {}},
],
}
]
apply_claude_code_bypass(basic_api_kwargs, "2.1.112")
tool_names = [tool["name"] for tool in basic_api_kwargs["tools"]]
assert tool_names == ["mcp_Bash", "mcp_Background_output", "mcp_Already_pascal"]
tool_use_block = basic_api_kwargs["messages"][0]["content"][-1]
assert tool_use_block["name"] == "mcp_Bash"
def test_apply_claude_code_bypass_injects_stainless_and_direct_browser_headers(
basic_api_kwargs,
):
apply_claude_code_bypass(basic_api_kwargs, "2.1.112")
extra_headers = basic_api_kwargs["extra_headers"]
assert extra_headers["anthropic-dangerous-direct-browser-access"] == "true"
assert extra_headers["x-stainless-lang"] == "js"
assert extra_headers["x-stainless-runtime"] == "node"
assert extra_headers["x-stainless-package-version"] == "0.81.0"
assert extra_headers["x-stainless-retry-count"] == "0"
assert extra_headers["x-stainless-timeout"] == "600"
assert extra_headers["x-stainless-os"] in ("MacOS", "Linux", "Windows")
assert extra_headers["x-stainless-arch"] in ("x64", "arm64", "ia32", "unknown")
def test_apply_claude_code_bypass_sets_beta_true_query_param(basic_api_kwargs):
apply_claude_code_bypass(basic_api_kwargs, "2.1.112")
assert basic_api_kwargs["extra_query"] == {"beta": "true"}
def test_apply_claude_code_bypass_preserves_existing_extra_headers(basic_api_kwargs):
basic_api_kwargs["extra_headers"] = {"anthropic-beta": "fast-mode-2026-02-01"}
apply_claude_code_bypass(basic_api_kwargs, "2.1.112")
assert basic_api_kwargs["extra_headers"]["anthropic-beta"] == "fast-mode-2026-02-01"
assert (
basic_api_kwargs["extra_headers"]["anthropic-dangerous-direct-browser-access"]
== "true"
)
def _make_fake_adapter_module(tool_names):
def original_normalize(response, strip_tool_prefix=False):
tool_calls = []
for name in tool_names:
stripped = name[len("mcp_"):] if strip_tool_prefix and name.startswith("mcp_") else name
tool_calls.append(
SimpleNamespace(
id="tool_1",
type="function",
function=SimpleNamespace(name=stripped, arguments="{}"),
)
)
msg = SimpleNamespace(content=None, tool_calls=tool_calls or None, reasoning=None)
return msg, "tool_calls"
module = SimpleNamespace(normalize_anthropic_response=original_normalize)
return module
def test_response_unhook_lowercases_first_char_of_tool_names_after_strip():
adapter = _make_fake_adapter_module(["mcp_Bash", "mcp_Background_output"])
assert _install_response_pascalcase_unhook(adapter) is True
msg, _reason = adapter.normalize_anthropic_response(
response=object(), strip_tool_prefix=True
)
names = [tc.function.name for tc in msg.tool_calls]
assert names == ["bash", "background_output"]
def test_response_unhook_is_noop_when_strip_tool_prefix_false():
adapter = _make_fake_adapter_module(["mcp_Bash"])
_install_response_pascalcase_unhook(adapter)
msg, _reason = adapter.normalize_anthropic_response(
response=object(), strip_tool_prefix=False
)
assert msg.tool_calls[0].function.name == "mcp_Bash"
def test_response_unhook_is_idempotent():
adapter = _make_fake_adapter_module(["mcp_Bash"])
assert _install_response_pascalcase_unhook(adapter) is True
assert _install_response_pascalcase_unhook(adapter) is True
msg, _reason = adapter.normalize_anthropic_response(
response=object(), strip_tool_prefix=True
)
assert msg.tool_calls[0].function.name == "bash"