mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(body-rules): 支持按 provider_type 覆盖 cache-sensitive 保护字段集合
Codex 通过 OpenAI CLI/Compact 格式转发时,endpoint body_rules 需要能 修改 instructions/input/tools 等 prompt 字段。新增 provider_type 维度的 保护集合映射,Codex 场景仅保护 prompt_cache_key,其余 prompt 字段交由 body_rules 自由调整。
This commit is contained in:
@@ -846,7 +846,10 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
mapped_model=mapped_model,
|
mapped_model=mapped_model,
|
||||||
envelope=envelope,
|
envelope=envelope,
|
||||||
extra_headers=extra_headers,
|
extra_headers=extra_headers,
|
||||||
protected_body_keys=get_cache_sensitive_protected_body_keys(provider_api_format),
|
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||||
|
provider_api_format,
|
||||||
|
provider_type=provider_type,
|
||||||
|
),
|
||||||
upstream_is_stream=upstream_is_stream,
|
upstream_is_stream=upstream_is_stream,
|
||||||
needs_conversion=needs_conversion,
|
needs_conversion=needs_conversion,
|
||||||
provider_api_format=provider_api_format,
|
provider_api_format=provider_api_format,
|
||||||
|
|||||||
@@ -442,7 +442,10 @@ class CliStreamMixin:
|
|||||||
extra_headers=extra_headers if extra_headers else None,
|
extra_headers=extra_headers if extra_headers else None,
|
||||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||||
envelope=envelope,
|
envelope=envelope,
|
||||||
protected_body_keys=get_cache_sensitive_protected_body_keys(provider_api_format),
|
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||||
|
provider_api_format,
|
||||||
|
provider_type=provider_type,
|
||||||
|
),
|
||||||
provider_api_format=provider_api_format,
|
provider_api_format=provider_api_format,
|
||||||
)
|
)
|
||||||
if upstream_is_stream:
|
if upstream_is_stream:
|
||||||
|
|||||||
@@ -263,7 +263,10 @@ class CliSyncMixin:
|
|||||||
extra_headers=extra_headers if extra_headers else None,
|
extra_headers=extra_headers if extra_headers else None,
|
||||||
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
pre_computed_auth=auth_info.as_tuple() if auth_info else None,
|
||||||
envelope=envelope,
|
envelope=envelope,
|
||||||
protected_body_keys=get_cache_sensitive_protected_body_keys(provider_api_format),
|
protected_body_keys=get_cache_sensitive_protected_body_keys(
|
||||||
|
provider_api_format,
|
||||||
|
provider_type=provider_type,
|
||||||
|
),
|
||||||
provider_api_format=provider_api_format,
|
provider_api_format=provider_api_format,
|
||||||
)
|
)
|
||||||
if upstream_is_stream:
|
if upstream_is_stream:
|
||||||
|
|||||||
@@ -514,7 +514,10 @@ class HandlerAdapterBase(ApiAdapter):
|
|||||||
body = apply_body_rules(
|
body = apply_body_rules(
|
||||||
body,
|
body,
|
||||||
body_rules,
|
body_rules,
|
||||||
protected_keys=get_cache_sensitive_protected_body_keys(cls.FORMAT_ID),
|
protected_keys=get_cache_sensitive_protected_body_keys(
|
||||||
|
cls.FORMAT_ID,
|
||||||
|
provider_type=provider_type,
|
||||||
|
),
|
||||||
original_body=body,
|
original_body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -129,11 +129,27 @@ _CACHE_SENSITIVE_BODY_FIELDS_BY_FORMAT: dict[str, frozenset[str]] = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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) -> frozenset[str]:
|
|
||||||
"""根据目标 Provider API 格式返回需要保护的顶层请求字段。"""
|
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()
|
fmt = str(provider_api_format or "").strip().lower()
|
||||||
extra = _CACHE_SENSITIVE_BODY_FIELDS_BY_FORMAT.get(fmt, frozenset())
|
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:
|
if not extra:
|
||||||
return PROTECTED_BODY_FIELDS
|
return PROTECTED_BODY_FIELDS
|
||||||
return frozenset({*PROTECTED_BODY_FIELDS, *extra})
|
return frozenset({*PROTECTED_BODY_FIELDS, *extra})
|
||||||
|
|||||||
@@ -279,6 +279,37 @@ def test_codex_passthrough_builder_preserves_real_codex_headers() -> None:
|
|||||||
assert "x-forwarded-scheme" not in headers
|
assert "x-forwarded-scheme" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
endpoint = SimpleNamespace(
|
||||||
|
api_family="openai",
|
||||||
|
endpoint_kind="cli",
|
||||||
|
header_rules=None,
|
||||||
|
body_rules=list(CODEX_DEFAULT_BODY_RULES),
|
||||||
|
)
|
||||||
|
key = SimpleNamespace(api_key="unused")
|
||||||
|
|
||||||
|
payload, _headers = builder.build(
|
||||||
|
{"model": "gpt-test", "input": []},
|
||||||
|
{"content-type": "application/json"},
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload["instructions"] == "You are GPT-5."
|
||||||
|
assert payload["store"] is False
|
||||||
|
assert "max_output_tokens" not in payload
|
||||||
|
|
||||||
|
|
||||||
def _encode_unsigned_jwt(payload: dict[str, object]) -> str:
|
def _encode_unsigned_jwt(payload: dict[str, object]) -> str:
|
||||||
token = jwt.encode(payload, key="", algorithm="none")
|
token = jwt.encode(payload, key="", algorithm="none")
|
||||||
return token.decode("utf-8") if isinstance(token, bytes) else token
|
return token.decode("utf-8") if isinstance(token, bytes) else token
|
||||||
|
|||||||
@@ -1532,6 +1532,14 @@ class TestProtectedBodyKeys:
|
|||||||
"prompt_cache_key",
|
"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(
|
assert get_cache_sensitive_protected_body_keys("claude:chat") == frozenset(
|
||||||
{"model", "stream", "messages", "system", "tools", "tool_choice"}
|
{"model", "stream", "messages", "system", "tools", "tool_choice"}
|
||||||
)
|
)
|
||||||
@@ -1550,6 +1558,30 @@ class TestProtectedBodyKeys:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_codex_openai_cli_allows_body_rules_on_prompt_fields(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,
|
||||||
|
[
|
||||||
|
{"action": "set", "path": "instructions", "value": "You are GPT-5."},
|
||||||
|
{"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
|
||||||
|
|
||||||
def test_gemini_camelcase_alias_prompt_fields_are_protected(self) -> None:
|
def test_gemini_camelcase_alias_prompt_fields_are_protected(self) -> None:
|
||||||
body = {
|
body = {
|
||||||
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
|
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
|
||||||
|
|||||||
Reference in New Issue
Block a user