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

@@ -81,6 +81,9 @@ from src.models.database import (
User, User,
) )
from src.services.provider.behavior import get_provider_behavior from src.services.provider.behavior import get_provider_behavior
from src.services.provider.prompt_cache import (
maybe_patch_request_with_prompt_cache_key,
)
from src.services.provider.stream_policy import ( from src.services.provider.stream_policy import (
enforce_stream_mode_for_upstream, enforce_stream_mode_for_upstream,
get_upstream_stream_policy, get_upstream_stream_policy,
@@ -807,6 +810,15 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
upstream_is_stream=upstream_is_stream, upstream_is_stream=upstream_is_stream,
) )
request_body = maybe_patch_request_with_prompt_cache_key(
request_body,
provider_api_format=str(provider_api_format) if provider_api_format else None,
provider_type=provider_type,
base_url=getattr(endpoint, "base_url", None),
user_api_key_id=str(getattr(self.api_key, "id", "") or ""),
request_headers=original_headers,
)
# 获取 URL 模型名 # 获取 URL 模型名
url_model = self.get_model_for_url(request_body, mapped_model) or model url_model = self.get_model_for_url(request_body, mapped_model) or model

View File

@@ -41,6 +41,9 @@ from src.core.exceptions import (
) )
from src.core.logger import logger from src.core.logger import logger
from src.services.provider.behavior import get_provider_behavior from src.services.provider.behavior import get_provider_behavior
from src.services.provider.prompt_cache import (
maybe_patch_request_with_prompt_cache_key,
)
from src.services.provider.stream_policy import ( from src.services.provider.stream_policy import (
enforce_stream_mode_for_upstream, enforce_stream_mode_for_upstream,
get_upstream_stream_policy, get_upstream_stream_policy,
@@ -398,6 +401,15 @@ class CliStreamMixin:
upstream_is_stream=upstream_is_stream, upstream_is_stream=upstream_is_stream,
) )
request_body = maybe_patch_request_with_prompt_cache_key(
request_body,
provider_api_format=provider_api_format,
provider_type=provider_type,
base_url=getattr(endpoint, "base_url", None),
user_api_key_id=str(getattr(self.api_key, "id", "") or ""),
request_headers=original_headers,
)
# 获取认证信息(处理 Service Account 等异步认证场景) # 获取认证信息(处理 Service Account 等异步认证场景)
auth_info = await get_provider_auth(endpoint, key) auth_info = await get_provider_auth(endpoint, key)

View File

@@ -36,6 +36,9 @@ from src.core.exceptions import (
) )
from src.core.logger import logger from src.core.logger import logger
from src.services.provider.behavior import get_provider_behavior from src.services.provider.behavior import get_provider_behavior
from src.services.provider.prompt_cache import (
maybe_patch_request_with_prompt_cache_key,
)
from src.services.provider.stream_policy import ( from src.services.provider.stream_policy import (
enforce_stream_mode_for_upstream, enforce_stream_mode_for_upstream,
get_upstream_stream_policy, get_upstream_stream_policy,
@@ -219,6 +222,15 @@ class CliSyncMixin:
upstream_is_stream=upstream_is_stream, upstream_is_stream=upstream_is_stream,
) )
request_body = maybe_patch_request_with_prompt_cache_key(
request_body,
provider_api_format=provider_api_format,
provider_type=provider_type,
base_url=getattr(endpoint, "base_url", None),
user_api_key_id=str(getattr(self.api_key, "id", "") or ""),
request_headers=original_headers,
)
# 获取认证信息(处理 Service Account 等异步认证场景) # 获取认证信息(处理 Service Account 等异步认证场景)
auth_info = await get_provider_auth(endpoint, key) auth_info = await get_provider_auth(endpoint, key)

View File

