fix: 修复同族 API 格式转换判断逻辑

- OPENAI 和 OPENAI_CLI 使用不同协议(Chat Completions vs Responses API),需要转换
- CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 格式相同仅认证不同,可透传
- 添加 max_completion_tokens 参数支持(OpenAI 新参数名)
- 移除登录表单冗余的 keyup.enter 事件处理
This commit is contained in:
fawney19
2026-01-27 21:59:12 +08:00
parent 6f340b7b91
commit 3c0bf5fdae
9 changed files with 220 additions and 14 deletions

View File

@@ -2381,8 +2381,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
当 Provider 的 API 格式与客户端请求的 API 格式不同时,需要转换响应。
例如:客户端请求 Claude 格式,但 Provider 返回 OpenAI 格式。
注意:CLAUDE 和 CLAUDE_CLI、OPENAI 和 OPENAI_CLI 等同族格式在响应层面是兼容的,
因此使用 get_base_format 比较基础格式(去除 _CLI 后缀)。
注意:
- CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI格式相同只是认证不同可透传
- OPENAI 和 OPENAI_CLI格式不同Chat Completions vs Responses API需要转换
"""
from src.core.api_format.utils import get_base_format
@@ -2393,17 +2394,39 @@ class CliMessageHandlerBase(BaseMessageHandler):
)
return False
# 比较基础格式CLAUDE_CLI -> CLAUDE, OPENAI_CLI -> OPENAI
provider_base = get_base_format(ctx.provider_api_format)
client_base = get_base_format(ctx.client_api_format)
result = provider_base != client_base
provider_format = str(ctx.provider_api_format).upper()
client_format = str(ctx.client_api_format).upper()
# 1. 格式完全匹配 -> 不需要转换
if provider_format == client_format:
logger.debug(
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
f"provider={provider_format}, client={client_format} -> False (exact match)"
)
return False
# 2. 同族格式检查
provider_base = get_base_format(provider_format)
client_base = get_base_format(client_format)
if provider_base == client_base:
# OPENAI 和 OPENAI_CLI 的请求/响应格式不同,需要转换
# CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI 格式相同,可透传
result = provider_base == "OPENAI"
logger.debug(
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
f"provider={provider_format}(base={provider_base}), "
f"client={client_format}(base={client_base}) -> {result} (same family, OPENAI needs conversion)"
)
return result
# 3. 跨格式 -> 需要转换
logger.debug(
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
f"provider={ctx.provider_api_format}(base={provider_base}), "
f"client={ctx.client_api_format}(base={client_base}) -> {result}"
f"provider={provider_format}(base={provider_base}), "
f"client={client_format}(base={client_base}) -> True (cross-format)"
)
return result
return True
def _mark_first_output(self, ctx: StreamContext, state: Dict[str, bool]) -> None:
"""

View File

@@ -59,12 +59,30 @@ def is_format_compatible(
if provider_format == client_format_upper:
return True, False, None
# 2. 同族格式匹配CLAUDE 和 CLAUDE_CLI、OPENAI 和 OPENAI_CLI 等)
# 这些格式在响应层面是兼容的,只是认证方式不同
# 2. 同族格式检查
provider_base = get_base_format(provider_format)
client_base = get_base_format(client_format_upper)
if provider_base == client_base:
return True, False, None
# 同族格式:检查是否需要转换
# - OPENAI 和 OPENAI_CLI 的请求/响应格式不同Chat Completions vs Responses API需要转换
# - CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI 格式相同,只是认证不同,可透传
if provider_base == "OPENAI":
# OPENAI/OPENAI_CLI 同族转换:兼容但需要转换
# 检查转换器能力(同族转换不需要全局开关和端点配置)
if registry.can_convert_full(
client_format_upper, provider_format, require_stream=is_stream
):
return True, True, None # 兼容,需要转换
else:
return (
False,
False,
f"不存在 {client_format} <-> {provider_format} 的完整转换器",
)
else:
# CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 等:格式相同,可透传
return True, False, None
# 3. 检查全局开关
if not global_conversion_enabled:

View File

@@ -3,6 +3,8 @@ Claude CLI Normalizer
CLAUDE_CLI 的请求/响应 body 与 CLAUDE 一致Anthropic Messages API差异主要在认证头。
因此这里复用 ClaudeNormalizer 的转换逻辑,仅更换 FORMAT_ID。
如需 CLI 特殊处理,可覆盖 request_from_internal / request_to_internal 等方法。
"""
from __future__ import annotations

View File

@@ -3,6 +3,8 @@ Gemini CLI Normalizer
GEMINI_CLI 的请求/响应 body 与 GEMINI 一致Google Gemini API差异主要在鉴权/UA 等请求层。
因此这里复用 GeminiNormalizer 的转换逻辑,仅更换 FORMAT_ID。
如需 CLI 特殊处理,可覆盖 request_from_internal / request_to_internal 等方法。
"""
from __future__ import annotations

View File

@@ -138,12 +138,18 @@ class OpenAINormalizer(FormatNormalizer):
stop_sequences = self._coerce_str_list(request.get("stop"))
# 兼容新旧参数名:优先使用 max_completion_tokens回退到 max_tokens
mct = request.get("max_completion_tokens")
max_tokens_value = self._optional_int(
mct if mct is not None else request.get("max_tokens")
)
internal = InternalRequest(
model=model,
messages=messages,
instructions=instructions,
system=system_text,
max_tokens=self._optional_int(request.get("max_tokens")),
max_tokens=max_tokens_value,
temperature=self._optional_float(request.get("temperature")),
top_p=self._optional_float(request.get("top_p")),
stop_sequences=stop_sequences,