mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
refactor(body-rules): 移除 protected_body_keys 机制,允许 body_rules 自由修改所有请求体字段
删除 get_cache_sensitive_protected_body_keys 函数及相关常量、_is_protected_path 辅助函数,从 apply_body_rules、RequestBuilder、ProviderRequestResult 等处移除 protected_body_keys 参数,更新所有调用点和测试用例。
This commit is contained in:
@@ -47,7 +47,6 @@ from src.api.handlers.base.chat_error_utils import (
|
||||
from src.api.handlers.base.parsers import get_parser_for_format
|
||||
from src.api.handlers.base.request_builder import (
|
||||
PassthroughRequestBuilder,
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
get_provider_auth,
|
||||
)
|
||||
from src.api.handlers.base.response_parser import ResponseParser
|
||||
@@ -106,7 +105,6 @@ class ProviderRequestResult:
|
||||
mapped_model: str | None
|
||||
envelope: Any # ProviderEnvelope | None
|
||||
extra_headers: dict[str, str] = field(default_factory=dict)
|
||||
protected_body_keys: frozenset[str] = field(default_factory=frozenset)
|
||||
upstream_is_stream: bool = True
|
||||
needs_conversion: bool = False
|
||||
provider_api_format: str = ""
|
||||
@@ -846,10 +844,6 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
mapped_model=mapped_model,
|
||||
envelope=envelope,
|
||||
extra_headers=extra_headers,
|
||||
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||
provider_api_format,
|
||||
provider_type=provider_type,
|
||||
),
|
||||
upstream_is_stream=upstream_is_stream,
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format=provider_api_format,
|
||||
@@ -930,7 +924,6 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
||||
extra_headers=prep.extra_headers if prep.extra_headers else None,
|
||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||
envelope=envelope,
|
||||
protected_body_keys=prep.protected_body_keys,
|
||||
provider_api_format=prep.provider_api_format,
|
||||
)
|
||||
if upstream_is_stream:
|
||||
|
||||
@@ -490,7 +490,6 @@ class ChatSyncExecutor:
|
||||
extra_headers=prep.extra_headers if prep.extra_headers else None,
|
||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||
envelope=envelope,
|
||||
protected_body_keys=prep.protected_body_keys,
|
||||
provider_api_format=prep.provider_api_format,
|
||||
)
|
||||
if upstream_is_stream:
|
||||
|
||||
@@ -20,7 +20,6 @@ from src.api.handlers.base.base_handler import (
|
||||
)
|
||||
from src.api.handlers.base.parsers import get_parser_for_format
|
||||
from src.api.handlers.base.request_builder import (
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
get_provider_auth,
|
||||
)
|
||||
from src.api.handlers.base.stream_context import StreamContext
|
||||
@@ -442,10 +441,6 @@ class CliStreamMixin:
|
||||
extra_headers=extra_headers if extra_headers else None,
|
||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||
envelope=envelope,
|
||||
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||
provider_api_format,
|
||||
provider_type=provider_type,
|
||||
),
|
||||
provider_api_format=provider_api_format,
|
||||
)
|
||||
if upstream_is_stream:
|
||||
|
||||
@@ -12,7 +12,6 @@ from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.handlers.base.parsers import get_parser_for_format
|
||||
from src.api.handlers.base.request_builder import (
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
get_provider_auth,
|
||||
)
|
||||
from src.api.handlers.base.stream_context import extract_proxy_timing, is_format_converted
|
||||
@@ -263,10 +262,6 @@ class CliSyncMixin:
|
||||
extra_headers=extra_headers if extra_headers else None,
|
||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||
envelope=envelope,
|
||||
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||
provider_api_format,
|
||||
provider_type=provider_type,
|
||||
),
|
||||
provider_api_format=provider_api_format,
|
||||
)
|
||||
if upstream_is_stream:
|
||||
|
||||
@@ -369,7 +369,6 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
from src.api.handlers.base.request_builder import (
|
||||
apply_body_rules,
|
||||
evaluate_condition,
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
)
|
||||
from src.core.api_format.headers import HeaderBuilder
|
||||
from src.core.provider_types import ProviderType
|
||||
@@ -514,10 +513,6 @@ class HandlerAdapterBase(ApiAdapter):
|
||||
body = apply_body_rules(
|
||||
body,
|
||||
body_rules,
|
||||
protected_keys=get_cache_sensitive_protected_body_keys(
|
||||
cls.FORMAT_ID,
|
||||
provider_type=provider_type,
|
||||
),
|
||||
original_body=body,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ def summarize_request_payload_shape(
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
provider_api_format: str | None,
|
||||
protected_body_keys: frozenset[str] | None,
|
||||
body_rules: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""生成最终出站 payload 的结构化摘要,避免记录正文。"""
|
||||
@@ -85,8 +84,6 @@ def summarize_request_payload_shape(
|
||||
k in payload for k in ("generation_config", "generationConfig")
|
||||
),
|
||||
"has_prompt_cache_key": bool(str(payload.get("prompt_cache_key") or "").strip()),
|
||||
"protected_body_keys_enabled": bool(protected_body_keys),
|
||||
"protected_body_keys": sorted(protected_body_keys or ()),
|
||||
"body_rule_count": len(body_rules) if isinstance(body_rules, list) else 0,
|
||||
}
|
||||
|
||||
@@ -98,63 +95,6 @@ def summarize_request_payload_shape(
|
||||
# 兼容别名:历史代码使用 SENSITIVE_HEADERS 命名
|
||||
SENSITIVE_HEADERS: frozenset[str] = UPSTREAM_DROP_HEADERS
|
||||
|
||||
# 请求体中受保护的字段(不能被 body_rules 修改)
|
||||
PROTECTED_BODY_FIELDS: frozenset[str] = frozenset(
|
||||
{
|
||||
"model", # 模型名由系统管理
|
||||
"stream", # 流式标志由系统管理
|
||||
}
|
||||
)
|
||||
|
||||
# cache-sensitive 顶层字段:用于阻止 endpoint body_rules 在最终出站前
|
||||
# 二次改写 prompt-bearing 结构,破坏上游 prompt cache 一致性。
|
||||
_CACHE_SENSITIVE_BODY_FIELDS_BY_FORMAT: dict[str, frozenset[str]] = {
|
||||
"openai:chat": frozenset({"messages", "tools", "tool_choice"}),
|
||||
"openai:cli": frozenset({"input", "instructions", "tools", "tool_choice", "prompt_cache_key"}),
|
||||
"openai:compact": frozenset(
|
||||
{"input", "instructions", "tools", "tool_choice", "prompt_cache_key"}
|
||||
),
|
||||
"claude:chat": frozenset({"messages", "system", "tools", "tool_choice"}),
|
||||
"gemini:chat": frozenset(
|
||||
{
|
||||
"contents",
|
||||
"system_instruction",
|
||||
"systemInstruction",
|
||||
"tools",
|
||||
"tool_config",
|
||||
"toolConfig",
|
||||
"generation_config",
|
||||
"generationConfig",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
# Provider 维度可覆盖默认 prompt-bearing 保护集合。
|
||||
# Codex OpenAI CLI/Compact 允许 endpoint body_rules 调整 instructions/input/tools/tool_choice,
|
||||
# 仅继续保护系统字段与 prompt_cache_key,避免"已保存但规则不生效"。
|
||||
_CODEX_PROTECTED: frozenset[str] = frozenset({"prompt_cache_key"})
|
||||
_CACHE_SENSITIVE_BODY_FIELDS_BY_PROVIDER_AND_FORMAT: dict[tuple[str, str], frozenset[str]] = {
|
||||
("codex", "openai:cli"): _CODEX_PROTECTED,
|
||||
("codex", "openai:compact"): _CODEX_PROTECTED,
|
||||
}
|
||||
|
||||
|
||||
def get_cache_sensitive_protected_body_keys(
|
||||
provider_api_format: str | None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> frozenset[str]:
|
||||
"""根据目标 Provider API 格式和 provider_type 返回需要保护的顶层请求字段。"""
|
||||
fmt = str(provider_api_format or "").strip().lower()
|
||||
pt = str(provider_type or "").strip().lower()
|
||||
extra = _CACHE_SENSITIVE_BODY_FIELDS_BY_PROVIDER_AND_FORMAT.get((pt, fmt))
|
||||
if extra is None:
|
||||
extra = _CACHE_SENSITIVE_BODY_FIELDS_BY_FORMAT.get(fmt, frozenset())
|
||||
if not extra:
|
||||
return PROTECTED_BODY_FIELDS
|
||||
return frozenset({*PROTECTED_BODY_FIELDS, *extra})
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 测试请求常量与辅助函数
|
||||
# ==============================================================================
|
||||
@@ -602,20 +542,11 @@ def _rename_nested_value(obj: dict[str, Any], from_path: str, to_path: str) -> b
|
||||
return True
|
||||
|
||||
|
||||
def _is_protected_path(parts: list[PathSegment], protected_lower: frozenset[str]) -> bool:
|
||||
"""检查路径的顶层 key 是否为受保护字段(int 索引不可能是受保护字段)"""
|
||||
if not parts:
|
||||
return False
|
||||
first = parts[0]
|
||||
return isinstance(first, str) and first.lower() in protected_lower
|
||||
|
||||
|
||||
def _extract_path(
|
||||
rule: dict[str, Any],
|
||||
protected_lower: frozenset[str],
|
||||
key: str = "path",
|
||||
) -> str | None:
|
||||
"""从规则中提取并校验 path 字段,返回 strip 后的路径或 None(无效/受保护时)。"""
|
||||
"""从规则中提取并校验 path 字段,返回 strip 后的路径或 None。"""
|
||||
raw = rule.get(key, "")
|
||||
if not isinstance(raw, str):
|
||||
return None
|
||||
@@ -623,8 +554,6 @@ def _extract_path(
|
||||
parts = _parse_path(path)
|
||||
if not parts:
|
||||
return None
|
||||
if _is_protected_path(parts, protected_lower):
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
@@ -976,7 +905,6 @@ def evaluate_condition(
|
||||
def apply_body_rules(
|
||||
body: dict[str, Any],
|
||||
rules: list[dict[str, Any]],
|
||||
protected_keys: frozenset[str] | None = None,
|
||||
original_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -1008,7 +936,6 @@ def apply_body_rules(
|
||||
Args:
|
||||
body: 原始请求体
|
||||
rules: 规则列表
|
||||
protected_keys: 受保护的字段(不能被 set/drop/rename 修改)
|
||||
original_body: 条件评估使用的原始请求体;未提供时回退到当前 body
|
||||
|
||||
Returns:
|
||||
@@ -1019,8 +946,6 @@ def apply_body_rules(
|
||||
|
||||
# 深拷贝,避免修改原始数据(尤其是嵌套 dict/list)
|
||||
result = copy.deepcopy(body)
|
||||
protected = protected_keys or PROTECTED_BODY_FIELDS
|
||||
protected_lower = frozenset(str(k).lower() for k in protected)
|
||||
|
||||
for rule in rules:
|
||||
if not isinstance(rule, dict):
|
||||
@@ -1041,7 +966,7 @@ def apply_body_rules(
|
||||
action = action.strip().lower()
|
||||
|
||||
if action == "set":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
parts = _parse_path(path)
|
||||
@@ -1055,7 +980,7 @@ def apply_body_rules(
|
||||
_set_nested_value(result, target_path, value)
|
||||
|
||||
elif action == "drop":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
parts = _parse_path(path)
|
||||
@@ -1085,12 +1010,6 @@ def apply_body_rules(
|
||||
if not from_parts or not to_parts:
|
||||
continue
|
||||
|
||||
# 受保护字段只检查顶层 key
|
||||
if _is_protected_path(from_parts, protected_lower) or _is_protected_path(
|
||||
to_parts, protected_lower
|
||||
):
|
||||
continue
|
||||
|
||||
# rename 不支持通配符(语义不明确)
|
||||
if _has_wildcard(from_parts) or _has_wildcard(to_parts):
|
||||
continue
|
||||
@@ -1098,7 +1017,7 @@ def apply_body_rules(
|
||||
_rename_nested_value(result, from_path, to_path)
|
||||
|
||||
elif action == "append":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
parts = _parse_path(path)
|
||||
@@ -1116,7 +1035,7 @@ def apply_body_rules(
|
||||
target.append(rule.get("value"))
|
||||
|
||||
elif action == "insert":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
index = rule.get("index")
|
||||
@@ -1129,7 +1048,7 @@ def apply_body_rules(
|
||||
target.insert(index, rule.get("value"))
|
||||
|
||||
elif action == "regex_replace":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
pattern = rule.get("pattern")
|
||||
@@ -1167,7 +1086,7 @@ def apply_body_rules(
|
||||
_set_nested_value(result, target_path, new_val)
|
||||
|
||||
elif action == "name_style":
|
||||
path = _extract_path(rule, protected_lower)
|
||||
path = _extract_path(rule)
|
||||
if not path:
|
||||
continue
|
||||
style = rule.get("style")
|
||||
@@ -1237,7 +1156,6 @@ class RequestBuilder(ABC):
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
pre_computed_auth: tuple[str, str] | None = None,
|
||||
envelope: ProviderEnvelope | None = None,
|
||||
protected_body_keys: frozenset[str] | None = None,
|
||||
provider_api_format: str | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, str]]:
|
||||
"""
|
||||
@@ -1252,7 +1170,6 @@ class RequestBuilder(ABC):
|
||||
is_stream: 是否为流式请求
|
||||
extra_headers: 额外请求头
|
||||
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value)
|
||||
protected_body_keys: body_rules 不允许修改的顶层请求字段
|
||||
provider_api_format: 运行时实际生效的 Provider API 格式,用于准确记录摘要日志
|
||||
|
||||
Returns:
|
||||
@@ -1270,7 +1187,6 @@ class RequestBuilder(ABC):
|
||||
payload = apply_body_rules(
|
||||
payload,
|
||||
body_rules,
|
||||
protected_keys=protected_body_keys,
|
||||
original_body=original_body,
|
||||
)
|
||||
|
||||
@@ -1280,7 +1196,6 @@ class RequestBuilder(ABC):
|
||||
summarize_request_payload_shape(
|
||||
payload,
|
||||
provider_api_format=effective_provider_api_format,
|
||||
protected_body_keys=protected_body_keys,
|
||||
body_rules=body_rules,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -251,7 +251,6 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
from src.api.handlers.base.request_builder import (
|
||||
apply_body_rules,
|
||||
evaluate_condition,
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
)
|
||||
from src.core.api_format.headers import HeaderBuilder
|
||||
|
||||
@@ -351,7 +350,6 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
body = apply_body_rules(
|
||||
body,
|
||||
body_rules,
|
||||
protected_keys=get_cache_sensitive_protected_body_keys(cls.FORMAT_ID),
|
||||
original_body=body,
|
||||
)
|
||||
|
||||
|
||||
@@ -333,7 +333,6 @@ def test_codex_passthrough_builder_preserves_real_codex_headers() -> None:
|
||||
|
||||
|
||||
def test_codex_passthrough_builder_applies_prompt_body_rules() -> None:
|
||||
from src.api.handlers.base.request_builder import get_cache_sensitive_protected_body_keys
|
||||
from src.core.api_format.metadata import CODEX_DEFAULT_BODY_RULES
|
||||
|
||||
builder = PassthroughRequestBuilder()
|
||||
@@ -351,10 +350,6 @@ def test_codex_passthrough_builder_applies_prompt_body_rules() -> None:
|
||||
endpoint,
|
||||
key,
|
||||
pre_computed_auth=("Authorization", "Bearer upstream-token"),
|
||||
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||
"openai:cli",
|
||||
provider_type="codex",
|
||||
),
|
||||
provider_api_format="openai:cli",
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Any
|
||||
|
||||
from src.api.handlers.base.request_builder import (
|
||||
apply_body_rules,
|
||||
get_cache_sensitive_protected_body_keys,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,17 +36,17 @@ class TestApplyBodyRulesNestedPaths:
|
||||
)
|
||||
assert result == {"old": {}, "new": {"path": "value"}}
|
||||
|
||||
def test_protected_top_level_key(self) -> None:
|
||||
def test_top_level_keys_can_be_updated(self) -> None:
|
||||
body = {"model": "gpt-4", "extra": {"model": "ignored"}}
|
||||
result = apply_body_rules(
|
||||
body,
|
||||
[
|
||||
{"action": "set", "path": "model.sub", "value": "x"}, # 应被忽略
|
||||
{"action": "set", "path": "extra.model", "value": "y"}, # 应生效
|
||||
{"action": "set", "path": "model", "value": "gpt-4.1"},
|
||||
{"action": "set", "path": "extra.model", "value": "y"},
|
||||
],
|
||||
)
|
||||
assert result["model"] == "gpt-4" # 顶层受保护,不变
|
||||
assert result["extra"]["model"] == "y" # extra 不受保护
|
||||
assert result["model"] == "gpt-4.1"
|
||||
assert result["extra"]["model"] == "y"
|
||||
|
||||
def test_escaped_dot(self) -> None:
|
||||
body: dict[str, Any] = {}
|
||||
@@ -185,14 +184,14 @@ class TestSetWithOriginalPlaceholder:
|
||||
)
|
||||
assert result == {"a": {"b": [{"content": "original"}]}}
|
||||
|
||||
def test_protected_field_ignored(self) -> None:
|
||||
"""受保护字段(model, stream)跳过"""
|
||||
def test_original_placeholder_updates_top_level_field(self) -> None:
|
||||
"""顶层字段同样支持 {{$original}} 占位符更新"""
|
||||
body = {"model": "gpt-4", "other": "val"}
|
||||
result = apply_body_rules(
|
||||
body,
|
||||
[{"action": "set", "path": "model", "value": "{{$original}}_modified"}],
|
||||
)
|
||||
assert result["model"] == "gpt-4"
|
||||
assert result["model"] == "gpt-4_modified"
|
||||
|
||||
def test_does_not_mutate_original(self) -> None:
|
||||
"""不修改原始 body"""
|
||||
@@ -1516,57 +1515,12 @@ class TestItemCondition:
|
||||
assert result["matched"] is True
|
||||
|
||||
|
||||
class TestProtectedBodyKeys:
|
||||
def test_get_cache_sensitive_protected_body_keys_by_format(self) -> None:
|
||||
assert get_cache_sensitive_protected_body_keys("openai:chat") == frozenset(
|
||||
{"model", "stream", "messages", "tools", "tool_choice"}
|
||||
)
|
||||
assert get_cache_sensitive_protected_body_keys("openai:cli") == frozenset(
|
||||
{
|
||||
"model",
|
||||
"stream",
|
||||
"input",
|
||||
"instructions",
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"prompt_cache_key",
|
||||
}
|
||||
)
|
||||
assert get_cache_sensitive_protected_body_keys(
|
||||
"openai:cli",
|
||||
provider_type="codex",
|
||||
) == frozenset({"model", "stream", "prompt_cache_key"})
|
||||
assert get_cache_sensitive_protected_body_keys(
|
||||
"openai:compact",
|
||||
provider_type="codex",
|
||||
) == frozenset({"model", "stream", "prompt_cache_key"})
|
||||
assert get_cache_sensitive_protected_body_keys("claude:chat") == frozenset(
|
||||
{"model", "stream", "messages", "system", "tools", "tool_choice"}
|
||||
)
|
||||
assert get_cache_sensitive_protected_body_keys("gemini:chat") == frozenset(
|
||||
{
|
||||
"model",
|
||||
"stream",
|
||||
"contents",
|
||||
"system_instruction",
|
||||
"systemInstruction",
|
||||
"tools",
|
||||
"tool_config",
|
||||
"toolConfig",
|
||||
"generation_config",
|
||||
"generationConfig",
|
||||
}
|
||||
)
|
||||
|
||||
def test_codex_openai_cli_allows_body_rules_on_prompt_fields(self) -> None:
|
||||
class TestPromptFieldMutations:
|
||||
def test_openai_cli_prompt_fields_can_be_updated(self) -> None:
|
||||
body = {
|
||||
"model": "gpt-5-codex",
|
||||
"input": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
protected_keys = get_cache_sensitive_protected_body_keys(
|
||||
"openai:cli",
|
||||
provider_type="codex",
|
||||
)
|
||||
|
||||
result = apply_body_rules(
|
||||
body,
|
||||
@@ -1575,14 +1529,13 @@ class TestProtectedBodyKeys:
|
||||
{"action": "set", "path": "input[0].content", "value": "patched"},
|
||||
{"action": "set", "path": "prompt_cache_key", "value": "blocked"},
|
||||
],
|
||||
protected_keys=protected_keys,
|
||||
)
|
||||
|
||||
assert result["instructions"] == "You are GPT-5."
|
||||
assert result["input"][0]["content"] == "patched"
|
||||
assert "prompt_cache_key" not in result
|
||||
assert result["prompt_cache_key"] == "blocked"
|
||||
|
||||
def test_gemini_camelcase_alias_prompt_fields_are_protected(self) -> None:
|
||||
def test_gemini_camelcase_alias_prompt_fields_can_be_updated(self) -> None:
|
||||
body = {
|
||||
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
|
||||
"systemInstruction": {"parts": [{"text": "system"}]},
|
||||
@@ -1590,7 +1543,6 @@ class TestProtectedBodyKeys:
|
||||
"generationConfig": {"temperature": 0.1},
|
||||
"metadata": {"safe": True},
|
||||
}
|
||||
protected_keys = get_cache_sensitive_protected_body_keys("gemini:chat")
|
||||
|
||||
result = apply_body_rules(
|
||||
body,
|
||||
@@ -1607,24 +1559,26 @@ class TestProtectedBodyKeys:
|
||||
},
|
||||
{"action": "set", "path": "metadata.safe", "value": False},
|
||||
],
|
||||
protected_keys=protected_keys,
|
||||
)
|
||||
|
||||
assert result["contents"] == [{"role": "user", "parts": [{"text": "hi"}]}]
|
||||
assert result["systemInstruction"] == {"parts": [{"text": "system"}]}
|
||||
assert result["toolConfig"] == {"functionCallingConfig": {"mode": "AUTO"}}
|
||||
assert result["generationConfig"] == {"temperature": 0.1}
|
||||
assert "generation_config" not in result
|
||||
assert result["contents"] == [
|
||||
{"role": "user", "parts": [{"text": "preface"}]},
|
||||
{"role": "user", "parts": [{"text": "hi"}]},
|
||||
{"role": "model", "parts": []},
|
||||
]
|
||||
assert result["systemInstruction"] == {"parts": [{"text": "mutated"}]}
|
||||
assert "toolConfig" not in result
|
||||
assert "generationConfig" not in result
|
||||
assert result["generation_config"] == {"temperature": 0.1}
|
||||
assert result["metadata"]["safe"] is False
|
||||
|
||||
def test_protected_prompt_fields_block_all_mutating_actions(self) -> None:
|
||||
def test_openai_prompt_fields_allow_all_mutating_actions(self) -> None:
|
||||
body = {
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [{"name": "ReadFile"}],
|
||||
"tool_choice": {"type": "function", "function": {"name": "ReadFile"}},
|
||||
"metadata": {"safe": True},
|
||||
}
|
||||
protected_keys = get_cache_sensitive_protected_body_keys("openai:chat")
|
||||
|
||||
result = apply_body_rules(
|
||||
body,
|
||||
@@ -1652,11 +1606,13 @@ class TestProtectedBodyKeys:
|
||||
{"action": "name_style", "path": "tools[*].name", "style": "snake_case"},
|
||||
{"action": "set", "path": "metadata.safe", "value": False},
|
||||
],
|
||||
protected_keys=protected_keys,
|
||||
)
|
||||
|
||||
assert result["messages"] == [{"role": "user", "content": "hi"}]
|
||||
assert result["tools"] == [{"name": "ReadFile"}]
|
||||
assert result["tool_choice"] == {"type": "function", "function": {"name": "ReadFile"}}
|
||||
assert "choice" not in result
|
||||
assert result["messages"] == [
|
||||
{"role": "system", "content": "x"},
|
||||
{"role": "assistant", "content": "x"},
|
||||
]
|
||||
assert "tools" not in result
|
||||
assert "tool_choice" not in result
|
||||
assert result["choice"] == {"type": "function", "function": {"name": "ReadFile"}}
|
||||
assert result["metadata"]["safe"] is False
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
|
||||
@@ -30,7 +29,7 @@ def test_validate_test_base_url_trims_whitespace() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claude_check_endpoint_passes_cache_sensitive_protected_keys_to_body_rules(
|
||||
async def test_claude_check_endpoint_passes_original_body_to_body_rules(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.handlers.base import endpoint_checker as endpoint_checker_module
|
||||
@@ -41,11 +40,9 @@ async def test_claude_check_endpoint_passes_cache_sensitive_protected_keys_to_bo
|
||||
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
|
||||
|
||||
@@ -80,15 +77,13 @@ async def test_claude_check_endpoint_passes_cache_sensitive_protected_keys_to_bo
|
||||
body_rules=[{"action": "set", "path": "messages", "value": []}],
|
||||
)
|
||||
|
||||
assert captured["protected_keys"] == get_cache_sensitive_protected_body_keys(
|
||||
ClaudeChatAdapter.FORMAT_ID
|
||||
)
|
||||
assert captured["original_body"] == captured["json_body"]
|
||||
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(
|
||||
async def test_gemini_check_endpoint_passes_original_body_to_body_rules(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from src.api.handlers.base import endpoint_checker as endpoint_checker_module
|
||||
@@ -99,11 +94,9 @@ async def test_gemini_check_endpoint_passes_alias_aware_protected_keys_to_body_r
|
||||
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
|
||||
|
||||
@@ -136,8 +129,6 @@ async def test_gemini_check_endpoint_passes_alias_aware_protected_keys_to_body_r
|
||||
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 captured["original_body"] == captured["json_body"]
|
||||
assert result["status_code"] == 200
|
||||
assert result["json_body"] == captured["json_body"]
|
||||
|
||||
@@ -296,15 +296,12 @@ class TestRequestPayloadSummary:
|
||||
summary = summarize_request_payload_shape(
|
||||
payload,
|
||||
provider_api_format="gemini:chat",
|
||||
protected_body_keys=frozenset({"contents", "toolConfig"}),
|
||||
body_rules=[{"action": "drop", "path": "toolConfig"}],
|
||||
)
|
||||
|
||||
assert summary["format"] == "gemini:chat"
|
||||
assert summary["contents_count"] == 1
|
||||
assert summary["message_count"] is None
|
||||
assert summary["protected_body_keys_enabled"] is True
|
||||
assert summary["protected_body_keys"] == ["contents", "toolConfig"]
|
||||
assert summary["body_rule_count"] == 1
|
||||
assert summary["top_level_keys"] == ["contents"]
|
||||
|
||||
@@ -319,7 +316,6 @@ class TestRequestPayloadSummary:
|
||||
summary = summarize_request_payload_shape(
|
||||
payload,
|
||||
provider_api_format="gemini:chat",
|
||||
protected_body_keys=None,
|
||||
body_rules=None,
|
||||
)
|
||||
|
||||
@@ -330,7 +326,6 @@ class TestRequestPayloadSummary:
|
||||
assert summary["tool_count"] is None
|
||||
assert summary["format"] == "gemini:chat"
|
||||
assert summary["contents_count"] == 1
|
||||
assert summary["protected_body_keys_enabled"] is False
|
||||
assert summary["body_rule_count"] == 0
|
||||
assert summary["top_level_keys"] == [
|
||||
"contents",
|
||||
|
||||
Reference in New Issue
Block a user