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:
@@ -172,7 +172,7 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
// 使用 mergeRecordStatus 保护已有的活跃状态,避免轮询更新被覆盖
|
// 使用 mergeRecordStatus 保护已有的活跃状态,避免轮询更新被覆盖
|
||||||
const nextRecords = (userData.records || []) as UsageRecord[]
|
const nextRecords = (userData.records || []) as UsageRecord[]
|
||||||
currentRecords.value = mergeRecordStatus(currentRecords.value, nextRecords)
|
currentRecords.value = mergeRecordStatus(currentRecords.value, nextRecords)
|
||||||
totalRecords.value = currentRecords.value.length
|
totalRecords.value = userData.pagination?.total ?? currentRecords.value.length
|
||||||
|
|
||||||
// 从记录中提取筛选选项和 API 格式统计
|
// 从记录中提取筛选选项和 API 格式统计
|
||||||
const models = new Set<string>()
|
const models = new Set<string>()
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
:available-providers="availableProviders"
|
:available-providers="availableProviders"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:total-records="totalRecords"
|
:total-records="effectiveTotalRecords"
|
||||||
:page-size-options="pageSizeOptions"
|
:page-size-options="pageSizeOptions"
|
||||||
:auto-refresh="globalAutoRefresh"
|
:auto-refresh="globalAutoRefresh"
|
||||||
@update:time-range="handleTimeRangeChange"
|
@update:time-range="handleTimeRangeChange"
|
||||||
@@ -391,7 +391,7 @@ onUnmounted(() => {
|
|||||||
stopGlobalAutoRefresh()
|
stopGlobalAutoRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 用户页面的前端分页
|
// 用户页面的前端分页(后端一次性返回所有记录,前端分页+筛选)
|
||||||
const paginatedRecords = computed(() => {
|
const paginatedRecords = computed(() => {
|
||||||
if (!isAdminPage.value) {
|
if (!isAdminPage.value) {
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
@@ -401,6 +401,14 @@ const paginatedRecords = computed(() => {
|
|||||||
return currentRecords.value
|
return currentRecords.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 用户页面使用前端筛选后的总数,管理员页面使用后端返回的总数
|
||||||
|
const effectiveTotalRecords = computed(() => {
|
||||||
|
if (!isAdminPage.value) {
|
||||||
|
return filteredRecords.value.length
|
||||||
|
}
|
||||||
|
return totalRecords.value
|
||||||
|
})
|
||||||
|
|
||||||
// 显示的记录
|
// 显示的记录
|
||||||
const displayRecords = computed(() => paginatedRecords.value)
|
const displayRecords = computed(() => paginatedRecords.value)
|
||||||
|
|
||||||
@@ -419,20 +427,22 @@ onMounted(async () => {
|
|||||||
const heatmapTask = loadHeatmapData().catch(err => {
|
const heatmapTask = loadHeatmapData().catch(err => {
|
||||||
log.error('加载热力图数据失败:', err)
|
log.error('加载热力图数据失败:', err)
|
||||||
})
|
})
|
||||||
const recordsTask = loadRecords(
|
|
||||||
{ page: currentPage.value, pageSize: pageSize.value },
|
|
||||||
getCurrentFilters()
|
|
||||||
)
|
|
||||||
|
|
||||||
const tasks: Promise<unknown>[] = [statsTask, heatmapTask, recordsTask]
|
const tasks: Promise<unknown>[] = [statsTask, heatmapTask]
|
||||||
|
|
||||||
if (isAdminPage.value) {
|
if (isAdminPage.value) {
|
||||||
|
// 管理员页面:stats 和 records 分开加载(后端分页)
|
||||||
|
tasks.push(loadRecords(
|
||||||
|
{ page: currentPage.value, pageSize: pageSize.value },
|
||||||
|
getCurrentFilters()
|
||||||
|
))
|
||||||
tasks.push(
|
tasks.push(
|
||||||
usersApi.getAllUsers().then(users => {
|
usersApi.getAllUsers().then(users => {
|
||||||
availableUsers.value = users.map(u => ({ id: u.id, username: u.username, email: u.email }))
|
availableUsers.value = users.map(u => ({ id: u.id, username: u.username, email: u.email }))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 用户页面:loadStats 已包含记录加载,不需要单独调用 loadRecords
|
||||||
|
|
||||||
await Promise.allSettled(tasks)
|
await Promise.allSettled(tasks)
|
||||||
})
|
})
|
||||||
@@ -442,21 +452,30 @@ async function handleTimeRangeChange(value: DateRangeParams) {
|
|||||||
timeRange.value = value
|
timeRange.value = value
|
||||||
currentPage.value = 1 // 重置到第一页
|
currentPage.value = 1 // 重置到第一页
|
||||||
await loadStats(timeRange.value)
|
await loadStats(timeRange.value)
|
||||||
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
|
||||||
}
|
}
|
||||||
|
// 用户页面:loadStats 已包含记录加载
|
||||||
|
}
|
||||||
|
|
||||||
// 处理分页变化
|
// 处理分页变化
|
||||||
async function handlePageChange(page: number) {
|
async function handlePageChange(page: number) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page, pageSize: pageSize.value }, getCurrentFilters())
|
await loadRecords({ page, pageSize: pageSize.value }, getCurrentFilters())
|
||||||
}
|
}
|
||||||
|
// 用户页面使用前端分页,无需重新请求
|
||||||
|
}
|
||||||
|
|
||||||
// 处理每页大小变化
|
// 处理每页大小变化
|
||||||
async function handlePageSizeChange(size: number) {
|
async function handlePageSizeChange(size: number) {
|
||||||
pageSize.value = size
|
pageSize.value = size
|
||||||
currentPage.value = 1 // 重置到第一页
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: size }, getCurrentFilters())
|
await loadRecords({ page: 1, pageSize: size }, getCurrentFilters())
|
||||||
}
|
}
|
||||||
|
// 用户页面使用前端分页,无需重新请求
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前筛选参数
|
// 获取当前筛选参数
|
||||||
function getCurrentFilters() {
|
function getCurrentFilters() {
|
||||||
@@ -475,8 +494,12 @@ async function handleFilterSearchChange(value: string) {
|
|||||||
filterSearch.value = value
|
filterSearch.value = value
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
|
||||||
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
|
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters())
|
||||||
}
|
}
|
||||||
|
// 用户页面:search 需要重新从后端拉取数据(后端支持 search 参数)
|
||||||
|
// 但通过 filteredRecords 做前端过滤已覆盖,无需额外请求
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFilterUserChange(value: string) {
|
async function handleFilterUserChange(value: string) {
|
||||||
filterUser.value = value
|
filterUser.value = value
|
||||||
@@ -526,8 +549,11 @@ async function handleFilterStatusChange(value: string) {
|
|||||||
// 刷新数据
|
// 刷新数据
|
||||||
async function refreshData() {
|
async function refreshData() {
|
||||||
await loadStats(timeRange.value)
|
await loadStats(timeRange.value)
|
||||||
|
if (isAdminPage.value) {
|
||||||
await loadRecords({ page: currentPage.value, pageSize: pageSize.value }, getCurrentFilters())
|
await loadRecords({ page: currentPage.value, pageSize: pageSize.value }, getCurrentFilters())
|
||||||
}
|
}
|
||||||
|
// 用户页面:loadStats 已包含记录加载
|
||||||
|
}
|
||||||
|
|
||||||
// 显示请求详情
|
// 显示请求详情
|
||||||
function showRequestDetail(id: string) {
|
function showRequestDetail(id: string) {
|
||||||
|
|||||||
@@ -315,18 +315,16 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
# Codex 特定设置(覆盖/删除不支持的字段)
|
# Codex 特定设置(覆盖/删除不支持的字段)
|
||||||
if is_codex:
|
if is_codex:
|
||||||
result["parallel_tool_calls"] = True
|
result["parallel_tool_calls"] = True
|
||||||
# 添加 reasoning.encrypted_content 到 include
|
# 和 codex passthrough patch 保持一致:固定 include 列表
|
||||||
include = result.get("include", [])
|
result["include"] = [self._CODEX_REQUIRED_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 不支持的字段
|
# 删除 Codex 不支持的字段
|
||||||
for key in (
|
for key in (
|
||||||
"previous_response_id",
|
"previous_response_id",
|
||||||
"service_tier",
|
"service_tier",
|
||||||
"max_completion_tokens",
|
"max_completion_tokens",
|
||||||
|
"truncation",
|
||||||
|
"context_management",
|
||||||
|
"user",
|
||||||
):
|
):
|
||||||
result.pop(key, None)
|
result.pop(key, None)
|
||||||
|
|
||||||
|
|||||||
@@ -30,31 +30,34 @@ class CodexOAuthEnvelope:
|
|||||||
"""Provider envelope hooks for Codex OAuth upstream."""
|
"""Provider envelope hooks for Codex OAuth upstream."""
|
||||||
|
|
||||||
name = "codex:oauth"
|
name = "codex:oauth"
|
||||||
|
_CODEX_VERSION = "0.101.0"
|
||||||
|
_CODEX_ORIGINATOR = "codex_cli_rs"
|
||||||
|
|
||||||
def extra_headers(self) -> dict[str, str] | None:
|
def extra_headers(self) -> dict[str, str] | None:
|
||||||
# These headers are best-effort: Codex upstream is stricter than public OpenAI API.
|
# Keep these headers provider-scoped to avoid leaking to other upstreams.
|
||||||
# Keep them provider-scoped (via ProviderEnvelope) to avoid leaking to other upstreams.
|
|
||||||
headers: dict[str, str] = {
|
headers: dict[str, str] = {
|
||||||
"OpenAI-Beta": "responses=experimental",
|
|
||||||
# Codex upstream is strict about Content-Type; variants like
|
# Codex upstream is strict about Content-Type; variants like
|
||||||
# "application/json; charset=utf-8" are rejected.
|
# "application/json; charset=utf-8" are rejected.
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-oai-web-search-eligible": "true",
|
"Version": self._CODEX_VERSION,
|
||||||
"session_id": str(uuid.uuid4()),
|
"Session_id": str(uuid.uuid4()),
|
||||||
"originator": "codex_cli_rs",
|
"Connection": "Keep-Alive",
|
||||||
# Ensure SSE is returned when upstream is forced to streaming mode.
|
"Originator": self._CODEX_ORIGINATOR,
|
||||||
"Accept": "text/event-stream",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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()
|
ua = str(getattr(config, "internal_user_agent_openai_cli", "") or "").strip()
|
||||||
if ua:
|
if ua:
|
||||||
headers["User-Agent"] = ua
|
headers["User-Agent"] = ua
|
||||||
|
|
||||||
# Add chatgpt-account-id from context (set by wrap_request).
|
# Add chatgpt-account-id from context (set by wrap_request).
|
||||||
# Context is NOT cleared here — build_codex_url reads is_compact from it later.
|
# 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:
|
if ctx and ctx.account_id:
|
||||||
headers["chatgpt-account-id"] = ctx.account_id
|
headers["Chatgpt-Account-Id"] = ctx.account_id
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ round-trip, so every field the client sent is preserved as-is unless explicitly
|
|||||||
modified here.
|
modified here.
|
||||||
|
|
||||||
Transformations applied:
|
Transformations applied:
|
||||||
- Force ``store=false`` (avoid persistence features not supported by some gateways).
|
- Force ``store=false``.
|
||||||
- Force ``stream=true`` (Codex gateways require streaming).
|
- Force ``stream=true`` (except compact requests).
|
||||||
- Force ``parallel_tool_calls=true``.
|
- Force ``parallel_tool_calls=true``.
|
||||||
- Ensure ``instructions`` exists (Codex expects it in some deployments).
|
- Ensure ``instructions`` exists (empty string when absent).
|
||||||
- Convert ``role=system`` messages to ``role=developer`` (Codex may not accept ``system``).
|
- Convert ``role=system`` messages to ``role=developer``.
|
||||||
- Drop request parameters known to be rejected by Codex gateways.
|
- Drop request parameters known to be rejected by Codex gateways.
|
||||||
- Ensure ``include`` contains ``"reasoning.encrypted_content"`` for parity with CLI behavior.
|
- Force ``include`` to ``["reasoning.encrypted_content"]``.
|
||||||
- Remove ``previous_response_id`` (not supported by Codex gateways).
|
- Drop compatibility-problematic fields (``context_management`` / ``user``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,11 +28,11 @@ _REJECTED_PARAMS: frozenset[str] = frozenset(
|
|||||||
{
|
{
|
||||||
"max_output_tokens",
|
"max_output_tokens",
|
||||||
"max_completion_tokens",
|
"max_completion_tokens",
|
||||||
"max_tokens",
|
|
||||||
"temperature",
|
"temperature",
|
||||||
"top_p",
|
"top_p",
|
||||||
"service_tier",
|
"service_tier",
|
||||||
"previous_response_id",
|
"previous_response_id",
|
||||||
|
"truncation",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,7 +53,11 @@ def patch_openai_cli_request_for_codex(request_body: dict[str, Any]) -> dict[str
|
|||||||
# Codex gateways often reject/ignore persistence; be explicit.
|
# Codex gateways often reject/ignore persistence; be explicit.
|
||||||
out["store"] = False
|
out["store"] = False
|
||||||
|
|
||||||
# Codex gateways require streaming.
|
# 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
|
out["stream"] = True
|
||||||
|
|
||||||
# Codex expects parallel tool calls enabled.
|
# Codex expects parallel tool calls enabled.
|
||||||
@@ -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).
|
# Ensure instructions exists (some gateways require it even if empty).
|
||||||
instructions = out.get("instructions")
|
instructions = out.get("instructions")
|
||||||
if not isinstance(instructions, str):
|
if not isinstance(instructions, str):
|
||||||
out["instructions"] = "You are a helpful coding assistant."
|
out["instructions"] = ""
|
||||||
|
|
||||||
# Convert "system" role to "developer" (Codex behavior).
|
# Convert "system" role to "developer" (Codex behavior).
|
||||||
input_items = out.get("input")
|
input_items = out.get("input")
|
||||||
@@ -78,23 +82,13 @@ def patch_openai_cli_request_for_codex(request_body: dict[str, Any]) -> dict[str
|
|||||||
patched_items.append(item)
|
patched_items.append(item)
|
||||||
out["input"] = patched_items
|
out["input"] = patched_items
|
||||||
|
|
||||||
# Ensure required include item exists.
|
# Keep codex behavior deterministic: force the exact include list.
|
||||||
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]
|
out["include"] = [_REQUIRED_INCLUDE_ITEM]
|
||||||
|
|
||||||
|
# Codex upstream currently rejects these fields.
|
||||||
|
out.pop("context_management", None)
|
||||||
|
out.pop("user", None)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ def get_upstream_stream_policy(
|
|||||||
provider_obj = getattr(endpoint, "provider", None)
|
provider_obj = getattr(endpoint, "provider", None)
|
||||||
pt = str(provider_type or getattr(provider_obj, "provider_type", "") or "").strip().lower()
|
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()
|
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).
|
# Explicit config wins (unless upstream has a hard constraint).
|
||||||
cfg = getattr(endpoint, "config", None)
|
cfg = getattr(endpoint, "config", None)
|
||||||
@@ -76,6 +85,7 @@ def get_upstream_stream_policy(
|
|||||||
pt == ProviderType.CODEX
|
pt == ProviderType.CODEX
|
||||||
and sig == "openai:cli"
|
and sig == "openai:cli"
|
||||||
and parsed == UpstreamStreamPolicy.FORCE_NON_STREAM
|
and parsed == UpstreamStreamPolicy.FORCE_NON_STREAM
|
||||||
|
and not is_codex_compact
|
||||||
):
|
):
|
||||||
return UpstreamStreamPolicy.FORCE_STREAM
|
return UpstreamStreamPolicy.FORCE_STREAM
|
||||||
if pt == ProviderType.KIRO and parsed == UpstreamStreamPolicy.FORCE_NON_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.
|
# Safe-by-default: Codex Responses OAuth behaves like SSE-only.
|
||||||
if pt == ProviderType.CODEX and sig == "openai:cli":
|
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.
|
# Kiro upstream streams binary AWS Event Stream; treat as stream-only.
|
||||||
if pt == ProviderType.KIRO:
|
if pt == ProviderType.KIRO:
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ def test_patch_openai_cli_request_for_codex_sets_store_and_instructions() -> Non
|
|||||||
|
|
||||||
assert out is not req
|
assert out is not req
|
||||||
assert out["store"] is False
|
assert out["store"] is False
|
||||||
assert "instructions" in out
|
assert out["stream"] is True
|
||||||
assert isinstance(out["instructions"], str)
|
assert out["instructions"] == ""
|
||||||
|
|
||||||
|
|
||||||
def test_patch_openai_cli_request_for_codex_strips_rejected_params() -> None:
|
def test_patch_openai_cli_request_for_codex_strips_rejected_params() -> None:
|
||||||
@@ -22,20 +22,24 @@ def test_patch_openai_cli_request_for_codex_strips_rejected_params() -> None:
|
|||||||
"input": [],
|
"input": [],
|
||||||
"max_output_tokens": 123,
|
"max_output_tokens": 123,
|
||||||
"max_completion_tokens": 456,
|
"max_completion_tokens": 456,
|
||||||
"max_tokens": 789,
|
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"top_p": 0.9,
|
"top_p": 0.9,
|
||||||
"service_tier": "default",
|
"service_tier": "default",
|
||||||
|
"truncation": "auto",
|
||||||
|
"context_management": {"compaction": {"type": "summary"}},
|
||||||
|
"user": "u_123",
|
||||||
}
|
}
|
||||||
out = patch_openai_cli_request_for_codex(req)
|
out = patch_openai_cli_request_for_codex(req)
|
||||||
|
|
||||||
for key in (
|
for key in (
|
||||||
"max_output_tokens",
|
"max_output_tokens",
|
||||||
"max_completion_tokens",
|
"max_completion_tokens",
|
||||||
"max_tokens",
|
|
||||||
"temperature",
|
"temperature",
|
||||||
"top_p",
|
"top_p",
|
||||||
"service_tier",
|
"service_tier",
|
||||||
|
"truncation",
|
||||||
|
"context_management",
|
||||||
|
"user",
|
||||||
):
|
):
|
||||||
assert key not in out
|
assert key not in out
|
||||||
|
|
||||||
@@ -68,8 +72,28 @@ def test_patch_openai_cli_request_for_codex_adds_required_include_item() -> None
|
|||||||
req = {"model": "gpt-test", "input": []}
|
req = {"model": "gpt-test", "input": []}
|
||||||
out = patch_openai_cli_request_for_codex(req)
|
out = patch_openai_cli_request_for_codex(req)
|
||||||
|
|
||||||
assert "include" in out
|
assert out["include"] == ["reasoning.encrypted_content"]
|
||||||
assert "reasoning.encrypted_content" in out["include"]
|
|
||||||
|
|
||||||
|
def test_patch_openai_cli_request_for_codex_overrides_include() -> None:
|
||||||
|
req = {
|
||||||
|
"model": "gpt-test",
|
||||||
|
"input": [],
|
||||||
|
"include": ["foo", "bar"],
|
||||||
|
}
|
||||||
|
out = patch_openai_cli_request_for_codex(req)
|
||||||
|
assert out["include"] == ["reasoning.encrypted_content"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_openai_cli_request_for_codex_compact_drops_stream() -> None:
|
||||||
|
req = {
|
||||||
|
"model": "gpt-test",
|
||||||
|
"input": [],
|
||||||
|
"_aether_compact": True,
|
||||||
|
"stream": True,
|
||||||
|
}
|
||||||
|
out = patch_openai_cli_request_for_codex(req)
|
||||||
|
assert "stream" 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:
|
||||||
@@ -110,7 +134,34 @@ def test_codex_envelope_extra_headers_includes_sse_accept_and_session() -> None:
|
|||||||
|
|
||||||
headers = codex_oauth_envelope.extra_headers() or {}
|
headers = codex_oauth_envelope.extra_headers() or {}
|
||||||
assert headers.get("Accept") == "text/event-stream"
|
assert headers.get("Accept") == "text/event-stream"
|
||||||
assert headers.get("x-oai-web-search-eligible") == "true"
|
assert headers.get("Originator") == "codex_cli_rs"
|
||||||
assert headers.get("originator") == "codex_cli_rs"
|
assert headers.get("Version") == "0.101.0"
|
||||||
assert isinstance(headers.get("session_id"), str)
|
assert headers.get("Connection") == "Keep-Alive"
|
||||||
assert headers.get("session_id")
|
assert isinstance(headers.get("Session_id"), str)
|
||||||
|
assert headers.get("Session_id")
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_envelope_extra_headers_compact_uses_json_accept() -> None:
|
||||||
|
from src.services.provider.adapters.codex.context import (
|
||||||
|
CodexRequestContext,
|
||||||
|
set_codex_request_context,
|
||||||
|
)
|
||||||
|
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||||
|
|
||||||
|
set_codex_request_context(CodexRequestContext(is_compact=True))
|
||||||
|
headers = codex_oauth_envelope.extra_headers() or {}
|
||||||
|
assert headers.get("Accept") == "application/json"
|
||||||
|
set_codex_request_context(None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_envelope_extra_headers_uses_account_id_header() -> None:
|
||||||
|
from src.services.provider.adapters.codex.context import (
|
||||||
|
CodexRequestContext,
|
||||||
|
set_codex_request_context,
|
||||||
|
)
|
||||||
|
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||||
|
|
||||||
|
set_codex_request_context(CodexRequestContext(account_id="acc_123"))
|
||||||
|
headers = codex_oauth_envelope.extra_headers() or {}
|
||||||
|
assert headers.get("Chatgpt-Account-Id") == "acc_123"
|
||||||
|
set_codex_request_context(None)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from src.services.provider.adapters.codex.context import (
|
||||||
|
CodexRequestContext,
|
||||||
|
set_codex_request_context,
|
||||||
|
)
|
||||||
from src.services.provider.transport import build_provider_url
|
from src.services.provider.transport import build_provider_url
|
||||||
|
|
||||||
|
|
||||||
@@ -44,3 +48,19 @@ def test_codex_openai_cli_does_not_duplicate_responses_suffix() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert url == "https://chatgpt.com/backend-api/codex/responses"
|
assert url == "https://chatgpt.com/backend-api/codex/responses"
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_openai_cli_uses_compact_suffix_when_context_marked_compact() -> None:
|
||||||
|
endpoint = _DummyEndpoint(
|
||||||
|
base_url="https://chatgpt.com/backend-api/codex",
|
||||||
|
api_format="openai:cli",
|
||||||
|
provider=SimpleNamespace(provider_type="codex"),
|
||||||
|
)
|
||||||
|
set_codex_request_context(CodexRequestContext(is_compact=True))
|
||||||
|
url = build_provider_url(
|
||||||
|
endpoint, # type: ignore[arg-type]
|
||||||
|
path_params={"model": "ignored"},
|
||||||
|
is_stream=False,
|
||||||
|
)
|
||||||
|
assert url == "https://chatgpt.com/backend-api/codex/responses/compact"
|
||||||
|
set_codex_request_context(None)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from src.services.provider.adapters.codex.context import (
|
||||||
|
CodexRequestContext,
|
||||||
|
set_codex_request_context,
|
||||||
|
)
|
||||||
from src.services.provider.stream_policy import (
|
from src.services.provider.stream_policy import (
|
||||||
UpstreamStreamPolicy,
|
UpstreamStreamPolicy,
|
||||||
enforce_stream_mode_for_upstream,
|
enforce_stream_mode_for_upstream,
|
||||||
@@ -42,6 +46,19 @@ def test_get_upstream_stream_policy_codex_ignores_force_non_stream_config() -> N
|
|||||||
assert get_upstream_stream_policy(ep) == UpstreamStreamPolicy.FORCE_STREAM
|
assert get_upstream_stream_policy(ep) == UpstreamStreamPolicy.FORCE_STREAM
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_upstream_stream_policy_codex_compact_forces_non_stream() -> None:
|
||||||
|
ep = _DummyEndpoint(
|
||||||
|
api_format="openai:cli",
|
||||||
|
config=None,
|
||||||
|
provider=SimpleNamespace(provider_type="codex"),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
set_codex_request_context(CodexRequestContext(is_compact=True))
|
||||||
|
assert get_upstream_stream_policy(ep) == UpstreamStreamPolicy.FORCE_NON_STREAM
|
||||||
|
finally:
|
||||||
|
set_codex_request_context(None)
|
||||||
|
|
||||||
|
|
||||||
def test_enforce_stream_mode_for_upstream_openai_chat_sets_stream_options_usage() -> None:
|
def test_enforce_stream_mode_for_upstream_openai_chat_sets_stream_options_usage() -> None:
|
||||||
body = {"stream": False}
|
body = {"stream": False}
|
||||||
out = enforce_stream_mode_for_upstream(
|
out = enforce_stream_mode_for_upstream(
|
||||||
|
|||||||
Reference in New Issue
Block a user