mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor(prompt-cache): 将 prompt_cache_key 生成从 Codex 专用模块提取为通用服务,支持 OpenAI 官方 API 和 Codex 端点
- 新增 prompt_cache.py 统一管理 prompt cache key 的生成逻辑 - 基于 User-Agent 识别客户端家族(openai_python/openai_node/codex_desktop 等),不同客户端生成不同 cache key - 在 chat_handler_base/cli_stream_mixin/cli_sync_mixin 统一调用 maybe_patch_request_with_prompt_cache_key - Codex request_patching 不再负责 prompt cache key 注入,仅保留内部标记清理 - 新增 is_official_openai_api_url 工具函数区分 OpenAI 官方 API 与兼容端点
This commit is contained in:
@@ -9,7 +9,6 @@ import pytest
|
||||
from src.api.handlers.base.request_builder import PassthroughRequestBuilder
|
||||
from src.services.provider.adapters.codex.context import set_codex_request_context
|
||||
from src.services.provider.adapters.codex.request_patching import (
|
||||
build_stable_codex_prompt_cache_key,
|
||||
maybe_patch_request_for_codex,
|
||||
patch_openai_cli_request_for_codex,
|
||||
)
|
||||
@@ -51,35 +50,22 @@ def test_patch_openai_cli_request_for_codex_is_passthrough_except_internal_senti
|
||||
assert out["input"][0]["role"] == "system"
|
||||
|
||||
|
||||
def test_patch_openai_cli_request_for_codex_generates_stable_prompt_cache_key_from_user_api_key_id() -> (
|
||||
None
|
||||
):
|
||||
req = {"model": "gpt-test", "input": []}
|
||||
|
||||
out = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123")
|
||||
|
||||
assert out is not req
|
||||
assert out["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
|
||||
|
||||
|
||||
def test_patch_openai_cli_request_for_codex_preserves_existing_prompt_cache_key() -> None:
|
||||
req = {"model": "gpt-test", "input": [], "prompt_cache_key": "client-cache-key"}
|
||||
|
||||
out = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123")
|
||||
out = patch_openai_cli_request_for_codex(req)
|
||||
|
||||
assert out["prompt_cache_key"] == "client-cache-key"
|
||||
|
||||
|
||||
def test_patch_openai_cli_request_for_codex_ignores_provider_key_and_uses_only_user_api_key_id() -> (
|
||||
None
|
||||
):
|
||||
req = {"model": "gpt-test", "input": []}
|
||||
def test_patch_openai_cli_request_for_codex_does_not_inject_prompt_cache_key() -> None:
|
||||
req = {"model": "gpt-test", "input": [], "_aether_compact": True}
|
||||
|
||||
out_a = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123")
|
||||
out_b = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123")
|
||||
out = patch_openai_cli_request_for_codex(req)
|
||||
|
||||
assert out_a["prompt_cache_key"] == out_b["prompt_cache_key"]
|
||||
assert out_a["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
|
||||
assert out is not req
|
||||
assert "_aether_compact" not in out
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_for_codex_is_noop_for_non_codex() -> None:
|
||||
@@ -113,6 +99,7 @@ def test_maybe_patch_request_for_codex_patches_for_codex_openai_cli() -> None:
|
||||
assert out is not req
|
||||
assert out["store"] is True
|
||||
assert "_aether_compact" not in out
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_for_codex_patches_for_codex_openai_compact() -> None:
|
||||
@@ -126,6 +113,7 @@ def test_maybe_patch_request_for_codex_patches_for_codex_openai_compact() -> Non
|
||||
assert out is not req
|
||||
assert out["store"] is True
|
||||
assert "_aether_compact" not in out
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_openai_cli_normalizer_request_from_internal_codex_variant_preserves_store() -> None:
|
||||
@@ -189,10 +177,10 @@ def test_codex_envelope_wrap_request_injects_stable_prompt_cache_key_from_user_a
|
||||
set_codex_request_context(None)
|
||||
|
||||
assert url_model is None
|
||||
assert out["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_codex_envelope_wrap_request_same_user_different_provider_keys_share_prompt_cache_key() -> (
|
||||
def test_codex_envelope_wrap_request_same_user_different_provider_keys_do_not_mutate_prompt_cache_key() -> (
|
||||
None
|
||||
):
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
@@ -225,8 +213,31 @@ def test_codex_envelope_wrap_request_same_user_different_provider_keys_share_pro
|
||||
finally:
|
||||
set_codex_request_context(None)
|
||||
|
||||
assert out_a["prompt_cache_key"] == out_b["prompt_cache_key"]
|
||||
assert out_a["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123")
|
||||
assert "prompt_cache_key" not in out_a
|
||||
assert "prompt_cache_key" not in out_b
|
||||
|
||||
|
||||
def test_codex_envelope_wrap_request_compact_does_not_inject_prompt_cache_key() -> None:
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
try:
|
||||
codex_oauth_envelope.prepare_context(
|
||||
provider_config=None,
|
||||
key_id="provider-key-123",
|
||||
user_api_key_id="user-key-123",
|
||||
is_stream=False,
|
||||
)
|
||||
out, _ = codex_oauth_envelope.wrap_request(
|
||||
{"model": "gpt-test", "input": [], "_aether_compact": True},
|
||||
model="gpt-test",
|
||||
url_model=None,
|
||||
decrypted_auth_config=None,
|
||||
)
|
||||
finally:
|
||||
set_codex_request_context(None)
|
||||
|
||||
assert "_aether_compact" not in out
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_codex_passthrough_builder_preserves_real_codex_headers() -> None:
|
||||
|
||||
198
tests/services/test_openai_prompt_cache.py
Normal file
198
tests/services/test_openai_prompt_cache.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.services.provider.prompt_cache import (
|
||||
build_stable_codex_prompt_cache_key,
|
||||
build_stable_openai_prompt_cache_key,
|
||||
maybe_patch_request_with_prompt_cache_key,
|
||||
normalize_prompt_cache_client_family,
|
||||
resolve_prompt_cache_client_family,
|
||||
)
|
||||
from src.utils.url_utils import is_official_openai_api_url
|
||||
|
||||
|
||||
def test_is_official_openai_api_url_matches_api_openai() -> None:
|
||||
assert is_official_openai_api_url("https://api.openai.com/v1")
|
||||
assert is_official_openai_api_url("api.openai.com")
|
||||
|
||||
|
||||
def test_is_official_openai_api_url_rejects_compatible_hosts() -> None:
|
||||
assert not is_official_openai_api_url("https://api.deepseek.com/v1")
|
||||
assert not is_official_openai_api_url("https://example.com/openai")
|
||||
|
||||
|
||||
def test_normalize_prompt_cache_client_family_is_version_stable() -> None:
|
||||
assert normalize_prompt_cache_client_family("AsyncOpenAI/Python 2.14.0") == "openai_python"
|
||||
assert normalize_prompt_cache_client_family("AsyncOpenAI/Python 2.15.1") == "openai_python"
|
||||
|
||||
|
||||
def test_resolve_prompt_cache_client_family_defaults_to_generic() -> None:
|
||||
assert resolve_prompt_cache_client_family(None) == "generic"
|
||||
assert resolve_prompt_cache_client_family({"x-test": "1"}) == "generic"
|
||||
|
||||
|
||||
def test_build_stable_openai_prompt_cache_key_changes_by_client_family() -> None:
|
||||
python_key = build_stable_openai_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="openai_python",
|
||||
)
|
||||
node_key = build_stable_openai_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="openai_node",
|
||||
)
|
||||
|
||||
assert python_key
|
||||
assert node_key
|
||||
assert python_key != node_key
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_for_official_chat() -> None:
|
||||
req = {"model": "gpt-5", "messages": [{"role": "user", "content": "hi"}]}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:chat",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "AsyncOpenAI/Python 2.14.0"},
|
||||
)
|
||||
|
||||
assert out is not req
|
||||
assert out["prompt_cache_key"] == build_stable_openai_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="openai_python",
|
||||
)
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_for_official_responses() -> None:
|
||||
req = {"model": "gpt-5", "input": []}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:cli",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "openai-node/4.0.0"},
|
||||
)
|
||||
|
||||
assert out is not req
|
||||
assert out["prompt_cache_key"] == build_stable_openai_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="openai_node",
|
||||
)
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_for_codex_openai_cli() -> None:
|
||||
req = {"model": "gpt-5", "input": []}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:cli",
|
||||
provider_type="codex",
|
||||
base_url="https://chatgpt.com/backend-api/codex",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "Codex Desktop/0.108.0-alpha.12"},
|
||||
)
|
||||
|
||||
assert out is not req
|
||||
assert out["prompt_cache_key"] == build_stable_codex_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="codex_desktop",
|
||||
)
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_skips_official_compact() -> None:
|
||||
req = {"model": "gpt-5", "input": [], "_aether_compact": True}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:compact",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
)
|
||||
|
||||
assert out is req
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_skips_codex_compact_marker() -> None:
|
||||
req = {"model": "gpt-5", "input": [], "_aether_compact": True}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:cli",
|
||||
provider_type="codex",
|
||||
base_url="https://chatgpt.com/backend-api/codex",
|
||||
user_api_key_id="user-key-123",
|
||||
)
|
||||
|
||||
assert out is req
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_preserves_existing_key() -> None:
|
||||
req = {"model": "gpt-5", "input": [], "prompt_cache_key": "client-cache-key"}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:cli",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "openai-node/4.0.0"},
|
||||
)
|
||||
|
||||
assert out is req
|
||||
assert out["prompt_cache_key"] == "client-cache-key"
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_skips_compatible_openai_like_hosts() -> None:
|
||||
req = {"model": "gpt-5", "messages": [{"role": "user", "content": "hi"}]}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:chat",
|
||||
provider_type="custom",
|
||||
base_url="https://api.deepseek.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "AsyncOpenAI/Python 2.14.0"},
|
||||
)
|
||||
|
||||
assert out is req
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_skips_unmatched_provider() -> None:
|
||||
req = {"model": "gpt-5", "input": []}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:video",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
request_headers={"user-agent": "AsyncOpenAI/Python 2.14.0"},
|
||||
)
|
||||
|
||||
assert out is req
|
||||
assert "prompt_cache_key" not in out
|
||||
|
||||
|
||||
def test_maybe_patch_request_with_prompt_cache_key_uses_generic_family_without_user_agent() -> None:
|
||||
req = {"model": "gpt-5", "messages": [{"role": "user", "content": "hi"}]}
|
||||
|
||||
out = maybe_patch_request_with_prompt_cache_key(
|
||||
req,
|
||||
provider_api_format="openai:chat",
|
||||
provider_type="custom",
|
||||
base_url="https://api.openai.com/v1",
|
||||
user_api_key_id="user-key-123",
|
||||
)
|
||||
|
||||
assert out is not req
|
||||
assert out["prompt_cache_key"] == build_stable_openai_prompt_cache_key(
|
||||
"user-key-123",
|
||||
client_family="generic",
|
||||
)
|
||||
Reference in New Issue
Block a user