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:
fawney19
2026-03-18 10:52:07 +08:00
parent 684689a82b
commit d026398bab
11 changed files with 40 additions and 213 deletions

View File

@@ -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",
)

View File

@@ -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

View File

@@ -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"]

View File

@@ -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",