mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: Codex 请求转换引入 patch_for_variant 快速路径,Kiro 重复账号允许覆盖
- FormatNormalizer 新增 patch_for_variant 可选方法,同格式 + variant 场景 跳过 internal 往返,直接在原始请求体上做最小补丁 - OpenAICliNormalizer 实现 Codex variant 快速路径,registry 优先尝试 - Codex request_patching 补充 stream=true、parallel_tool_calls=true、 移除 previous_response_id - Kiro 重复账号从拒绝改为允许覆盖(用户重新导入同一账号场景) - Kiro Key 命名增加 auth_method 后缀,提取 _build_kiro_key_name 辅助函数 - Kiro token refresh 错误日志从 debug 提升为 warning,响应体截取增至 500 字符
This commit is contained in:
@@ -342,6 +342,21 @@ async def _fetch_kiro_email(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_kiro_key_name(
|
||||||
|
email: str | None,
|
||||||
|
auth_method: str | None,
|
||||||
|
refresh_token: str | None,
|
||||||
|
) -> str:
|
||||||
|
"""根据 email / auth_method / refresh_token 生成 Kiro Key 名称。"""
|
||||||
|
method = auth_method or "social"
|
||||||
|
if not email:
|
||||||
|
token_hash = hashlib.sha256((refresh_token or "").encode()).hexdigest()[:6]
|
||||||
|
base = f"kiro_{token_hash}"
|
||||||
|
else:
|
||||||
|
base = email
|
||||||
|
return f"{base} ({method})"
|
||||||
|
|
||||||
|
|
||||||
def _check_duplicate_oauth_account(
|
def _check_duplicate_oauth_account(
|
||||||
db: Session,
|
db: Session,
|
||||||
provider_id: str,
|
provider_id: str,
|
||||||
@@ -425,17 +440,18 @@ def _check_duplicate_oauth_account(
|
|||||||
)
|
)
|
||||||
return existing_key
|
return existing_key
|
||||||
|
|
||||||
# 活跃的重复账号,拒绝添加
|
# Kiro 类型:活跃的重复账号也允许覆盖(用户可能重新导入同一账号)
|
||||||
is_kiro = new_provider_type == "kiro" or existing_provider_type == "kiro"
|
is_kiro = new_provider_type == "kiro" or existing_provider_type == "kiro"
|
||||||
if is_kiro and new_email:
|
if is_kiro:
|
||||||
auth_method_display = (
|
logger.info(
|
||||||
"Social" if (new_auth_method or "").lower() == "social" else "IdC"
|
"Kiro 重复账号将覆盖更新(key_id={}, name={})",
|
||||||
|
existing_key.id,
|
||||||
|
existing_key.name,
|
||||||
)
|
)
|
||||||
raise InvalidRequestException(
|
return existing_key
|
||||||
f"该 Kiro 账号 ({new_email}, {auth_method_display}) "
|
|
||||||
f"已存在于当前 Provider 中(名称: {existing_key.name})"
|
# 非 Kiro 的活跃重复账号,拒绝添加
|
||||||
)
|
if new_email:
|
||||||
elif new_email:
|
|
||||||
raise InvalidRequestException(
|
raise InvalidRequestException(
|
||||||
f"该 OAuth 账号 ({new_email}) 已存在于当前 Provider 中"
|
f"该 OAuth 账号 ({new_email}) 已存在于当前 Provider 中"
|
||||||
f"(名称: {existing_key.name})"
|
f"(名称: {existing_key.name})"
|
||||||
@@ -1337,8 +1353,8 @@ async def import_refresh_token(
|
|||||||
try:
|
try:
|
||||||
access_token, new_cfg = await refresh_access_token(cfg, proxy_config=proxy_config)
|
access_token, new_cfg = await refresh_access_token(cfg, proxy_config=proxy_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Kiro Refresh Token 验证失败: {}", e)
|
logger.warning("Kiro Refresh Token 验证失败: {} | {}", type(e).__name__, e)
|
||||||
raise InvalidRequestException("Kiro Refresh Token 验证失败,请检查凭据是否有效")
|
raise InvalidRequestException(f"Kiro Refresh Token 验证失败: {type(e).__name__}")
|
||||||
|
|
||||||
# 先获取 email,确保重复检查时有 email 可用
|
# 先获取 email,确保重复检查时有 email 可用
|
||||||
email = await _fetch_kiro_email(new_cfg.to_dict(), proxy_config=proxy_config)
|
email = await _fetch_kiro_email(new_cfg.to_dict(), proxy_config=proxy_config)
|
||||||
@@ -1349,10 +1365,10 @@ async def import_refresh_token(
|
|||||||
existing_key = _check_duplicate_oauth_account(db, provider_id, new_cfg.to_dict())
|
existing_key = _check_duplicate_oauth_account(db, provider_id, new_cfg.to_dict())
|
||||||
replaced = False
|
replaced = False
|
||||||
|
|
||||||
# Kiro 确定账号名称(与 Codex/Antigravity 保持一致,使用 email)
|
# Kiro 确定账号名称:email + auth_method 区分不同来源
|
||||||
name = (payload.name or "").strip()
|
name = (payload.name or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
name = email or f"账号_{int(time.time())}"
|
name = _build_kiro_key_name(email, new_cfg.auth_method, new_cfg.refresh_token)
|
||||||
|
|
||||||
if existing_key:
|
if existing_key:
|
||||||
new_key = _update_existing_oauth_key(
|
new_key = _update_existing_oauth_key(
|
||||||
@@ -1904,7 +1920,7 @@ async def _batch_import_kiro_internal(
|
|||||||
name = existing_key.name
|
name = existing_key.name
|
||||||
replaced = True
|
replaced = True
|
||||||
else:
|
else:
|
||||||
name = email or f"账号_{int(time.time())}"
|
name = _build_kiro_key_name(email, new_cfg.auth_method, new_cfg.refresh_token)
|
||||||
new_key = _create_oauth_key(
|
new_key = _create_oauth_key(
|
||||||
db,
|
db,
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
|
|||||||
@@ -48,6 +48,21 @@ class FormatNormalizer(ABC):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# ============ 同格式变体补丁(可选) ============
|
||||||
|
|
||||||
|
def patch_for_variant(
|
||||||
|
self,
|
||||||
|
request: dict[str, Any],
|
||||||
|
variant: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""同格式 + variant 场景下的轻量补丁(跳过 internal 转换)。
|
||||||
|
|
||||||
|
子类可覆盖此方法,对已知 variant 直接在原始请求体上做最小修改。
|
||||||
|
返回 None 表示不支持该 variant 的快速路径,registry 将回退到完整的
|
||||||
|
request_to_internal -> request_from_internal 流程。
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
# ============ 响应转换 ============
|
# ============ 响应转换 ============
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -112,6 +112,20 @@ class OpenAICliNormalizer(FormatNormalizer):
|
|||||||
# Requests
|
# Requests
|
||||||
# =========================
|
# =========================
|
||||||
|
|
||||||
|
def patch_for_variant(
|
||||||
|
self,
|
||||||
|
request: dict[str, Any],
|
||||||
|
variant: str,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Codex 同格式透传:直接在原始请求体上做最小补丁,跳过 internal 转换。"""
|
||||||
|
if variant.lower() != "codex":
|
||||||
|
return None
|
||||||
|
from src.services.provider.adapters.codex.request_patching import (
|
||||||
|
patch_openai_cli_request_for_codex,
|
||||||
|
)
|
||||||
|
|
||||||
|
return patch_openai_cli_request_for_codex(request)
|
||||||
|
|
||||||
def request_to_internal(self, request: dict[str, Any]) -> InternalRequest:
|
def request_to_internal(self, request: dict[str, Any]) -> InternalRequest:
|
||||||
model = str(request.get("model") or "")
|
model = str(request.get("model") or "")
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ class FormatConversionRegistry:
|
|||||||
if str(source_format).upper() == str(target_format).upper() and not target_variant:
|
if str(source_format).upper() == str(target_format).upper() and not target_variant:
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
# 同格式 + variant: 优先尝试轻量补丁(跳过 internal 转换)
|
||||||
|
if str(source_format).upper() == str(target_format).upper() and target_variant:
|
||||||
|
normalizer = self._require_normalizer(source_format)
|
||||||
|
with _track_conversion_metrics(
|
||||||
|
"request_patch", str(source_format).upper(), str(target_format).upper()
|
||||||
|
):
|
||||||
|
patched = normalizer.patch_for_variant(request, target_variant)
|
||||||
|
if patched is not None:
|
||||||
|
return patched
|
||||||
|
|
||||||
src = self._require_normalizer(source_format)
|
src = self._require_normalizer(source_format)
|
||||||
tgt = self._require_normalizer(target_format)
|
tgt = self._require_normalizer(target_format)
|
||||||
|
|
||||||
@@ -137,6 +147,16 @@ class FormatConversionRegistry:
|
|||||||
if str(source_format).upper() == str(target_format).upper() and not target_variant:
|
if str(source_format).upper() == str(target_format).upper() and not target_variant:
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
# 同格式 + variant: 优先尝试轻量补丁(跳过 internal 转换)
|
||||||
|
if str(source_format).upper() == str(target_format).upper() and target_variant:
|
||||||
|
normalizer = self._require_normalizer(source_format)
|
||||||
|
with _track_conversion_metrics(
|
||||||
|
"request_patch", str(source_format).upper(), str(target_format).upper()
|
||||||
|
):
|
||||||
|
patched = normalizer.patch_for_variant(request, target_variant)
|
||||||
|
if patched is not None:
|
||||||
|
return patched
|
||||||
|
|
||||||
src = self._require_normalizer(source_format)
|
src = self._require_normalizer(source_format)
|
||||||
tgt = self._require_normalizer(target_format)
|
tgt = self._require_normalizer(target_format)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
Codex provider request patching helpers (standalone / passthrough path).
|
Codex provider request patching helpers (passthrough path).
|
||||||
|
|
||||||
In the main request pipeline, Codex-specific transformations are handled by the
|
This is the **primary** Codex request transformation used by the normalizer's
|
||||||
``openai:cli`` normalizer with ``target_variant="codex"`` (triggered automatically
|
``patch_for_variant("codex")`` fast path. It applies minimal, non-destructive
|
||||||
via ``register_behavior_variant("codex", same_format=True)``).
|
patches directly on the original request dict -- no internal representation
|
||||||
|
round-trip, so every field the client sent is preserved as-is unless explicitly
|
||||||
This module provides an **equivalent** standalone patcher for contexts where the full
|
modified here.
|
||||||
normalizer pipeline is not used (e.g. external tooling, one-off scripts, or future
|
|
||||||
passthrough-only paths). It is intentionally kept in sync with the normalizer logic.
|
|
||||||
|
|
||||||
Transformations applied:
|
Transformations applied:
|
||||||
- Force ``store=false`` (avoid persistence features not supported by some gateways).
|
- Force ``store=false`` (avoid persistence features not supported by some gateways).
|
||||||
|
- Force ``stream=true`` (Codex gateways require streaming).
|
||||||
|
- Force ``parallel_tool_calls=true``.
|
||||||
- Ensure ``instructions`` exists (Codex expects it in some deployments).
|
- Ensure ``instructions`` exists (Codex expects it in some deployments).
|
||||||
- Convert ``role=system`` messages to ``role=developer`` (Codex may not accept ``system``).
|
- Convert ``role=system`` messages to ``role=developer`` (Codex may not accept ``system``).
|
||||||
- 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.
|
- Ensure ``include`` contains ``"reasoning.encrypted_content"`` for parity with CLI behavior.
|
||||||
|
- Remove ``previous_response_id`` (not supported by Codex gateways).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -31,6 +32,7 @@ _REJECTED_PARAMS: frozenset[str] = frozenset(
|
|||||||
"temperature",
|
"temperature",
|
||||||
"top_p",
|
"top_p",
|
||||||
"service_tier",
|
"service_tier",
|
||||||
|
"previous_response_id",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +53,12 @@ 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.
|
||||||
|
out["stream"] = True
|
||||||
|
|
||||||
|
# Codex expects parallel tool calls enabled.
|
||||||
|
out["parallel_tool_calls"] = True
|
||||||
|
|
||||||
# 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):
|
||||||
|
|||||||
@@ -150,12 +150,13 @@ async def refresh_social_token(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
if resp.status_code < 200 or resp.status_code >= 300:
|
||||||
logger.debug(
|
body_text = (resp.text or "").strip()[:500]
|
||||||
|
logger.warning(
|
||||||
"kiro social refresh error: HTTP {} | {}",
|
"kiro social refresh error: HTTP {} | {}",
|
||||||
resp.status_code,
|
resp.status_code,
|
||||||
(resp.text or "").strip()[:200],
|
body_text,
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"kiro social refresh failed: HTTP {resp.status_code}")
|
raise RuntimeError(f"kiro social refresh failed: HTTP {resp.status_code} | {body_text}")
|
||||||
|
|
||||||
data: dict[str, Any]
|
data: dict[str, Any]
|
||||||
try:
|
try:
|
||||||
@@ -242,12 +243,13 @@ async def refresh_idc_token(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
if resp.status_code < 200 or resp.status_code >= 300:
|
||||||
logger.debug(
|
body_text = (resp.text or "").strip()[:500]
|
||||||
|
logger.warning(
|
||||||
"kiro idc refresh error: HTTP {} | {}",
|
"kiro idc refresh error: HTTP {} | {}",
|
||||||
resp.status_code,
|
resp.status_code,
|
||||||
(resp.text or "").strip()[:200],
|
body_text,
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"kiro idc refresh failed: HTTP {resp.status_code}")
|
raise RuntimeError(f"kiro idc refresh failed: HTTP {resp.status_code} | {body_text}")
|
||||||
|
|
||||||
data: dict[str, Any]
|
data: dict[str, Any]
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user