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:
fawney19
2026-03-17 01:55:24 +08:00
parent 4ecaefbade
commit d2f1431269
9 changed files with 469 additions and 52 deletions

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