mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(normalizer): 稳定请求体字段顺序以提升 prompt cache 命中率
在 FormatNormalizer 基类新增 _reorder_request_keys 方法,OpenAI/OpenAI CLI normalizer 各自定义前缀字段顺序(model, tools, messages / model, instructions, tools, input),确保静态字段前置、动态内容后置。同时调整 OpenAI CLI 中 instructions 字段的构建位置使其在 input 之前。
This commit is contained in:
@@ -160,6 +160,21 @@ class FormatNormalizer(ABC):
|
|||||||
def _extract_extra(self, payload: dict[str, Any], known_keys: set[str]) -> dict[str, Any]:
|
def _extract_extra(self, payload: dict[str, Any], known_keys: set[str]) -> dict[str, Any]:
|
||||||
return {k: v for k, v in payload.items() if k not in known_keys}
|
return {k: v for k, v in payload.items() if k not in known_keys}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reorder_request_keys(
|
||||||
|
payload: dict[str, Any],
|
||||||
|
prefix_keys: tuple[str, ...],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""将指定字段前置,其余保持原序,用于稳定请求体前缀以提升 prompt cache 命中率。"""
|
||||||
|
ordered: dict[str, Any] = {}
|
||||||
|
for key in prefix_keys:
|
||||||
|
if key in payload:
|
||||||
|
ordered[key] = payload[key]
|
||||||
|
for key, value in payload.items():
|
||||||
|
if key not in ordered:
|
||||||
|
ordered[key] = value
|
||||||
|
return ordered
|
||||||
|
|
||||||
def _merge_dropped(self, target: dict[str, int], source: dict[str, int]) -> None:
|
def _merge_dropped(self, target: dict[str, int], source: dict[str, int]) -> None:
|
||||||
for k, v in source.items():
|
for k, v in source.items():
|
||||||
target[k] = target.get(k, 0) + int(v)
|
target[k] = target.get(k, 0) + int(v)
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
if key in _CHAT_PASSTHROUGH_KEYS and key not in result and key not in _HANDLED_KEYS:
|
if key in _CHAT_PASSTHROUGH_KEYS and key not in result and key not in _HANDLED_KEYS:
|
||||||
result[key] = value
|
result[key] = value
|
||||||
|
|
||||||
return result
|
return self._reorder_request_prefix_keys(result)
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Responses
|
# Responses
|
||||||
@@ -1401,6 +1401,11 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
joined = "\n\n".join(parts)
|
joined = "\n\n".join(parts)
|
||||||
return joined or None
|
return joined or None
|
||||||
|
|
||||||
|
_REQUEST_PREFIX_KEYS = ("model", "tools", "messages")
|
||||||
|
|
||||||
|
def _reorder_request_prefix_keys(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return self._reorder_request_keys(payload, self._REQUEST_PREFIX_KEYS)
|
||||||
|
|
||||||
def _openai_tools_to_internal(self, tools: Any) -> list[ToolDefinition] | None:
|
def _openai_tools_to_internal(self, tools: Any) -> list[ToolDefinition] | None:
|
||||||
if not tools or not isinstance(tools, list):
|
if not tools or not isinstance(tools, list):
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -262,11 +262,6 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
isinstance(request_flags, dict) and request_flags.get("has_stream")
|
isinstance(request_flags, dict) and request_flags.get("has_stream")
|
||||||
)
|
)
|
||||||
|
|
||||||
result: dict[str, Any] = {
|
|
||||||
"model": internal.model,
|
|
||||||
"input": self._internal_messages_to_input(internal.messages, system_to_developer=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 合并 instructions,如果没有则使用 system。
|
# 合并 instructions,如果没有则使用 system。
|
||||||
# 仅在 internal 中确有内容或原请求显式提供过时输出,
|
# 仅在 internal 中确有内容或原请求显式提供过时输出,
|
||||||
# 让 Codex 默认 body_rules 仍可在字段缺失时注入默认 instructions。
|
# 让 Codex 默认 body_rules 仍可在字段缺失时注入默认 instructions。
|
||||||
@@ -275,8 +270,14 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
if internal.instructions
|
if internal.instructions
|
||||||
else internal.system
|
else internal.system
|
||||||
)
|
)
|
||||||
|
result: dict[str, Any] = {"model": internal.model}
|
||||||
if instructions_text or has_explicit_instructions:
|
if instructions_text or has_explicit_instructions:
|
||||||
result["instructions"] = instructions_text or ""
|
result["instructions"] = instructions_text or ""
|
||||||
|
# Keep the stable system prefix ahead of the typically dynamic input payload.
|
||||||
|
result["input"] = self._internal_messages_to_input(
|
||||||
|
internal.messages,
|
||||||
|
system_to_developer=False,
|
||||||
|
)
|
||||||
|
|
||||||
if internal.max_tokens is not None:
|
if internal.max_tokens is not None:
|
||||||
# Responses API 使用 max_output_tokens
|
# Responses API 使用 max_output_tokens
|
||||||
@@ -402,7 +403,7 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
if is_codex_variant and "store" not in result:
|
if is_codex_variant and "store" not in result:
|
||||||
result["store"] = False
|
result["store"] = False
|
||||||
|
|
||||||
return result
|
return self._reorder_request_prefix_keys(result)
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# Responses
|
# Responses
|
||||||
@@ -2128,6 +2129,11 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
joined = "\n\n".join(parts)
|
joined = "\n\n".join(parts)
|
||||||
return joined or None
|
return joined or None
|
||||||
|
|
||||||
|
_REQUEST_PREFIX_KEYS = ("model", "instructions", "tools", "input")
|
||||||
|
|
||||||
|
def _reorder_request_prefix_keys(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return self._reorder_request_keys(payload, self._REQUEST_PREFIX_KEYS)
|
||||||
|
|
||||||
def _error_type_from_value(self, value: str) -> ErrorType:
|
def _error_type_from_value(self, value: str) -> ErrorType:
|
||||||
for t in ErrorType:
|
for t in ErrorType:
|
||||||
if t.value == value:
|
if t.value == value:
|
||||||
|
|||||||
@@ -54,6 +54,23 @@ def test_openai_cli_request_to_claude() -> None:
|
|||||||
assert claude_req["messages"][0]["content"] == "hi"
|
assert claude_req["messages"][0]["content"] == "hi"
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_request_to_openai_cli_places_instructions_before_input() -> None:
|
||||||
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
|
claude_req = {
|
||||||
|
"model": "claude-3-5-sonnet-latest",
|
||||||
|
"system": "You are precise.",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
openai_cli_req = reg.convert_request(claude_req, "claude:chat", "openai:cli")
|
||||||
|
|
||||||
|
assert openai_cli_req["instructions"] == "You are precise."
|
||||||
|
assert list(openai_cli_req.keys())[:3] == ["model", "instructions", "input"]
|
||||||
|
assert openai_cli_req["input"][0]["role"] == "user"
|
||||||
|
|
||||||
|
|
||||||
def test_claude_response_to_openai_cli() -> None:
|
def test_claude_response_to_openai_cli() -> None:
|
||||||
reg = _make_registry_with_cli()
|
reg = _make_registry_with_cli()
|
||||||
|
|
||||||
@@ -426,6 +443,7 @@ def test_openai_cli_custom_tool_and_choice_convert_to_openai_chat() -> None:
|
|||||||
|
|
||||||
out = reg.convert_request(openai_cli_req, "openai:cli", "openai:chat")
|
out = reg.convert_request(openai_cli_req, "openai:cli", "openai:chat")
|
||||||
|
|
||||||
|
assert list(out.keys())[:3] == ["model", "tools", "messages"]
|
||||||
assert out["tools"] == [
|
assert out["tools"] == [
|
||||||
{
|
{
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
@@ -504,6 +522,7 @@ def test_openai_chat_web_search_options_convert_to_openai_cli_tools() -> None:
|
|||||||
|
|
||||||
out = reg.convert_request(openai_chat_req, "openai:chat", "openai:cli")
|
out = reg.convert_request(openai_chat_req, "openai:chat", "openai:cli")
|
||||||
|
|
||||||
|
assert list(out.keys())[:3] == ["model", "tools", "input"]
|
||||||
assert out["tools"] == [
|
assert out["tools"] == [
|
||||||
{
|
{
|
||||||
"type": "web_search",
|
"type": "web_search",
|
||||||
|
|||||||
Reference in New Issue
Block a user