mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 01:17:46 +08:00
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:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user