Files
Aether/_deprecated_py_src/services/provider/adapters/codex/plugin.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

372 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Codex provider plugin — 统一注册入口。
将 Codex 对各通用 registry / capability registry 的注册集中在一个文件中:
- Transport Hook (URL 构建)
- Auth Enricher (OAuth enrichment)
- Provider Format Capability默认 body_rules
- Model Fetcher (fixed catalog — Codex has no /v1/models endpoint)
新增 provider 时参照此文件创建对应的 plugin.py 即可。
"""
from __future__ import annotations
import hashlib
from collections.abc import Mapping
from typing import Any
from urllib.parse import urlencode
from src.core.logger import logger
# ---------------------------------------------------------------------------
# Preset model catalog
# ---------------------------------------------------------------------------
# Codex upstream (chatgpt.com/backend-api/codex) has no /v1/models endpoint.
# We use the unified preset models registry from preset_models.py.
from src.services.provider.preset_models import create_preset_models_fetcher
fetch_models_codex = create_preset_models_fetcher("codex")
# ---------------------------------------------------------------------------
# Transport Hook
# ---------------------------------------------------------------------------
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 _build_short_header_id(seed: str) -> str:
return hashlib.sha256(seed.encode()).hexdigest()[:16]
def _normalize_codex_debug_organizations(value: Any) -> list[dict[str, Any]]:
if not isinstance(value, list):
return []
items: list[dict[str, Any]] = []
for raw in value:
if not isinstance(raw, Mapping):
continue
normalized: dict[str, Any] = {}
org_id = str(raw.get("id") or "").strip()
if org_id:
normalized["id"] = org_id
title = str(raw.get("title") or "").strip()
if title:
normalized["title"] = title
role = str(raw.get("role") or "").strip()
if role:
normalized["role"] = role
if "is_default" in raw:
normalized["is_default"] = bool(raw.get("is_default"))
if normalized:
items.append(normalized)
return items
def _build_safe_codex_debug_snapshot(values: Mapping[str, Any] | None) -> dict[str, Any]:
if not isinstance(values, Mapping):
return {}
snapshot: dict[str, Any] = {}
for field in (
"email",
"account_id",
"account_user_id",
"plan_type",
"user_id",
"account_name",
):
raw = values.get(field)
if isinstance(raw, str):
normalized = raw.strip()
if normalized:
snapshot[field] = normalized
elif raw is not None:
snapshot[field] = raw
organizations = _normalize_codex_debug_organizations(values.get("organizations"))
if organizations:
snapshot["organizations"] = organizations
return snapshot
def _build_codex_headers(
request_body: Any,
original_headers: Mapping[str, Any] | None,
*,
include_conversation_id: bool,
decrypted_auth_config: dict[str, Any] | None = None,
) -> dict[str, str]:
"""Codex upstream: chatgpt-account-id + session_id + conversation_id."""
headers: dict[str, str] = {}
if decrypted_auth_config:
account_id = str(decrypted_auth_config.get("account_id") or "").strip()
if account_id:
headers["chatgpt-account-id"] = account_id
if isinstance(request_body, dict):
cache_key = str(request_body.get("prompt_cache_key") or "").strip()
if cache_key:
short_id = _build_short_header_id(cache_key)
if not _get_header_value(original_headers, "session_id"):
headers["session_id"] = short_id
if include_conversation_id and not _get_header_value(
original_headers, "conversation_id"
):
headers["conversation_id"] = short_id
return headers
def build_codex_cli_headers(
request_body: Any,
original_headers: Mapping[str, Any] | None,
*,
decrypted_auth_config: dict[str, Any] | None = None,
) -> dict[str, str]:
from src.services.provider.adapters.codex.context import is_codex_compact_request
return _build_codex_headers(
request_body,
original_headers,
include_conversation_id=not is_codex_compact_request(endpoint_sig="openai:cli"),
decrypted_auth_config=decrypted_auth_config,
)
def build_codex_compact_headers(
request_body: Any,
original_headers: Mapping[str, Any] | None,
*,
decrypted_auth_config: dict[str, Any] | None = None,
) -> dict[str, str]:
return _build_codex_headers(
request_body,
original_headers,
include_conversation_id=False,
decrypted_auth_config=decrypted_auth_config,
)
def build_codex_url(
endpoint: Any,
*,
is_stream: bool,
effective_query_params: dict[str, Any],
**_kwargs: Any,
) -> str:
"""构建 Codex OAuth URL。
Codex upstream (chatgpt.com/backend-api/codex) 使用 /responses
而非标准 OpenAI 的 /v1/responses。compact 模式使用 /responses/compact。
"""
_ = is_stream # Codex 不需要根据 stream 切换路径
endpoint_sig = str(getattr(endpoint, "api_format", "") or "").strip().lower()
from src.services.provider.adapters.codex.context import is_codex_compact_request
is_compact = is_codex_compact_request(endpoint_sig=endpoint_sig)
base = str(endpoint.base_url).rstrip("/")
# 如果用户已在 base_url 中包含了 /responses不要重复追加
if base.endswith("/responses"):
url = f"{base}/compact" if is_compact else base
elif base.endswith("/responses/compact"):
url = base if is_compact else base.removesuffix("/compact")
else:
suffix = "/responses/compact" if is_compact else "/responses"
url = f"{base}{suffix}"
if effective_query_params:
query_string = urlencode(effective_query_params, doseq=True)
if query_string:
url = f"{url}?{query_string}"
return url
# ---------------------------------------------------------------------------
# Auth Enricher
# ---------------------------------------------------------------------------
async def enrich_codex(
auth_config: dict[str, Any],
token_response: dict[str, Any],
access_token: str,
proxy_config: dict[str, Any] | None,
) -> dict[str, Any]:
"""Codex auth_config enrichment: parse token claims -> account/team identity metadata."""
from src.core.provider_oauth_utils import (
fetch_openai_account_name,
parse_codex_id_token,
)
def _read_non_empty_str(*values: Any) -> str | None:
for value in values:
if isinstance(value, str):
normalized = value.strip()
if normalized:
return normalized
return None
# Prefer explicit fields if token endpoint returns them directly.
direct_account_id = _read_non_empty_str(
token_response.get("account_id"),
token_response.get("accountId"),
token_response.get("chatgpt_account_id"),
token_response.get("chatgptAccountId"),
)
if direct_account_id and not auth_config.get("account_id"):
auth_config["account_id"] = direct_account_id
direct_account_user_id = _read_non_empty_str(
token_response.get("account_user_id"),
token_response.get("accountUserId"),
token_response.get("chatgpt_account_user_id"),
token_response.get("chatgptAccountUserId"),
)
if direct_account_user_id and not auth_config.get("account_user_id"):
auth_config["account_user_id"] = direct_account_user_id
direct_plan_type = _read_non_empty_str(
token_response.get("plan_type"),
token_response.get("planType"),
token_response.get("chatgpt_plan_type"),
token_response.get("chatgptPlanType"),
)
if direct_plan_type and not auth_config.get("plan_type"):
auth_config["plan_type"] = direct_plan_type
direct_user_id = _read_non_empty_str(
token_response.get("user_id"),
token_response.get("userId"),
token_response.get("chatgpt_user_id"),
token_response.get("chatgptUserId"),
)
if direct_user_id and not auth_config.get("user_id"):
auth_config["user_id"] = direct_user_id
direct_email = _read_non_empty_str(token_response.get("email"))
if direct_email and not auth_config.get("email"):
auth_config["email"] = direct_email
direct_snapshot = _build_safe_codex_debug_snapshot(
{
"email": direct_email,
"account_id": direct_account_id,
"account_user_id": direct_account_user_id,
"plan_type": direct_plan_type,
"user_id": direct_user_id,
}
)
logger.debug(
"Codex enrich_auth_config: id_token_present={} access_token_present={} token_keys={}",
bool(token_response.get("id_token") or token_response.get("idToken")),
bool(token_response.get("access_token") or token_response.get("accessToken")),
list(token_response.keys()),
)
logger.debug("Codex direct token response values: {}", direct_snapshot)
token_candidates = [
("id_token", token_response.get("id_token")),
("idToken", token_response.get("idToken")),
("access_token", token_response.get("access_token")),
("accessToken", token_response.get("accessToken")),
]
for source_name, token_payload in token_candidates:
codex_info = parse_codex_id_token(token_payload)
if not codex_info:
continue
logger.debug(
"Codex parsed token values: source={} fields={} values={}",
source_name,
list(codex_info.keys()),
_build_safe_codex_debug_snapshot(codex_info),
)
for key, value in codex_info.items():
if not auth_config.get(key):
auth_config[key] = value
account_id = _read_non_empty_str(auth_config.get("account_id"))
if account_id:
account_name = await fetch_openai_account_name(
access_token,
account_id,
proxy_config=proxy_config,
timeout_seconds=10.0,
)
logger.debug(
"Codex account_name lookup: account_id={} resolved_account_name={}",
account_id,
account_name,
)
if account_name:
auth_config["account_name"] = account_name
logger.debug(
"Codex enrich_auth_config final metadata: {}",
_build_safe_codex_debug_snapshot(auth_config),
)
return auth_config
# ---------------------------------------------------------------------------
# Unified Registration
# ---------------------------------------------------------------------------
def register_all() -> None:
"""一次性注册 Codex 的所有 hooks 到各通用 registry。"""
from src.core.api_format.capabilities import register_provider_default_body_rules
from src.core.provider_oauth_utils import register_auth_enricher
from src.services.model.upstream_fetcher import UpstreamModelsFetcherRegistry
from src.services.provider.transport import register_transport_hook
from src.services.provider.upstream_headers import register_upstream_headers_hook
# Transport
register_transport_hook("codex", "openai:cli", build_codex_url)
register_transport_hook("codex", "openai:compact", build_codex_url)
register_upstream_headers_hook("codex", "openai:cli", build_codex_cli_headers)
register_upstream_headers_hook("codex", "openai:compact", build_codex_compact_headers)
# Auth
register_auth_enricher("codex", enrich_codex)
# Provider Format Capability默认 body_rules
from src.core.api_format.metadata import CODEX_DEFAULT_BODY_RULES
register_provider_default_body_rules("codex", "openai:cli", CODEX_DEFAULT_BODY_RULES)
# Export: Codex uses the default export builder (strip null + temp fields)
# No need to register a custom one — the default in export.py suffices.
# Model Fetcher
UpstreamModelsFetcherRegistry.register(
provider_types=["codex"],
fetcher=fetch_models_codex,
)