feat(codex): 拆分openai:compact为独立端点,简化Codex请求为透传模式

- 新增openai:compact端点类型(EndpointKind.COMPACT),独立于openai:cli
- OpenAICompactAdapter继承OpenAICliAdapter,自动标记compact模式
- Codex请求补丁改为纯透传:仅清理内部标记,不再修改客户端payload
- stream_policy支持openai:compact独立策略,compact端点移除stream字段
- candidate_builder支持compact回退到cli端点
- auth_type: vertex_ai重命名为service_account,保持向后兼容
- Vertex Provider新增api_formats与auth_type组合校验
- KeyAllowedModels对话框改为从Provider获取模型,展示provider_model_name
- Dialog内Select组件自动禁用Portal,修复层级遮挡问题
- 新增Codex compact端点回填迁移脚本
This commit is contained in:
fawney19
2026-03-01 23:55:26 +08:00
parent 4bf3a453e7
commit 97d42703da
37 changed files with 650 additions and 286 deletions

View File

@@ -199,24 +199,18 @@ class OpenAICliNormalizer(FormatNormalizer):
return internal
# Codex 需要的 include 项
_CODEX_REQUIRED_INCLUDE = "reasoning.encrypted_content"
def request_from_internal(
self,
internal: InternalRequest,
*,
target_variant: str | None = None,
) -> dict[str, Any]:
_ = target_variant
openai_cli_extra = internal.extra.get("openai_cli", {})
is_compact = bool(openai_cli_extra.get("_aether_compact"))
is_codex = str(target_variant or "").lower() == "codex" and not is_compact
result: dict[str, Any] = {
"model": internal.model,
"input": self._internal_messages_to_input(
internal.messages, system_to_developer=is_codex
),
"input": self._internal_messages_to_input(internal.messages, system_to_developer=False),
}
# 合并 instructions如果没有则使用 system
@@ -229,20 +223,17 @@ class OpenAICliNormalizer(FormatNormalizer):
# 统一添加该字段以确保兼容性
result["instructions"] = instructions_text or ""
# max_output_tokens/temperature/top_p: Codex 不支持,标准 API 可选
if not is_codex:
if internal.max_tokens is not None:
# Responses API 使用 max_output_tokens
result["max_output_tokens"] = internal.max_tokens
if internal.temperature is not None:
result["temperature"] = internal.temperature
if internal.top_p is not None:
result["top_p"] = internal.top_p
if internal.max_tokens is not None:
# Responses API 使用 max_output_tokens
result["max_output_tokens"] = internal.max_tokens
if internal.temperature is not None:
result["temperature"] = internal.temperature
if internal.top_p is not None:
result["top_p"] = internal.top_p
if internal.stop_sequences:
result["stop"] = list(internal.stop_sequences)
# Codex 强制要求 stream=true其他情况尊重客户端请求
result["stream"] = True if is_codex else bool(internal.stream)
result["stream"] = bool(internal.stream)
if internal.tools:
# Responses API 使用扁平结构: {type, name, description, parameters}
@@ -308,26 +299,10 @@ class OpenAICliNormalizer(FormatNormalizer):
if key not in handled_keys and key not in result:
result[key] = value
# 统一设置 store=falseCodex 强制要求,标准 API 兼容)
# 标准 Responses API 默认设置 store=false
if "store" not in result:
result["store"] = False
# Codex 特定设置(覆盖/删除不支持的字段)
if is_codex:
result["parallel_tool_calls"] = True
# 和 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)
return result
# =========================

View File

@@ -62,6 +62,10 @@ def _detect_data_format(
return EndpointSignature(api_family=ApiFamily.CLAUDE, endpoint_kind=EndpointKind.CLI)
return EndpointSignature(api_family=ApiFamily.CLAUDE, endpoint_kind=EndpointKind.CHAT)
# OpenAI compact: /responses/compact
if "/responses/compact" in normalized:
return EndpointSignature(api_family=ApiFamily.OPENAI, endpoint_kind=EndpointKind.COMPACT)
# OpenAI CLI: /responses
if "/responses" in normalized:
return EndpointSignature(api_family=ApiFamily.OPENAI, endpoint_kind=EndpointKind.CLI)

View File

@@ -36,6 +36,7 @@ class EndpointKind(str, Enum):
CHAT = "chat"
CLI = "cli"
COMPACT = "compact"
VIDEO = "video"
IMAGE = "image"

View File

@@ -123,6 +123,19 @@ _ENDPOINT_DEFINITIONS: dict[tuple[ApiFamily, EndpointKind], EndpointDefinition]
protected_keys=frozenset({"authorization", "content-type"}),
data_format_id="openai_responses",
),
(ApiFamily.OPENAI, EndpointKind.COMPACT): EndpointDefinition(
api_family=ApiFamily.OPENAI,
endpoint_kind=EndpointKind.COMPACT,
aliases=("openai_compact", "responses_compact"),
default_path="/v1/responses/compact",
auth_method=AuthMethod.BEARER,
auth_header="Authorization",
auth_type="bearer",
protected_keys=frozenset({"authorization", "content-type"}),
# compact endpoint is non-streaming by design.
stream_in_body=False,
data_format_id="openai_responses",
),
(ApiFamily.OPENAI, EndpointKind.VIDEO): EndpointDefinition(
api_family=ApiFamily.OPENAI,
endpoint_kind=EndpointKind.VIDEO,