feat(v1.5.0): fix PR #11 regressions + port upstream PR #126/#136
PR #11 left literal \n escapes in <system-reminder> blocks (which
break Anthropic's content validator), capitalized X-Stainless-* headers
that diverge from upstream's lowercase JS SDK output, a broken
_pascalcase_mcp_name that ignored the mcp_ prefix, and tests that
referenced removed functions. Restore the Opus 4.6 temperature
stripping that was dropped in the merge.
Port upstream PR #136 (repair_tool_pairs) to prevent HTTP 400 on long
conversations with orphaned tool_use/tool_result blocks, and PR #126
(haiku effort stripping) to prevent HTTP 400 on haiku models.
Resolve the unfinished intent of commit 6d9cade by lowercasing the
first character after MCP-namespace unwrap so hermes's tool dispatcher
resolves the registered snake_case name without its auto-repair
warning. Patch normalize_response on both pre-0.11 hermes
(agent.anthropic_adapter) and 0.11+ hermes (AnthropicTransport) for
backward compatibility.
This commit is contained in:
+245
-30
@@ -5,10 +5,16 @@ from types import SimpleNamespace
|
||||
|
||||
from anthropic_billing_bypass import (
|
||||
_BILLING_ENTRYPOINT,
|
||||
_MCP_HERMES_NAMESPACE,
|
||||
_SYSTEM_IDENTITY,
|
||||
_fix_temperature_for_oauth_adaptive,
|
||||
_install_response_pascalcase_unhook,
|
||||
_model_disables_effort,
|
||||
_pascalcase_mcp_name,
|
||||
_repair_tool_pairs,
|
||||
_strip_effort,
|
||||
_unwrap_tool_name,
|
||||
_wrap_tool_name,
|
||||
apply_claude_code_bypass,
|
||||
)
|
||||
|
||||
@@ -42,6 +48,9 @@ def test_apply_claude_code_bypass_relocates_non_identity_system_text_to_first_us
|
||||
assert "<system-reminder>\nStay helpful.\n</system-reminder>" in text
|
||||
assert "<system-reminder>\nExtra system guidance\n</system-reminder>" in text
|
||||
assert text.endswith("hello world")
|
||||
# Regression: PR #11 introduced literal \n escapes (issue #?). Real
|
||||
# newlines must be present so Anthropic's content validator passes.
|
||||
assert "\\n" not in text
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_is_idempotent(basic_api_kwargs):
|
||||
@@ -107,6 +116,17 @@ def test_fix_temperature_for_oauth_adaptive_without_temperature_is_noop():
|
||||
assert api_kwargs == {"model": "claude-opus-4-6-20260101"}
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_strips_temperature_for_opus46(simple_messages):
|
||||
api_kwargs = {
|
||||
"system": "plain",
|
||||
"messages": [dict(message) for message in simple_messages],
|
||||
"model": "claude-opus-4-6-20260101",
|
||||
"temperature": 0.2,
|
||||
}
|
||||
apply_claude_code_bypass(api_kwargs, "2.1.112")
|
||||
assert "temperature" not in api_kwargs
|
||||
|
||||
|
||||
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"
|
||||
@@ -124,29 +144,71 @@ def test_pascalcase_mcp_name_ignores_unprefixed_names():
|
||||
assert _pascalcase_mcp_name("") == ""
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_rewrites_tool_names_to_pascalcase(basic_api_kwargs):
|
||||
def test_wrap_tool_name_namespaces_to_mcp_hermes():
|
||||
assert _wrap_tool_name("mcp_bash") == "mcp__hermes__Bash"
|
||||
assert _wrap_tool_name("mcp_background_output") == "mcp__hermes__Background_output"
|
||||
assert _wrap_tool_name("bash") == "mcp__hermes__Bash"
|
||||
|
||||
|
||||
def test_wrap_tool_name_is_idempotent_when_already_namespaced():
|
||||
assert _wrap_tool_name("mcp__hermes__Bash") == "mcp__hermes__Bash"
|
||||
|
||||
|
||||
def test_unwrap_tool_name_lowercases_first_char():
|
||||
assert _unwrap_tool_name("mcp__hermes__Bash") == "bash"
|
||||
assert _unwrap_tool_name("mcp__hermes__Background_output") == "background_output"
|
||||
|
||||
|
||||
def test_unwrap_tool_name_handles_already_stripped_prefix():
|
||||
# When hermes's transport strips ``mcp_`` before our hook runs, the name
|
||||
# arrives as ``_hermes__Bash``.
|
||||
assert _unwrap_tool_name("_hermes__Bash") == "bash"
|
||||
|
||||
|
||||
def test_unwrap_tool_name_returns_unknown_names_unchanged():
|
||||
assert _unwrap_tool_name("plain_bash") == "plain_bash"
|
||||
assert _unwrap_tool_name("") == ""
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_rewrites_tool_names_to_hermes_namespace(
|
||||
basic_api_kwargs,
|
||||
):
|
||||
basic_api_kwargs["tools"] = [
|
||||
{"name": "mcp_bash"},
|
||||
{"name": "mcp_background_output"},
|
||||
{"name": "mcp_Already_pascal"},
|
||||
{"name": "mcp__hermes__Already_done"},
|
||||
]
|
||||
# Provide a paired tool_use / tool_result so _repair_tool_pairs doesn't
|
||||
# eat the assistant message before tool-name rewriting runs.
|
||||
basic_api_kwargs["messages"] = [
|
||||
{"role": "user", "content": "hello world"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "name": "mcp_bash", "id": "tool_1", "input": {}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "hello world"},
|
||||
{"type": "tool_use", "name": "mcp_bash", "id": "tool_1", "input": {}},
|
||||
{"type": "tool_result", "tool_use_id": "tool_1", "content": "ok"},
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
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"]
|
||||
assert tool_names == [
|
||||
"mcp__hermes__Bash",
|
||||
"mcp__hermes__Background_output",
|
||||
"mcp__hermes__Already_done",
|
||||
]
|
||||
assert all(name.startswith(_MCP_HERMES_NAMESPACE) for name in tool_names)
|
||||
|
||||
tool_use_block = basic_api_kwargs["messages"][0]["content"][-1]
|
||||
assert tool_use_block["name"] == "mcp_Bash"
|
||||
assistant_msg = basic_api_kwargs["messages"][1]
|
||||
tool_use_block = assistant_msg["content"][0]
|
||||
assert tool_use_block["name"] == "mcp__hermes__Bash"
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_injects_stainless_and_direct_browser_headers(
|
||||
@@ -163,6 +225,9 @@ def test_apply_claude_code_bypass_injects_stainless_and_direct_browser_headers(
|
||||
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")
|
||||
# Regression: PR #11 capitalized these to X-Stainless-*; lowercase keys
|
||||
# match upstream JS SDK output exactly.
|
||||
assert "X-Stainless-Lang" not in extra_headers
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_sets_beta_true_query_param(basic_api_kwargs):
|
||||
@@ -183,29 +248,173 @@ def test_apply_claude_code_bypass_preserves_existing_extra_headers(basic_api_kwa
|
||||
)
|
||||
|
||||
|
||||
def _make_fake_adapter_module(tool_names):
|
||||
def test_repair_tool_pairs_drops_orphaned_tool_use_and_result():
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "t1", "name": "bash", "input": {}},
|
||||
{"type": "tool_use", "id": "t2", "name": "read", "input": {}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "t1", "content": "ok"},
|
||||
{"type": "tool_result", "tool_use_id": "tX", "content": "stale"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
repaired = _repair_tool_pairs(messages)
|
||||
|
||||
assert len(repaired) == 3
|
||||
assistant_blocks = repaired[1]["content"]
|
||||
assert [b["id"] for b in assistant_blocks] == ["t1"]
|
||||
user_results = repaired[2]["content"]
|
||||
assert [b["tool_use_id"] for b in user_results] == ["t1"]
|
||||
|
||||
|
||||
def test_repair_tool_pairs_drops_messages_with_only_orphans():
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "tX", "name": "bash", "input": {}}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
repaired = _repair_tool_pairs(messages)
|
||||
|
||||
assert len(repaired) == 1
|
||||
assert repaired[0]["role"] == "user"
|
||||
|
||||
|
||||
def test_repair_tool_pairs_returns_input_when_nothing_to_repair():
|
||||
messages = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "tool_use", "id": "t1", "name": "bash", "input": {}}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": "ok"}],
|
||||
},
|
||||
]
|
||||
|
||||
repaired = _repair_tool_pairs(messages)
|
||||
assert repaired is messages # identity-preserving no-op
|
||||
|
||||
|
||||
def test_apply_claude_code_bypass_repairs_tool_pairs_before_signing(simple_messages):
|
||||
api_kwargs = {
|
||||
"system": "p",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello world"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "tX", "name": "mcp_read", "input": {}}
|
||||
],
|
||||
},
|
||||
],
|
||||
"model": "claude-opus-4-6-20260101",
|
||||
}
|
||||
|
||||
apply_claude_code_bypass(api_kwargs, "2.1.112")
|
||||
|
||||
# Orphan removed; assistant message dropped (now empty after filter).
|
||||
assert len(api_kwargs["messages"]) == 1
|
||||
assert api_kwargs["messages"][0]["role"] == "user"
|
||||
|
||||
|
||||
def test_model_disables_effort_for_haiku():
|
||||
assert _model_disables_effort("claude-haiku-3-5")
|
||||
assert _model_disables_effort("claude-3-5-haiku-20241022")
|
||||
assert not _model_disables_effort("claude-opus-4-6-20260101")
|
||||
assert not _model_disables_effort("claude-sonnet-4-7")
|
||||
|
||||
|
||||
def test_strip_effort_removes_from_output_config_for_haiku():
|
||||
api = {
|
||||
"model": "claude-haiku-3-5",
|
||||
"output_config": {"effort": "low", "other": True},
|
||||
}
|
||||
_strip_effort(api)
|
||||
assert api["output_config"] == {"other": True}
|
||||
|
||||
|
||||
def test_strip_effort_drops_empty_parent_dict():
|
||||
api = {
|
||||
"model": "claude-haiku-3-5",
|
||||
"output_config": {"effort": "low"},
|
||||
"thinking": {"effort": "medium"},
|
||||
}
|
||||
_strip_effort(api)
|
||||
assert "output_config" not in api
|
||||
assert "thinking" not in api
|
||||
|
||||
|
||||
def test_strip_effort_preserves_other_thinking_keys():
|
||||
api = {
|
||||
"model": "claude-haiku-3-5",
|
||||
"thinking": {"effort": "low", "budget_tokens": 1024},
|
||||
}
|
||||
_strip_effort(api)
|
||||
assert api["thinking"] == {"budget_tokens": 1024}
|
||||
|
||||
|
||||
def test_strip_effort_is_noop_for_non_haiku():
|
||||
api = {
|
||||
"model": "claude-opus-4-6-20260101",
|
||||
"output_config": {"effort": "low"},
|
||||
}
|
||||
_strip_effort(api)
|
||||
assert api["output_config"] == {"effort": "low"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response unwrap hook tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_fake_adapter_module(tool_names, *, namespace=True):
|
||||
"""Build a SimpleNamespace mimicking ``agent.anthropic_adapter``.
|
||||
|
||||
Tool names returned by ``original_normalize`` arrive in
|
||||
``mcp__hermes__Foo`` form because that's what we sent on the way out.
|
||||
"""
|
||||
wrapped_names = [
|
||||
(_MCP_HERMES_NAMESPACE + n[len("mcp_"):][0].upper() + n[len("mcp_") + 1:])
|
||||
if namespace and n.startswith("mcp_")
|
||||
else n
|
||||
for n in 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
|
||||
for name in wrapped_names:
|
||||
tool_calls.append(
|
||||
SimpleNamespace(
|
||||
id="tool_1",
|
||||
type="function",
|
||||
function=SimpleNamespace(name=stripped, arguments="{}"),
|
||||
function=SimpleNamespace(name=name, 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
|
||||
return SimpleNamespace(normalize_anthropic_response=original_normalize)
|
||||
|
||||
|
||||
def test_response_unhook_lowercases_first_char_of_tool_names_after_strip():
|
||||
adapter = _make_fake_adapter_module(["mcp_Bash", "mcp_Background_output"])
|
||||
def test_response_unhook_lowercases_first_char_of_tool_names():
|
||||
adapter = _make_fake_adapter_module(["mcp_bash", "mcp_background_output"])
|
||||
|
||||
assert _install_response_pascalcase_unhook(adapter) is True
|
||||
assert _install_response_pascalcase_unhook(adapter, force=True) is True
|
||||
|
||||
msg, _reason = adapter.normalize_anthropic_response(
|
||||
response=object(), strip_tool_prefix=True
|
||||
@@ -215,25 +424,31 @@ def test_response_unhook_lowercases_first_char_of_tool_names_after_strip():
|
||||
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
|
||||
def test_response_unhook_strips_hermes_namespace_when_present():
|
||||
adapter = _make_fake_adapter_module(
|
||||
["mcp__hermes__Bash"], namespace=False
|
||||
)
|
||||
|
||||
assert msg.tool_calls[0].function.name == "mcp_Bash"
|
||||
_install_response_pascalcase_unhook(adapter, force=True)
|
||||
|
||||
msg, _reason = adapter.normalize_anthropic_response(response=object())
|
||||
assert msg.tool_calls[0].function.name == "bash"
|
||||
|
||||
|
||||
def test_response_unhook_leaves_unknown_names_alone():
|
||||
adapter = _make_fake_adapter_module(["plain_tool"], namespace=False)
|
||||
|
||||
_install_response_pascalcase_unhook(adapter, force=True)
|
||||
|
||||
msg, _reason = adapter.normalize_anthropic_response(response=object())
|
||||
assert msg.tool_calls[0].function.name == "plain_tool"
|
||||
|
||||
|
||||
def test_response_unhook_is_idempotent():
|
||||
adapter = _make_fake_adapter_module(["mcp_Bash"])
|
||||
adapter = _make_fake_adapter_module(["mcp_bash"])
|
||||
|
||||
assert _install_response_pascalcase_unhook(adapter) is True
|
||||
assert _install_response_pascalcase_unhook(adapter, force=True) is True
|
||||
assert _install_response_pascalcase_unhook(adapter) is True
|
||||
|
||||
msg, _reason = adapter.normalize_anthropic_response(
|
||||
response=object(), strip_tool_prefix=True
|
||||
)
|
||||
msg, _reason = adapter.normalize_anthropic_response(response=object())
|
||||
assert msg.tool_calls[0].function.name == "bash"
|
||||
|
||||
Reference in New Issue
Block a user