mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(codex): 支持compact端点非流式请求,更新请求头与字段清理
- Codex适配器支持compact端点:非流式请求使用application/json Accept头, stream_policy根据compact上下文返回FORCE_NON_STREAM - 更新Codex请求头格式:添加Version/Connection头,header key首字母大写 - 简化include列表处理:normalizer和request_patching统一强制为固定列表 - 清理Codex不支持的字段:truncation、context_management、user - 默认instructions改为空字符串 - 前端用量页面:用户页面使用前端筛选后总数,避免不必要的后端分页请求
This commit is contained in:
@@ -315,18 +315,16 @@ class OpenAICliNormalizer(FormatNormalizer):
|
||||
# Codex 特定设置(覆盖/删除不支持的字段)
|
||||
if is_codex:
|
||||
result["parallel_tool_calls"] = True
|
||||
# 添加 reasoning.encrypted_content 到 include
|
||||
include = result.get("include", [])
|
||||
if not isinstance(include, list):
|
||||
include = []
|
||||
if self._CODEX_REQUIRED_INCLUDE not in include:
|
||||
include.append(self._CODEX_REQUIRED_INCLUDE)
|
||||
result["include"] = include
|
||||
# 和 codex passthrough patch 保持一致:固定 include 列表
|
||||
result["include"] = [self._CODEX_REQUIRED_INCLUDE]
|
||||
# 删除 Codex 不支持的字段
|
||||
for key in (
|
||||
"previous_response_id",
|
||||
"service_tier",
|
||||
"max_completion_tokens",
|
||||
"truncation",
|
||||
"context_management",
|
||||
"user",
|
||||
):
|
||||
result.pop(key, None)
|
||||
|
||||
|
||||
@@ -30,31 +30,34 @@ class CodexOAuthEnvelope:
|
||||
"""Provider envelope hooks for Codex OAuth upstream."""
|
||||
|
||||
name = "codex:oauth"
|
||||
_CODEX_VERSION = "0.101.0"
|
||||
_CODEX_ORIGINATOR = "codex_cli_rs"
|
||||
|
||||
def extra_headers(self) -> dict[str, str] | None:
|
||||
# These headers are best-effort: Codex upstream is stricter than public OpenAI API.
|
||||
# Keep them provider-scoped (via ProviderEnvelope) to avoid leaking to other upstreams.
|
||||
# Keep these headers provider-scoped to avoid leaking to other upstreams.
|
||||
headers: dict[str, str] = {
|
||||
"OpenAI-Beta": "responses=experimental",
|
||||
# Codex upstream is strict about Content-Type; variants like
|
||||
# "application/json; charset=utf-8" are rejected.
|
||||
"Content-Type": "application/json",
|
||||
"x-oai-web-search-eligible": "true",
|
||||
"session_id": str(uuid.uuid4()),
|
||||
"originator": "codex_cli_rs",
|
||||
# Ensure SSE is returned when upstream is forced to streaming mode.
|
||||
"Accept": "text/event-stream",
|
||||
"Version": self._CODEX_VERSION,
|
||||
"Session_id": str(uuid.uuid4()),
|
||||
"Connection": "Keep-Alive",
|
||||
"Originator": self._CODEX_ORIGINATOR,
|
||||
}
|
||||
|
||||
# Compact endpoint is non-stream; normal responses endpoint expects SSE.
|
||||
ctx = get_codex_request_context()
|
||||
is_compact = bool(ctx.is_compact) if ctx else False
|
||||
headers["Accept"] = "application/json" if is_compact else "text/event-stream"
|
||||
|
||||
ua = str(getattr(config, "internal_user_agent_openai_cli", "") or "").strip()
|
||||
if ua:
|
||||
headers["User-Agent"] = ua
|
||||
|
||||
# Add chatgpt-account-id from context (set by wrap_request).
|
||||
# Context is NOT cleared here — build_codex_url reads is_compact from it later.
|
||||
ctx = get_codex_request_context()
|
||||
if ctx and ctx.account_id:
|
||||
headers["chatgpt-account-id"] = ctx.account_id
|
||||
headers["Chatgpt-Account-Id"] = ctx.account_id
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ round-trip, so every field the client sent is preserved as-is unless explicitly
|
||||
modified here.
|
||||
|
||||
Transformations applied:
|
||||
- Force ``store=false`` (avoid persistence features not supported by some gateways).
|
||||
- Force ``stream=true`` (Codex gateways require streaming).
|
||||
- Force ``store=false``.
|
||||
- Force ``stream=true`` (except compact requests).
|
||||
- Force ``parallel_tool_calls=true``.
|
||||
- Ensure ``instructions`` exists (Codex expects it in some deployments).
|
||||
- Convert ``role=system`` messages to ``role=developer`` (Codex may not accept ``system``).
|
||||
- Ensure ``instructions`` exists (empty string when absent).
|
||||
- Convert ``role=system`` messages to ``role=developer``.
|
||||
- Drop request parameters known to be rejected by Codex gateways.
|
||||
- Ensure ``include`` contains ``"reasoning.encrypted_content"`` for parity with CLI behavior.
|
||||
- Remove ``previous_response_id`` (not supported by Codex gateways).
|
||||
- Force ``include`` to ``["reasoning.encrypted_content"]``.
|
||||
- Drop compatibility-problematic fields (``context_management`` / ``user``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,11 +28,11 @@ _REJECTED_PARAMS: frozenset[str] = frozenset(
|
||||
{
|
||||
"max_output_tokens",
|
||||
"max_completion_tokens",
|
||||
"max_tokens",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"service_tier",
|
||||
"previous_response_id",
|
||||
"truncation",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -53,8 +53,12 @@ def patch_openai_cli_request_for_codex(request_body: dict[str, Any]) -> dict[str
|
||||
# Codex gateways often reject/ignore persistence; be explicit.
|
||||
out["store"] = False
|
||||
|
||||
# Codex gateways require streaming.
|
||||
out["stream"] = True
|
||||
# Codex compact endpoint is non-streaming; normal responses requires stream=true.
|
||||
is_compact = bool(out.pop("_aether_compact", False))
|
||||
if is_compact:
|
||||
out.pop("stream", None)
|
||||
else:
|
||||
out["stream"] = True
|
||||
|
||||
# Codex expects parallel tool calls enabled.
|
||||
out["parallel_tool_calls"] = True
|
||||
@@ -62,7 +66,7 @@ def patch_openai_cli_request_for_codex(request_body: dict[str, Any]) -> dict[str
|
||||
# Ensure instructions exists (some gateways require it even if empty).
|
||||
instructions = out.get("instructions")
|
||||
if not isinstance(instructions, str):
|
||||
out["instructions"] = "You are a helpful coding assistant."
|
||||
out["instructions"] = ""
|
||||
|
||||
# Convert "system" role to "developer" (Codex behavior).
|
||||
input_items = out.get("input")
|
||||
@@ -78,22 +82,12 @@ def patch_openai_cli_request_for_codex(request_body: dict[str, Any]) -> dict[str
|
||||
patched_items.append(item)
|
||||
out["input"] = patched_items
|
||||
|
||||
# Ensure required include item exists.
|
||||
include = out.get("include")
|
||||
if include is None:
|
||||
out["include"] = [_REQUIRED_INCLUDE_ITEM]
|
||||
elif isinstance(include, str):
|
||||
out["include"] = (
|
||||
[include] if include == _REQUIRED_INCLUDE_ITEM else [include, _REQUIRED_INCLUDE_ITEM]
|
||||
)
|
||||
elif isinstance(include, (list, tuple, set)):
|
||||
include_list = list(include)
|
||||
if _REQUIRED_INCLUDE_ITEM not in include_list:
|
||||
include_list.append(_REQUIRED_INCLUDE_ITEM)
|
||||
out["include"] = include_list
|
||||
else:
|
||||
# Unknown type; overwrite to keep behavior deterministic.
|
||||
out["include"] = [_REQUIRED_INCLUDE_ITEM]
|
||||
# Keep codex behavior deterministic: force the exact include list.
|
||||
out["include"] = [_REQUIRED_INCLUDE_ITEM]
|
||||
|
||||
# Codex upstream currently rejects these fields.
|
||||
out.pop("context_management", None)
|
||||
out.pop("user", None)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -60,6 +60,15 @@ def get_upstream_stream_policy(
|
||||
provider_obj = getattr(endpoint, "provider", None)
|
||||
pt = str(provider_type or getattr(provider_obj, "provider_type", "") or "").strip().lower()
|
||||
sig = str(endpoint_sig or getattr(endpoint, "api_format", "") or "").strip().lower()
|
||||
is_codex_compact = False
|
||||
if pt == ProviderType.CODEX and sig == "openai:cli":
|
||||
try:
|
||||
from src.services.provider.adapters.codex.context import get_codex_request_context
|
||||
|
||||
ctx = get_codex_request_context()
|
||||
is_codex_compact = bool(ctx and ctx.is_compact)
|
||||
except Exception:
|
||||
is_codex_compact = False
|
||||
|
||||
# Explicit config wins (unless upstream has a hard constraint).
|
||||
cfg = getattr(endpoint, "config", None)
|
||||
@@ -76,6 +85,7 @@ def get_upstream_stream_policy(
|
||||
pt == ProviderType.CODEX
|
||||
and sig == "openai:cli"
|
||||
and parsed == UpstreamStreamPolicy.FORCE_NON_STREAM
|
||||
and not is_codex_compact
|
||||
):
|
||||
return UpstreamStreamPolicy.FORCE_STREAM
|
||||
if pt == ProviderType.KIRO and parsed == UpstreamStreamPolicy.FORCE_NON_STREAM:
|
||||
@@ -84,7 +94,11 @@ def get_upstream_stream_policy(
|
||||
|
||||
# Safe-by-default: Codex Responses OAuth behaves like SSE-only.
|
||||
if pt == ProviderType.CODEX and sig == "openai:cli":
|
||||
return UpstreamStreamPolicy.FORCE_STREAM
|
||||
return (
|
||||
UpstreamStreamPolicy.FORCE_NON_STREAM
|
||||
if is_codex_compact
|
||||
else UpstreamStreamPolicy.FORCE_STREAM
|
||||
)
|
||||
|
||||
# Kiro upstream streams binary AWS Event Stream; treat as stream-only.
|
||||
if pt == ProviderType.KIRO:
|
||||
|
||||
Reference in New Issue
Block a user