mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(normalizer): 移除 Claude system 中的 billing header
This commit is contained in:
@@ -66,6 +66,7 @@ from src.core.logger import logger
|
|||||||
|
|
||||||
class ClaudeNormalizer(FormatNormalizer):
|
class ClaudeNormalizer(FormatNormalizer):
|
||||||
FORMAT_ID = "claude:chat"
|
FORMAT_ID = "claude:chat"
|
||||||
|
_ANTHROPIC_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:"
|
||||||
capabilities = FormatCapabilities(
|
capabilities = FormatCapabilities(
|
||||||
supports_stream=True,
|
supports_stream=True,
|
||||||
supports_error_conversion=True,
|
supports_error_conversion=True,
|
||||||
@@ -1128,7 +1129,12 @@ class ClaudeNormalizer(FormatNormalizer):
|
|||||||
if system_value is None:
|
if system_value is None:
|
||||||
return None, dropped, None
|
return None, dropped, None
|
||||||
if isinstance(system_value, str):
|
if isinstance(system_value, str):
|
||||||
return (system_value or None), dropped, None
|
sanitized_text, removed_header_count = self._strip_anthropic_billing_headers(
|
||||||
|
system_value
|
||||||
|
)
|
||||||
|
if removed_header_count:
|
||||||
|
dropped["claude_system_billing_header_removed"] = removed_header_count
|
||||||
|
return sanitized_text, dropped, None
|
||||||
|
|
||||||
if isinstance(system_value, list):
|
if isinstance(system_value, list):
|
||||||
texts: list[str] = []
|
texts: list[str] = []
|
||||||
@@ -1143,14 +1149,26 @@ class ClaudeNormalizer(FormatNormalizer):
|
|||||||
if item.get("type") == "text":
|
if item.get("type") == "text":
|
||||||
text = item.get("text")
|
text = item.get("text")
|
||||||
if text:
|
if text:
|
||||||
texts.append(str(text))
|
normalized_text, removed_header_count = (
|
||||||
|
self._strip_anthropic_billing_headers(text)
|
||||||
|
)
|
||||||
|
if removed_header_count:
|
||||||
|
dropped["claude_system_billing_header_removed"] = (
|
||||||
|
dropped.get("claude_system_billing_header_removed", 0)
|
||||||
|
+ removed_header_count
|
||||||
|
)
|
||||||
|
if not normalized_text:
|
||||||
|
continue
|
||||||
|
texts.append(normalized_text)
|
||||||
seg_extra: dict[str, Any] = {}
|
seg_extra: dict[str, Any] = {}
|
||||||
cc = item.get("cache_control")
|
cc = item.get("cache_control")
|
||||||
if isinstance(cc, dict):
|
if isinstance(cc, dict):
|
||||||
seg_extra["cache_control"] = cc
|
seg_extra["cache_control"] = cc
|
||||||
has_cache_control = True
|
has_cache_control = True
|
||||||
segments.append(
|
segments.append(
|
||||||
InstructionSegment(role=Role.SYSTEM, text=str(text), extra=seg_extra)
|
InstructionSegment(
|
||||||
|
role=Role.SYSTEM, text=normalized_text, extra=seg_extra
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
dropped_key = f"claude_system_item:{item.get('type')}"
|
dropped_key = f"claude_system_item:{item.get('type')}"
|
||||||
@@ -1165,6 +1183,28 @@ class ClaudeNormalizer(FormatNormalizer):
|
|||||||
dropped["claude_system_unsupported"] = dropped.get("claude_system_unsupported", 0) + 1
|
dropped["claude_system_unsupported"] = dropped.get("claude_system_unsupported", 0) + 1
|
||||||
return None, dropped, None
|
return None, dropped, None
|
||||||
|
|
||||||
|
def _strip_anthropic_billing_headers(self, text: Any) -> tuple[str | None, int]:
|
||||||
|
normalized_text = str(text)
|
||||||
|
stripped_leading = normalized_text.lstrip()
|
||||||
|
if not stripped_leading.lower().startswith(self._ANTHROPIC_BILLING_HEADER_PREFIX):
|
||||||
|
return normalized_text, 0
|
||||||
|
|
||||||
|
lines = stripped_leading.splitlines(keepends=True)
|
||||||
|
idx = 0
|
||||||
|
removed_header_count = 0
|
||||||
|
while idx < len(lines) and lines[idx].strip().lower().startswith(
|
||||||
|
self._ANTHROPIC_BILLING_HEADER_PREFIX
|
||||||
|
):
|
||||||
|
removed_header_count += 1
|
||||||
|
idx += 1
|
||||||
|
while idx < len(lines) and not lines[idx].strip():
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
sanitized_text = "".join(lines[idx:])
|
||||||
|
if not sanitized_text.strip():
|
||||||
|
return None, removed_header_count
|
||||||
|
return sanitized_text, removed_header_count
|
||||||
|
|
||||||
def _join_instructions(self, instructions: list[InstructionSegment]) -> str | None:
|
def _join_instructions(self, instructions: list[InstructionSegment]) -> str | None:
|
||||||
parts = [seg.text for seg in instructions if seg.text]
|
parts = [seg.text for seg in instructions if seg.text]
|
||||||
joined = "\n\n".join(parts)
|
joined = "\n\n".join(parts)
|
||||||
|
|||||||
@@ -369,8 +369,8 @@ def test_claude_request_metadata_preserved() -> None:
|
|||||||
assert out["metadata"]["user_id"] == "user_abc123_session_xyz456"
|
assert out["metadata"]["user_id"] == "user_abc123_session_xyz456"
|
||||||
|
|
||||||
|
|
||||||
def test_claude_system_array_format() -> None:
|
def test_claude_system_array_format_drops_billing_header() -> None:
|
||||||
"""测试 Claude CLI 风格的 system 数组格式"""
|
"""Claude system 数组中的 Anthropic billing header 不应进入内部 instructions。"""
|
||||||
n = ClaudeNormalizer()
|
n = ClaudeNormalizer()
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
@@ -386,12 +386,51 @@ def test_claude_system_array_format() -> None:
|
|||||||
|
|
||||||
internal = n.request_to_internal(req)
|
internal = n.request_to_internal(req)
|
||||||
|
|
||||||
# system 数组中的多个 text 应该用 \n\n 连接
|
# billing header 应被移除,其余 system 文本仍按顺序保留
|
||||||
assert internal.system is not None
|
assert internal.system is not None
|
||||||
assert "x-anthropic-billing-header" in internal.system
|
assert "x-anthropic-billing-header" not in internal.system
|
||||||
assert "You are Claude Code" in internal.system
|
assert "You are Claude Code" in internal.system
|
||||||
assert "Extract file paths" in internal.system
|
assert "Extract file paths" in internal.system
|
||||||
assert "\n\n" in internal.system
|
assert "\n\n" in internal.system
|
||||||
|
assert internal.extra["raw"]["dropped_blocks"]["claude_system_billing_header_removed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_system_string_strips_billing_header_and_keeps_following_prompt() -> None:
|
||||||
|
n = ClaudeNormalizer()
|
||||||
|
|
||||||
|
req = {
|
||||||
|
"model": "claude-3-opus",
|
||||||
|
"messages": [{"role": "user", "content": "hello"}],
|
||||||
|
"system": "x-anthropic-billing-header: cc_version=2.1.19\n\nYou are Claude Code.\nKeep answers terse.",
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal = n.request_to_internal(req)
|
||||||
|
|
||||||
|
assert internal.system == "You are Claude Code.\nKeep answers terse."
|
||||||
|
assert internal.extra["raw"]["dropped_blocks"]["claude_system_billing_header_removed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_system_array_strips_billing_header_prefix_but_keeps_same_block_prompt() -> None:
|
||||||
|
n = ClaudeNormalizer()
|
||||||
|
|
||||||
|
req = {
|
||||||
|
"model": "claude-3-opus",
|
||||||
|
"messages": [{"role": "user", "content": "hello"}],
|
||||||
|
"system": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "x-anthropic-billing-header: cc_version=2.1.19\n\nYou are Claude Code.",
|
||||||
|
},
|
||||||
|
{"type": "text", "text": "Extract file paths."},
|
||||||
|
],
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal = n.request_to_internal(req)
|
||||||
|
|
||||||
|
assert internal.system == "You are Claude Code.\n\nExtract file paths."
|
||||||
|
assert internal.extra["raw"]["dropped_blocks"]["claude_system_billing_header_removed"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_claude_request_reuses_raw_tool_choice_and_stabilizes_message_sequence() -> None:
|
def test_claude_request_reuses_raw_tool_choice_and_stabilizes_message_sequence() -> None:
|
||||||
|
|||||||
@@ -71,6 +71,67 @@ def test_claude_request_to_openai_cli_places_instructions_before_input() -> None
|
|||||||
assert openai_cli_req["input"][0]["role"] == "user"
|
assert openai_cli_req["input"][0]["role"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_request_to_openai_cli_drops_anthropic_billing_header() -> None:
|
||||||
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
|
claude_req = {
|
||||||
|
"model": "claude-3-5-sonnet-latest",
|
||||||
|
"system": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "x-anthropic-billing-header: cc_version=2.1.74.705; cc_entrypoint=claude-vscode; cch=4fb9f;",
|
||||||
|
},
|
||||||
|
{"type": "text", "text": "You are Claude Code."},
|
||||||
|
{"type": "text", "text": "Keep answers terse."},
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
openai_cli_req = reg.convert_request(claude_req, "claude:chat", "openai:cli")
|
||||||
|
|
||||||
|
assert openai_cli_req["instructions"] == "You are Claude Code.\n\nKeep answers terse."
|
||||||
|
assert "x-anthropic-billing-header" not in openai_cli_req["instructions"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_request_to_openai_cli_keeps_prompt_after_billing_header_prefix() -> None:
|
||||||
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
|
claude_req = {
|
||||||
|
"model": "claude-3-5-sonnet-latest",
|
||||||
|
"system": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "x-anthropic-billing-header: cc_version=2.1.74.705\n\nYou are Claude Code.",
|
||||||
|
},
|
||||||
|
{"type": "text", "text": "Keep answers terse."},
|
||||||
|
],
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
openai_cli_req = reg.convert_request(claude_req, "claude:chat", "openai:cli")
|
||||||
|
|
||||||
|
assert openai_cli_req["instructions"] == "You are Claude Code.\n\nKeep answers terse."
|
||||||
|
assert "x-anthropic-billing-header" not in openai_cli_req["instructions"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_request_to_openai_cli_strips_billing_header_from_string_system() -> None:
|
||||||
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
|
claude_req = {
|
||||||
|
"model": "claude-3-5-sonnet-latest",
|
||||||
|
"system": "x-anthropic-billing-header: cc_version=2.1.74.705\n\nYou are Claude Code.\nKeep answers terse.",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
openai_cli_req = reg.convert_request(claude_req, "claude:chat", "openai:cli")
|
||||||
|
|
||||||
|
assert openai_cli_req["instructions"] == "You are Claude Code.\nKeep answers terse."
|
||||||
|
assert "x-anthropic-billing-header" not in openai_cli_req["instructions"]
|
||||||
|
|
||||||
|
|
||||||
def test_claude_response_to_openai_cli() -> None:
|
def test_claude_response_to_openai_cli() -> None:
|
||||||
reg = _make_registry_with_cli()
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user