fix: 统一格式兼容性检查,透传格式也需开关启用

- 同族格式(如 CLAUDE/CLAUDE_CLI)现在也需要全局开关和端点开关启用才能透传
- 格式转换关闭时,Models API 只返回客户端格式本身
- 添加测试用例验证透传格式受开关限制
This commit is contained in:
fawney19
2026-01-28 13:08:14 +08:00
parent 95fc848e59
commit e7fda0eadd
3 changed files with 60 additions and 40 deletions

View File

@@ -113,13 +113,14 @@ def _get_convertible_formats(client_format: str, global_conversion_enabled: bool
获取客户端格式可转换到的所有目标格式列表 获取客户端格式可转换到的所有目标格式列表
当启用格式转换时,返回所有可以转换的格式; 当启用格式转换时,返回所有可以转换的格式;
否则只返回客户端格式本身。 否则只返回客户端格式本身(不包括同族的其他格式)
""" """
if not global_conversion_enabled:
return _get_formats_for_api(client_format)
client_format_upper = client_format.upper() client_format_upper = client_format.upper()
# 格式转换关闭时,只返回客户端格式本身
if not global_conversion_enabled:
return [client_format_upper]
# 收集所有可转换的格式 # 收集所有可转换的格式
register_default_normalizers() register_default_normalizers()
convertible_formats = [] convertible_formats = []
@@ -137,7 +138,7 @@ def _get_convertible_formats(client_format: str, global_conversion_enabled: bool
): ):
convertible_formats.append(target_format) convertible_formats.append(target_format)
return convertible_formats if convertible_formats else _get_formats_for_api(client_format) return convertible_formats if convertible_formats else [client_format_upper]
def _flatten_provider_formats(provider_to_formats: dict[str, set[str]]) -> list[str]: def _flatten_provider_formats(provider_to_formats: dict[str, set[str]]) -> list[str]:

View File