@@ -69,18 +69,14 @@ class CodexOAuthEnvelope:
# Compact sentinel may have been popped earlier by finalize_provider_request; # Compact sentinel may have been popped earlier by finalize_provider_request;
# prefer the pre-set context var (set by adapter), fall back to request body. # prefer the pre-set context var (set by adapter), fall back to request body.
existing_ctx = get_codex_request_context() existing_ctx = get_codex_request_context()
user_api_key_id = existing_ctx.user_api_key_id if existing_ctx else None
is_compact = (existing_ctx.is_compact if existing_ctx else False) or bool( is_compact = (existing_ctx.is_compact if existing_ctx else False) or bool(
request_body.get("_aether_compact", False) request_body.get("_aether_compact", False)
) )
patched_request_body = patch_openai_cli_request_for_codex( patched_request_body = patch_openai_cli_request_for_codex(request_body)
request_body,
user_api_key_id=user_api_key_id,
)
set_codex_request_context( set_codex_request_context(
CodexRequestContext( CodexRequestContext(
account_id=str(account_id) if account_id else None, account_id=str(account_id) if account_id else None,
user_api_key_id=user_api_key_id, user_api_key_id=existing_ctx.user_api_key_id if existing_ctx else None,
is_compact=is_compact, is_compact=is_compact,
) )
) )

View File

