refactor(conversion): body_rules 保护 cache-sensitive 字段,normalizer 保真优化与诊断日志

- RequestBuilder 新增 protected_body_keys 机制,按 provider API 格式阻止 body_rules
  改写 prompt cache 相关的顶层请求字段(messages/tools/system 等)
- Claude/Gemini normalizer 优先复用原始 raw tool_choice,避免 round-trip 丢失信息
- Claude normalizer 修复 content blocks 输出顺序(flush_text_parts),
  _coerce_claude_message_sequence 返回结构化诊断
- OpenAI normalizer 保留 raw tool call arguments 字符串与原始 tool 定义 extra 字段
- schema_utils allOf 合并保持 required 字段插入顺序
- 各转换环节增加结构化 debug 日志用于调试
This commit is contained in:
fawney19
2026-03-17 01:25:29 +08:00
parent c97c9332eb
commit 4ecaefbade
22 changed files with 1191 additions and 115 deletions
+118
View File
@@ -1,6 +1,10 @@
from typing import Any
import pytest
from src.api.handlers.base.request_builder import get_cache_sensitive_protected_body_keys
from src.api.handlers.claude.adapter import ClaudeChatAdapter
from src.api.handlers.gemini.adapter import GeminiChatAdapter
@pytest.mark.asyncio
@@ -23,3 +27,117 @@ def test_validate_test_base_url_trims_whitespace() -> None:
assert ClaudeChatAdapter._validate_test_base_url(" https://api.anthropic.com/v1 ") == (
"https://api.anthropic.com/v1"
)
@pytest.mark.asyncio
async def test_claude_check_endpoint_passes_cache_sensitive_protected_keys_to_body_rules(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as endpoint_checker_module
from src.api.handlers.base import request_builder as request_builder_module
captured: dict[str, Any] = {}
def fake_apply_body_rules(
body: dict[str, Any],
body_rules: list[dict[str, Any]],
protected_keys: frozenset[str] | None = None,
original_body: dict[str, Any] | None = None,
) -> dict[str, Any]:
captured["body_rules"] = body_rules
captured["protected_keys"] = protected_keys
captured["original_body"] = original_body
return body
async def fake_run_endpoint_check(**kwargs: Any) -> dict[str, Any]:
captured["json_body"] = kwargs["json_body"]
return {"status_code": 200, "json_body": kwargs["json_body"]}
def fake_build_request_body(
cls: type[ClaudeChatAdapter],
request_data: dict[str, Any] | None = None,
*,
base_url: str | None = None,
provider_type: str | None = None,
) -> dict[str, Any]:
del request_data, base_url, provider_type
return {
"messages": [{"role": "user", "content": "hello"}],
"system": "keep",
}
monkeypatch.setattr(request_builder_module, "apply_body_rules", fake_apply_body_rules)
monkeypatch.setattr(endpoint_checker_module, "run_endpoint_check", fake_run_endpoint_check)
monkeypatch.setattr(
ClaudeChatAdapter, "build_request_body", classmethod(fake_build_request_body)
)
result = await ClaudeChatAdapter.check_endpoint(
client=None, # type: ignore[arg-type]
base_url="https://api.anthropic.com/v1",
api_key="test-key",
request_data={"model": "claude-sonnet-4-5-20250929", "stream": False},
body_rules=[{"action": "set", "path": "messages", "value": []}],
)
assert captured["protected_keys"] == get_cache_sensitive_protected_body_keys(
ClaudeChatAdapter.FORMAT_ID
)
assert result["status_code"] == 200
assert result["json_body"] == captured["json_body"]
@pytest.mark.asyncio
async def test_gemini_check_endpoint_passes_alias_aware_protected_keys_to_body_rules(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as endpoint_checker_module
from src.api.handlers.base import request_builder as request_builder_module
captured: dict[str, Any] = {}
def fake_apply_body_rules(
body: dict[str, Any],
body_rules: list[dict[str, Any]],
protected_keys: frozenset[str] | None = None,
original_body: dict[str, Any] | None = None,
) -> dict[str, Any]:
captured["body_rules"] = body_rules
captured["protected_keys"] = protected_keys
captured["original_body"] = original_body
return body
async def fake_run_endpoint_check(**kwargs: Any) -> dict[str, Any]:
captured["json_body"] = kwargs["json_body"]
return {"status_code": 200, "json_body": kwargs["json_body"]}
def fake_build_request_body(
cls: type[GeminiChatAdapter], request_data: dict[str, Any] | None = None
) -> dict[str, Any]:
_ = cls, request_data
return {
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
"systemInstruction": {"parts": [{"text": "system"}]},
"toolConfig": {"functionCallingConfig": {"mode": "AUTO"}},
"generationConfig": {"temperature": 0.1},
}
monkeypatch.setattr(request_builder_module, "apply_body_rules", fake_apply_body_rules)
monkeypatch.setattr(endpoint_checker_module, "run_endpoint_check", fake_run_endpoint_check)
monkeypatch.setattr(
GeminiChatAdapter, "build_request_body", classmethod(fake_build_request_body)
)
result = await GeminiChatAdapter.check_endpoint(
client=None, # type: ignore[arg-type]
base_url="https://generativelanguage.googleapis.com",
api_key="test-key",
request_data={"model": "gemini-2.5-pro", "stream": False},
body_rules=[{"action": "drop", "path": "toolConfig"}],
)
protected_keys = captured["protected_keys"]
assert protected_keys == get_cache_sensitive_protected_body_keys(GeminiChatAdapter.FORMAT_ID)
assert {"systemInstruction", "toolConfig", "generationConfig"}.issubset(protected_keys)
assert result["status_code"] == 200
assert result["json_body"] == captured["json_body"]