mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
183
src/services/provider/prompt_cache.py
Normal file
183
src/services/provider/prompt_cache.py
Normal 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",
|
||||||
|
]
|
||||||
@@ -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 端点。
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
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