@@ -3,30 +3,17 @@
Codex requests are mostly passthrough: Codex requests are mostly passthrough:
- Do not mutate client payload fields unless Codex-specific compatibility requires it. - Do not mutate client payload fields unless Codex-specific compatibility requires it.
- Strip internal sentinel fields that must never reach upstream. - Strip internal sentinel fields that must never reach upstream.
- When the caller's user API key is known and the request did not provide one,
synthesize a stable ``prompt_cache_key`` so prompt caching can be reused.
""" """
from __future__ import annotations from __future__ import annotations
import uuid
from typing import Any from typing import Any
from src.core.provider_types import ProviderType from src.core.provider_types import ProviderType
def build_stable_codex_prompt_cache_key(user_api_key_id: str | None) -> str | None:
"""Build a deterministic Codex prompt cache key from the caller's user API key id."""
normalized = str(user_api_key_id or "").strip()
if not normalized:
return None
return str(uuid.uuid5(uuid.NAMESPACE_OID, f"aether:codex:prompt-cache:user:{normalized}"))
def patch_openai_cli_request_for_codex( def patch_openai_cli_request_for_codex(
request_body: dict[str, Any], request_body: dict[str, Any],
*,
user_api_key_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Patch an OpenAI CLI (Responses API style) request body for Codex gateways. Patch an OpenAI CLI (Responses API style) request body for Codex gateways.
@@ -36,11 +23,6 @@ def patch_openai_cli_request_for_codex(
out: dict[str, Any] = dict(request_body) out: dict[str, Any] = dict(request_body)
# Internal routing marker; never send upstream. # Internal routing marker; never send upstream.
out.pop("_aether_compact", None) out.pop("_aether_compact", None)
prompt_cache_key = str(out.get("prompt_cache_key") or "").strip()
if not prompt_cache_key:
stable_key = build_stable_codex_prompt_cache_key(user_api_key_id)
if stable_key:
out["prompt_cache_key"] = stable_key
return out return out
@@ -49,7 +31,6 @@ def maybe_patch_request_for_codex(
provider_type: str | None, provider_type: str | None,
provider_api_format: str | None, provider_api_format: str | None,
request_body: Any, request_body: Any,
user_api_key_id: str | None = None,
) -> Any: ) -> Any:
""" """
Conditionally patch request body for Codex gateways. Conditionally patch request body for Codex gateways.
@@ -65,11 +46,10 @@ def maybe_patch_request_for_codex(
return request_body return request_body
if not isinstance(request_body, dict): if not isinstance(request_body, dict):
return request_body return request_body
return patch_openai_cli_request_for_codex(request_body, user_api_key_id=user_api_key_id) return patch_openai_cli_request_for_codex(request_body)
__all__ = [ __all__ = [
"build_stable_codex_prompt_cache_key",
"maybe_patch_request_for_codex", "maybe_patch_request_for_codex",
"patch_openai_cli_request_for_codex", "patch_openai_cli_request_for_codex",
] ]

View File

@@ -0,0 +1,183 @@
"""Helpers for synthesizing stable prompt cache keys."""
from __future__ import annotations
import re
import uuid
from collections.abc import Mapping
from typing import Any
from src.core.provider_types import ProviderType, normalize_provider_type
from src.utils.url_utils import is_official_openai_api_url
_OFFICIAL_OPENAI_PROMPT_CACHE_FORMATS: frozenset[str] = frozenset({"openai:chat", "openai:cli"})
_USER_AGENT_CLIENT_FAMILY_PATTERNS: tuple[tuple[tuple[str, ...], str], ...] = (
(("codex desktop",), "codex_desktop"),
(("asyncopenai/python", "openai/python"), "openai_python"),
(("openai-node", "openai/javascript"), "openai_node"),
(("openai-go",), "openai_go"),
(("openai-java",), "openai_java"),
(("openai-ruby",), "openai_ruby"),
(("postmanruntime/",), "postman"),
(("curl/",), "curl"),
(("mozilla/",), "browser"),
)
def _get_header_value(headers: Mapping[str, Any] | None, header_name: str) -> str | None:
if not isinstance(headers, Mapping):
return None
target = str(header_name or "").strip().lower()
if not target:
return None
for name, value in headers.items():
if str(name or "").strip().lower() != target:
continue
normalized = str(value or "").strip()
return normalized or None
return None
def normalize_prompt_cache_client_family(user_agent: str | None) -> str:
"""Reduce a raw User-Agent string to a stable client-family token."""
raw = str(user_agent or "").strip().lower()
if not raw:
return "generic"
for patterns, family in _USER_AGENT_CLIENT_FAMILY_PATTERNS:
if any(pattern in raw for pattern in patterns):
return family
normalized = re.sub(r"[^a-z0-9]+", "_", raw.split()[0].split("/")[0]).strip("_")
return normalized[:48] if normalized else "generic"
def resolve_prompt_cache_client_family(request_headers: Mapping[str, Any] | None) -> str:
"""Best-effort client-family extraction from request headers."""
return normalize_prompt_cache_client_family(_get_header_value(request_headers, "user-agent"))
def _build_stable_prompt_cache_key(
user_api_key_id: str | None,
*,
scope: str,
client_family: str | None = None,
) -> str | None:
normalized = str(user_api_key_id or "").strip()
if not normalized:
return None
family = str(client_family or "").strip().lower() or "generic"
namespace = f"aether:{scope}:prompt-cache:v2:user:{normalized}:client:{family}"
return str(uuid.uuid5(uuid.NAMESPACE_OID, namespace))
def build_stable_openai_prompt_cache_key(
user_api_key_id: str | None,
*,
client_family: str | None = None,
) -> str | None:
"""Build a deterministic official OpenAI prompt cache key from the caller's user API key id."""
return _build_stable_prompt_cache_key(
user_api_key_id,
scope="openai",
client_family=client_family,
)
def build_stable_codex_prompt_cache_key(
user_api_key_id: str | None,
*,
client_family: str | None = None,
) -> str | None:
"""Build a deterministic Codex prompt cache key from the caller's user API key id."""
return _build_stable_prompt_cache_key(
user_api_key_id,
scope="codex",
client_family=client_family,
)
def resolve_prompt_cache_key_scope(
*,
request_body: dict[str, Any] | None = None,
provider_api_format: str | None,
provider_type: str | None = None,
base_url: str | None = None,
) -> str | None:
"""Resolve which prompt cache strategy should be used for this request."""
fmt = str(provider_api_format or "").strip().lower()
if fmt == "openai:compact":
return None
# Belt-and-suspenders: finalize_provider_request 通常已 pop _aether_compact
# 但 openai:compact format 检查在上方已拦截;此处防御非 compact 格式端点意外携带标记。
request = request_body if isinstance(request_body, dict) else {}
if bool(request.get("_aether_compact", False)):
return None
pt = normalize_provider_type(provider_type)
if pt == ProviderType.CODEX.value and fmt == "openai:cli":
return "codex"
if fmt in _OFFICIAL_OPENAI_PROMPT_CACHE_FORMATS and is_official_openai_api_url(base_url):
return "openai"
return None
def maybe_patch_request_with_prompt_cache_key(
request_body: Any,
*,
provider_api_format: str | None,
provider_type: str | None = None,
base_url: str | None = None,
user_api_key_id: str | None = None,
request_headers: Mapping[str, Any] | None = None,
) -> Any:
"""Inject a stable prompt_cache_key when the target upstream supports deterministic reuse."""
if not isinstance(request_body, dict):
return request_body
scope = resolve_prompt_cache_key_scope(
request_body=request_body,
provider_api_format=provider_api_format,
provider_type=provider_type,
base_url=base_url,
)
if not scope:
return request_body
prompt_cache_key = str(request_body.get("prompt_cache_key") or "").strip()
if prompt_cache_key:
return request_body
client_family = resolve_prompt_cache_client_family(request_headers)
if scope == "codex":
stable_key = build_stable_codex_prompt_cache_key(
user_api_key_id,
client_family=client_family,
)
else:
stable_key = build_stable_openai_prompt_cache_key(
user_api_key_id,
client_family=client_family,
)
if not stable_key:
return request_body
out = dict(request_body)
out["prompt_cache_key"] = stable_key
return out
__all__ = [
"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",
"resolve_prompt_cache_key_scope",
]

View File

@@ -6,6 +6,19 @@ URL 处理工具函数
from __future__ import annotations from __future__ import annotations
from urllib.parse import urlparse
def is_official_openai_api_url(base_url: str | None) -> bool:
"""判断是否为 OpenAI 官方 API 端点。"""
value = str(base_url or "").strip()
if not value:
return False
parsed = urlparse(value if "://" in value else f"https://{value}")
host = str(parsed.hostname or "").strip().lower()
return host == "api.openai.com"
def is_codex_url(base_url: str) -> bool: def is_codex_url(base_url: str) -> bool:
"""判断是否是 Codex OAuth 端点。 """判断是否是 Codex OAuth 端点。

View File

@@ -9,7 +9,6 @@ import pytest
from src.api.handlers.base.request_builder import PassthroughRequestBuilder 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.context import set_codex_request_context
from src.services.provider.adapters.codex.request_patching import ( from src.services.provider.adapters.codex.request_patching import (
build_stable_codex_prompt_cache_key,
maybe_patch_request_for_codex, maybe_patch_request_for_codex,
patch_openai_cli_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" 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: 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"} 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" 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() -> ( def test_patch_openai_cli_request_for_codex_does_not_inject_prompt_cache_key() -> None:
None req = {"model": "gpt-test", "input": [], "_aether_compact": True}
):
req = {"model": "gpt-test", "input": []}
out_a = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123") out = patch_openai_cli_request_for_codex(req)
out_b = patch_openai_cli_request_for_codex(req, user_api_key_id="user-key-123")
assert out_a["prompt_cache_key"] == out_b["prompt_cache_key"] assert out is not req
assert out_a["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123") 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: 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 is not req
assert out["store"] is True assert out["store"] is True
assert "_aether_compact" not in out 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: 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 is not req
assert out["store"] is True assert out["store"] is True
assert "_aether_compact" not in out 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: 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) set_codex_request_context(None)
assert url_model is 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 None
): ):
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope 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: finally:
set_codex_request_context(None) set_codex_request_context(None)
assert out_a["prompt_cache_key"] == out_b["prompt_cache_key"] assert "prompt_cache_key" not in out_a
assert out_a["prompt_cache_key"] == build_stable_codex_prompt_cache_key("user-key-123") 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: def test_codex_passthrough_builder_preserves_real_codex_headers() -> None:

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