@@ -5,8 +5,9 @@
转换逻辑: 转换逻辑:
1. 格式完全匹配 -> 透传(无需转换) 1. 格式完全匹配 -> 透传(无需转换)
2. data_format_id 相同 -> 透传(如 CLAUDE/CLAUDE_CLI 数据格式相同) 2. 格式不同 -> 需要检查全局开关 + 端点开关
3. data_format_id 同 -> 需要转换,检查全局开关 + 端点配置 + 转换器能力 - data_format_id 同 -> 可透传(无需数据转换),但需全局开关 + 端点开关
- data_format_id 不同 -> 需要转换,检查全局开关 + 端点配置 + 转换器能力
""" """
from __future__ import annotations from __future__ import annotations
@@ -64,38 +65,20 @@ 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. 检查是否可以透传data_format_id 相同) # 2. 格式不同 -> 需要检查全局格式转换开关
# 例如CLAUDE/CLAUDE_CLI 的 data_format_id 都是 "claude",数据格式相同可透传 # 即使 data_format_id 相同(如 CLAUDE/CLAUDE_CLI也需要全局开关启用
# OPENAI 是 "openai_chat"OPENAI_CLI 是 "openai_responses",需要转换
if can_passthrough(client_format_upper, provider_format):
# 可透传,但仍需检查端点的格式限制配置(如果有)
if endpoint_format_acceptance_config and isinstance(endpoint_format_acceptance_config, dict):
config = endpoint_format_acceptance_config
# 检查 reject_formats即使可透传也应遵守拒绝列表
reject_formats = config.get("reject_formats", [])
if client_format_upper in [f.upper() for f in reject_formats]:
return False, False, f"端点拒绝 {client_format} 格式"
# 检查 accept_formats如果配置了白名单也需在白名单中
accept_formats = config.get("accept_formats", [])
if accept_formats and client_format_upper not in [f.upper() for f in accept_formats]:
return False, False, f"端点不接受 {client_format} 格式"
# 通过检查后,可透传(无需转换)
return True, False, None
# 3. 需要转换的情况data_format_id 不同)
# 检查全局开关(来自环境变量,默认开启)
if not global_conversion_enabled: if not global_conversion_enabled:
return False, False, "全局格式转换未启用(环境变量 FORMAT_CONVERSION_ENABLED=false" return False, False, "全局格式转换未启用(环境变量 FORMAT_CONVERSION_ENABLED=false"
# 4. 检查端点配置(核心控制) # 3. 格式不同时,统一检查端点配置(核心控制)
if endpoint_format_acceptance_config is None: if endpoint_format_acceptance_config is None:
return False, False, "端点未配置格式转换" return False, False, "端点未配置格式接受策略"
config = endpoint_format_acceptance_config config = endpoint_format_acceptance_config
if not isinstance(config, dict): if not isinstance(config, dict):
return False, False, "端点格式配置无效" return False, False, "端点格式配置无效"
if not config.get("enabled", False): if not config.get("enabled", False):
return False, False, "端点格式转换未启用" return False, False, "端点格式接受未启用"
# 检查 reject_formats优先 # 检查 reject_formats优先
reject_formats = config.get("reject_formats", []) reject_formats = config.get("reject_formats", [])
@@ -107,11 +90,19 @@ def is_format_compatible(
if accept_formats and client_format_upper not in [f.upper() for f in accept_formats]: if accept_formats and client_format_upper not in [f.upper() for f in accept_formats]:
return False, False, f"端点不接受 {client_format} 格式" return False, False, f"端点不接受 {client_format} 格式"
# 4. 检查是否可以透传data_format_id 相同)
# 例如CLAUDE/CLAUDE_CLI 的 data_format_id 都是 "claude",数据格式相同可透传
# OPENAI 是 "openai_chat"OPENAI_CLI 是 "openai_responses",需要转换
if can_passthrough(client_format_upper, provider_format):
# data_format_id 相同,可透传(无需数据转换)
return True, False, None
# 5. 需要数据转换的情况data_format_id 不同)
# 检查流式转换 # 检查流式转换
if is_stream and not config.get("stream_conversion", True): if is_stream and not config.get("stream_conversion", True):
return False, False, "端点不支持流式格式转换" return False, False, "端点不支持流式格式转换"
# 5. 检查转换器能力 # 6. 检查转换器能力
if not registry.can_convert_full( if not registry.can_convert_full(
client_format_upper, client_format_upper,
provider_format, provider_format,

View File

@@ -168,13 +168,13 @@ def test_conversion_allowed_when_converter_supports_full() -> None:
def test_claude_cli_to_claude_no_conversion_needed() -> None: def test_claude_cli_to_claude_no_conversion_needed() -> None:
"""CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传""" """CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传(需开关启用)"""
ok, needs_conv, reason = is_format_compatible( ok, needs_conv, reason = is_format_compatible(
"CLAUDE_CLI", "CLAUDE_CLI",
"CLAUDE", "CLAUDE",
endpoint_format_acceptance_config=None, endpoint_format_acceptance_config={"enabled": True},
is_stream=False, is_stream=False,
global_conversion_enabled=False, global_conversion_enabled=True,
registry=MagicMock(), registry=MagicMock(),
) )
assert ok is True assert ok is True
@@ -183,13 +183,13 @@ def test_claude_cli_to_claude_no_conversion_needed() -> None:
def test_claude_to_claude_cli_no_conversion_needed() -> None: def test_claude_to_claude_cli_no_conversion_needed() -> None:
"""CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传""" """CLAUDE 和 CLAUDE_CLI 格式相同,只是认证不同,可透传(需开关启用)"""
ok, needs_conv, reason = is_format_compatible( ok, needs_conv, reason = is_format_compatible(
"CLAUDE", "CLAUDE",
"CLAUDE_CLI", "CLAUDE_CLI",
endpoint_format_acceptance_config=None, endpoint_format_acceptance_config={"enabled": True},
is_stream=False, is_stream=False,
global_conversion_enabled=False, global_conversion_enabled=True,
registry=MagicMock(), registry=MagicMock(),
) )
assert ok is True assert ok is True
@@ -198,13 +198,13 @@ def test_claude_to_claude_cli_no_conversion_needed() -> None:
def test_gemini_cli_to_gemini_no_conversion_needed() -> None: def test_gemini_cli_to_gemini_no_conversion_needed() -> None:
"""GEMINI 和 GEMINI_CLI 格式相同,只是认证不同,可透传""" """GEMINI 和 GEMINI_CLI 格式相同,只是认证不同,可透传(需开关启用)"""
ok, needs_conv, reason = is_format_compatible( ok, needs_conv, reason = is_format_compatible(
"GEMINI_CLI", "GEMINI_CLI",
"GEMINI", "GEMINI",
endpoint_format_acceptance_config=None, endpoint_format_acceptance_config={"enabled": True},
is_stream=False, is_stream=False,
global_conversion_enabled=False, global_conversion_enabled=True,
registry=MagicMock(), registry=MagicMock(),
) )
assert ok is True assert ok is True
@@ -212,6 +212,34 @@ def test_gemini_cli_to_gemini_no_conversion_needed() -> None:
assert reason is None assert reason is None
def test_claude_cli_to_claude_blocked_when_global_switch_disabled() -> None:
"""透传格式CLAUDE_CLI -> CLAUDE也受全局开关限制"""
ok, needs_conv, reason = is_format_compatible(
"CLAUDE_CLI",
"CLAUDE",
endpoint_format_acceptance_config={"enabled": True},
is_stream=False,
global_conversion_enabled=False,
registry=MagicMock(),
)
assert ok is False
assert reason and ("全局" in reason or "FORMAT_CONVERSION_ENABLED" in reason)
def test_claude_cli_to_claude_blocked_when_endpoint_not_configured() -> None:
"""透传格式CLAUDE_CLI -> CLAUDE也需要端点配置"""
ok, needs_conv, reason = is_format_compatible(
"CLAUDE_CLI",
"CLAUDE",
endpoint_format_acceptance_config=None,
is_stream=False,
global_conversion_enabled=True,
registry=MagicMock(),
)
assert ok is False
assert reason and "未配置" in reason
def test_openai_cli_to_openai_needs_conversion() -> None: def test_openai_cli_to_openai_needs_conversion() -> None:
"""OPENAI 和 OPENAI_CLI 格式不同Chat Completions vs Responses API需要转换""" """OPENAI 和 OPENAI_CLI 格式不同Chat Completions vs Responses API需要转换"""
registry = MagicMock() registry = MagicMock()