mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -179,7 +179,6 @@
|
|||||||
required
|
required
|
||||||
placeholder="输入密码"
|
placeholder="输入密码"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2381,8 +2381,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
当 Provider 的 API 格式与客户端请求的 API 格式不同时,需要转换响应。
|
当 Provider 的 API 格式与客户端请求的 API 格式不同时,需要转换响应。
|
||||||
例如:客户端请求 Claude 格式,但 Provider 返回 OpenAI 格式。
|
例如:客户端请求 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
|
from src.core.api_format.utils import get_base_format
|
||||||
|
|
||||||
@@ -2393,17 +2394,39 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 比较基础格式(CLAUDE_CLI -> CLAUDE, OPENAI_CLI -> OPENAI)
|
provider_format = str(ctx.provider_api_format).upper()
|
||||||
provider_base = get_base_format(ctx.provider_api_format)
|
client_format = str(ctx.client_api_format).upper()
|
||||||
client_base = get_base_format(ctx.client_api_format)
|
|
||||||
result = provider_base != client_base
|
|
||||||
|
|
||||||
|
# 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(
|
logger.debug(
|
||||||
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
|
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
|
||||||
f"provider={ctx.provider_api_format}(base={provider_base}), "
|
f"provider={provider_format}(base={provider_base}), "
|
||||||
f"client={ctx.client_api_format}(base={client_base}) -> {result}"
|
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:
|
def _mark_first_output(self, ctx: StreamContext, state: Dict[str, bool]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -59,12 +59,30 @@ def is_format_compatible(
|
|||||||
if provider_format == client_format_upper:
|
if provider_format == client_format_upper:
|
||||||
return True, False, None
|
return True, False, None
|
||||||
|
|
||||||
# 2. 同族格式匹配(CLAUDE 和 CLAUDE_CLI、OPENAI 和 OPENAI_CLI 等)
|
# 2. 同族格式检查
|
||||||
# 这些格式在响应层面是兼容的,只是认证方式不同
|
|
||||||
provider_base = get_base_format(provider_format)
|
provider_base = get_base_format(provider_format)
|
||||||
client_base = get_base_format(client_format_upper)
|
client_base = get_base_format(client_format_upper)
|
||||||
|
|
||||||
if provider_base == client_base:
|
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. 检查全局开关
|
# 3. 检查全局开关
|
||||||
if not global_conversion_enabled:
|
if not global_conversion_enabled:
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ Claude CLI Normalizer
|
|||||||
|
|
||||||
CLAUDE_CLI 的请求/响应 body 与 CLAUDE 一致(Anthropic Messages API),差异主要在认证头。
|
CLAUDE_CLI 的请求/响应 body 与 CLAUDE 一致(Anthropic Messages API),差异主要在认证头。
|
||||||
因此这里复用 ClaudeNormalizer 的转换逻辑,仅更换 FORMAT_ID。
|
因此这里复用 ClaudeNormalizer 的转换逻辑,仅更换 FORMAT_ID。
|
||||||
|
|
||||||
|
如需 CLI 特殊处理,可覆盖 request_from_internal / request_to_internal 等方法。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ Gemini CLI Normalizer
|
|||||||
|
|
||||||
GEMINI_CLI 的请求/响应 body 与 GEMINI 一致(Google Gemini API),差异主要在鉴权/UA 等请求层。
|
GEMINI_CLI 的请求/响应 body 与 GEMINI 一致(Google Gemini API),差异主要在鉴权/UA 等请求层。
|
||||||
因此这里复用 GeminiNormalizer 的转换逻辑,仅更换 FORMAT_ID。
|
因此这里复用 GeminiNormalizer 的转换逻辑,仅更换 FORMAT_ID。
|
||||||
|
|
||||||
|
如需 CLI 特殊处理,可覆盖 request_from_internal / request_to_internal 等方法。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -138,12 +138,18 @@ class OpenAINormalizer(FormatNormalizer):
|
|||||||
|
|
||||||
stop_sequences = self._coerce_str_list(request.get("stop"))
|
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(
|
internal = InternalRequest(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
system=system_text,
|
system=system_text,
|
||||||
max_tokens=self._optional_int(request.get("max_tokens")),
|
max_tokens=max_tokens_value,
|
||||||
temperature=self._optional_float(request.get("temperature")),
|
temperature=self._optional_float(request.get("temperature")),
|
||||||
top_p=self._optional_float(request.get("top_p")),
|
top_p=self._optional_float(request.get("top_p")),
|
||||||
stop_sequences=stop_sequences,
|
stop_sequences=stop_sequences,
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
"B"
|
"B"
|
||||||
],
|
],
|
||||||
"stream": true,
|
"stream": true,
|
||||||
|
"stream_options": {
|
||||||
|
"include_usage": true
|
||||||
|
},
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"tool_choice": "auto",
|
"tool_choice": "auto",
|
||||||
"tools": [
|
"tools": [
|
||||||
|
|||||||
@@ -161,3 +161,123 @@ def test_conversion_allowed_when_converter_supports_full() -> None:
|
|||||||
assert ok is True
|
assert ok is True
|
||||||
assert needs_conv is True
|
assert needs_conv is True
|
||||||
assert reason is None
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 同族格式测试 ====================
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_cli_to_claude_no_conversion_needed() -> None:
|
||||||
|
"""CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传"""
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"CLAUDE_CLI",
|
||||||
|
"CLAUDE",
|
||||||
|
endpoint_format_acceptance_config=None,
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False,
|
||||||
|
registry=MagicMock(),
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_to_claude_cli_no_conversion_needed() -> None:
|
||||||
|
"""CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传"""
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"CLAUDE",
|
||||||
|
"CLAUDE_CLI",
|
||||||
|
endpoint_format_acceptance_config=None,
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False,
|
||||||
|
registry=MagicMock(),
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_gemini_cli_to_gemini_no_conversion_needed() -> None:
|
||||||
|
"""GEMINI 和 GEMINI_CLI 格式相同,只是认证不同,可透传"""
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"GEMINI_CLI",
|
||||||
|
"GEMINI",
|
||||||
|
endpoint_format_acceptance_config=None,
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False,
|
||||||
|
registry=MagicMock(),
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_needs_conversion() -> None:
|
||||||
|
"""OPENAI 和 OPENAI_CLI 格式不同(Chat Completions vs Responses API),需要转换"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config=None, # 同族转换不需要端点配置
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False, # 同族转换不需要全局开关
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is True # 需要转换!
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_to_openai_cli_needs_conversion() -> None:
|
||||||
|
"""OPENAI 和 OPENAI_CLI 格式不同(Chat Completions vs Responses API),需要转换"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI",
|
||||||
|
"OPENAI_CLI",
|
||||||
|
endpoint_format_acceptance_config=None, # 同族转换不需要端点配置
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False, # 同族转换不需要全局开关
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is True # 需要转换!
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_stream_needs_conversion() -> None:
|
||||||
|
"""OPENAI_CLI 流式请求到 OPENAI 也需要转换"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config=None,
|
||||||
|
is_stream=True,
|
||||||
|
global_conversion_enabled=False,
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is True
|
||||||
|
assert needs_conv is True
|
||||||
|
assert reason is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_fails_without_converter() -> None:
|
||||||
|
"""如果转换器不支持,同族转换也会失败"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = False
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config=None,
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False,
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is False
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason and "转换器" in reason
|
||||||
|
|||||||
@@ -81,6 +81,39 @@ def test_openai_request_instructions_roundtrip() -> None:
|
|||||||
assert out_messages[3]["content"] == "ok"
|
assert out_messages[3]["content"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_request_max_completion_tokens_support() -> None:
|
||||||
|
"""测试 max_completion_tokens 参数的兼容性(OpenAI API 新参数名)"""
|
||||||
|
n = OpenAINormalizer()
|
||||||
|
|
||||||
|
# 测试 max_completion_tokens 优先于 max_tokens
|
||||||
|
req_new = {
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_completion_tokens": 100,
|
||||||
|
"max_tokens": 50, # 旧参数应被忽略
|
||||||
|
}
|
||||||
|
internal = n.request_to_internal(req_new)
|
||||||
|
assert internal.max_tokens == 100
|
||||||
|
|
||||||
|
# 测试仅使用 max_completion_tokens
|
||||||
|
req_only_new = {
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_completion_tokens": 200,
|
||||||
|
}
|
||||||
|
internal = n.request_to_internal(req_only_new)
|
||||||
|
assert internal.max_tokens == 200
|
||||||
|
|
||||||
|
# 测试仅使用 max_tokens(向后兼容)
|
||||||
|
req_only_old = {
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 150,
|
||||||
|
}
|
||||||
|
internal = n.request_to_internal(req_only_old)
|
||||||
|
assert internal.max_tokens == 150
|
||||||
|
|
||||||
|
|
||||||
def test_openai_request_tool_calls_and_tool_role_roundtrip() -> None:
|
def test_openai_request_tool_calls_and_tool_role_roundtrip() -> None:
|
||||||
n = OpenAINormalizer()
|
n = OpenAINormalizer()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user