mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 使用 data_format_id 重构格式兼容性判断逻辑
- 在 ApiFormatDefinition 中新增 data_format_id 字段标识数据格式 - 新增 can_passthrough() 函数判断格式间是否可透传 - 重构 compatibility.py 使用 data_format_id 替代 get_base_format() - 使用量记录支持从 endpoint 回填格式信息(兼容历史数据) - 前端优化格式转换显示逻辑,区分跨格式转换和同族格式差异 - 首页 header 响应式布局优化
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
|
||||
转换逻辑:
|
||||
1. 格式完全匹配 -> 透传(无需转换)
|
||||
2. 同族格式透传(CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 格式相同,只是认证不同)
|
||||
3. 需要转换的情况 -> 检查全局开关 + 端点配置 + 转换器能力
|
||||
2. data_format_id 相同 -> 透传(如 CLAUDE/CLAUDE_CLI 数据格式相同)
|
||||
3. data_format_id 不同 -> 需要转换,检查全局开关 + 端点配置 + 转换器能力
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Optional, Tuple
|
||||
if TYPE_CHECKING:
|
||||
from src.core.api_format.conversion.registry import FormatConversionRegistry
|
||||
|
||||
from src.core.api_format.utils import get_base_format
|
||||
from src.core.api_format.metadata import can_passthrough
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,17 +64,25 @@ def is_format_compatible(
|
||||
if provider_format == client_format_upper:
|
||||
return True, False, None
|
||||
|
||||
# 2. 同族格式检查
|
||||
provider_base = get_base_format(provider_format)
|
||||
client_base = get_base_format(client_format_upper)
|
||||
|
||||
is_same_family = provider_base == client_base
|
||||
if is_same_family and provider_base != "OPENAI":
|
||||
# CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 等:格式相同,只是认证不同,可透传
|
||||
# 注意:这些格式之间没有定义转换器,因为数据格式完全相同
|
||||
# 2. 检查是否可以透传(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):
|
||||
# 可透传,但仍需检查端点的格式限制配置(如果有)
|
||||
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. 需要转换的情况(OPENAI/OPENAI_CLI 同族转换 或 跨格式转换)
|
||||
# 3. 需要转换的情况(data_format_id 不同)
|
||||
# 检查全局开关(来自环境变量,默认开启)
|
||||
if not global_conversion_enabled:
|
||||
return False, False, "全局格式转换未启用(环境变量 FORMAT_CONVERSION_ENABLED=false)"
|
||||
|
||||
@@ -38,6 +38,9 @@ class ApiFormatDefinition:
|
||||
- protected_keys: 不应被 extra_headers 覆盖的头部(小写)
|
||||
- model_in_body: 是否需要在请求体中包含 model 字段(Gemini 等格式通过 URL 传递模型名)
|
||||
- stream_in_body: 是否需要在请求体中包含 stream 字段(Gemini 等格式通过 URL 端点区分流式)
|
||||
- data_format_id: 数据格式标识,相同 ID 的格式数据结构相同可以透传,不同则需要转换
|
||||
例如:CLAUDE/CLAUDE_CLI 都是 "claude",可以透传
|
||||
OPENAI 是 "openai_chat",OPENAI_CLI 是 "openai_responses",需要转换
|
||||
"""
|
||||
|
||||
api_format: APIFormat
|
||||
@@ -50,6 +53,7 @@ class ApiFormatDefinition:
|
||||
protected_keys: frozenset[str] = field(default_factory=frozenset) # 受保护的头部 key(小写)
|
||||
model_in_body: bool = True # 是否需要在请求体中包含 model 字段
|
||||
stream_in_body: bool = True # 是否需要在请求体中包含 stream 字段
|
||||
data_format_id: str = "" # 数据格式标识,相同 ID 可透传,不同需转换
|
||||
|
||||
def iter_aliases(self) -> Iterable[str]:
|
||||
"""返回大小写统一后的别名集合,包含枚举名本身。"""
|
||||
@@ -70,6 +74,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
auth_type="header",
|
||||
extra_headers={"anthropic-version": "2023-06-01"},
|
||||
protected_keys=frozenset({"x-api-key", "content-type", "anthropic-version"}),
|
||||
data_format_id="claude", # CLAUDE/CLAUDE_CLI 数据格式相同
|
||||
),
|
||||
APIFormat.CLAUDE_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.CLAUDE_CLI,
|
||||
@@ -79,6 +84,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
data_format_id="claude", # CLAUDE/CLAUDE_CLI 数据格式相同
|
||||
),
|
||||
APIFormat.OPENAI: ApiFormatDefinition(
|
||||
api_format=APIFormat.OPENAI,
|
||||
@@ -98,6 +104,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
data_format_id="openai_chat", # Chat Completions API 格式
|
||||
),
|
||||
APIFormat.OPENAI_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.OPENAI_CLI,
|
||||
@@ -107,6 +114,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
data_format_id="openai_responses", # Responses API 格式,与 OPENAI 不同需转换
|
||||
),
|
||||
APIFormat.GEMINI: ApiFormatDefinition(
|
||||
api_format=APIFormat.GEMINI,
|
||||
@@ -118,6 +126,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
|
||||
model_in_body=False, # Gemini 通过 URL 路径传递模型名
|
||||
stream_in_body=False, # Gemini 通过 URL 端点区分流式(streamGenerateContent vs generateContent)
|
||||
data_format_id="gemini", # GEMINI/GEMINI_CLI 数据格式相同
|
||||
),
|
||||
APIFormat.GEMINI_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.GEMINI_CLI,
|
||||
@@ -129,6 +138,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
|
||||
model_in_body=False, # Gemini 通过 URL 路径传递模型名
|
||||
stream_in_body=False, # Gemini 通过 URL 端点区分流式
|
||||
data_format_id="gemini", # GEMINI/GEMINI_CLI 数据格式相同
|
||||
),
|
||||
}
|
||||
|
||||
@@ -227,6 +237,62 @@ def get_protected_keys(api_format: APIFormat) -> frozenset[str]:
|
||||
return frozenset({"authorization", "content-type"})
|
||||
|
||||
|
||||
def get_data_format_id(api_format: Union[str, APIFormat]) -> str:
|
||||
"""
|
||||
获取格式的数据格式标识。
|
||||
|
||||
相同 data_format_id 的格式数据结构相同,可以透传;不同则需要转换。
|
||||
|
||||
Args:
|
||||
api_format: API 格式(字符串或枚举)
|
||||
|
||||
Returns:
|
||||
数据格式标识,未找到时返回格式名称本身(小写)
|
||||
"""
|
||||
# 统一转换为 APIFormat 枚举
|
||||
if isinstance(api_format, str):
|
||||
resolved = resolve_api_format(api_format)
|
||||
if resolved is None:
|
||||
# 未知格式:返回小写,与已定义格式的 data_format_id 风格一致
|
||||
return api_format.lower()
|
||||
api_format = resolved
|
||||
|
||||
definition = API_FORMAT_DEFINITIONS.get(api_format)
|
||||
if definition and definition.data_format_id:
|
||||
return definition.data_format_id
|
||||
# 兜底:返回格式名称本身(小写)
|
||||
return api_format.value.lower()
|
||||
|
||||
|
||||
def can_passthrough(client_format: Union[str, APIFormat], endpoint_format: Union[str, APIFormat]) -> bool:
|
||||
"""
|
||||
判断两个格式之间是否可以透传(不需要数据转换)。
|
||||
|
||||
透传条件:
|
||||
1. 格式完全相同
|
||||
2. data_format_id 相同(如 CLAUDE 和 CLAUDE_CLI 都是 "claude")
|
||||
|
||||
Args:
|
||||
client_format: 客户端请求格式
|
||||
endpoint_format: 端点 API 格式
|
||||
|
||||
Returns:
|
||||
True 表示可以透传,False 表示需要转换
|
||||
"""
|
||||
# 统一转换为字符串比较
|
||||
client_str = client_format.value if isinstance(client_format, APIFormat) else str(client_format).upper()
|
||||
endpoint_str = endpoint_format.value if isinstance(endpoint_format, APIFormat) else str(endpoint_format).upper()
|
||||
|
||||
# 完全相同
|
||||
if client_str == endpoint_str:
|
||||
return True
|
||||
|
||||
# 检查 data_format_id
|
||||
client_data_id = get_data_format_id(client_format)
|
||||
endpoint_data_id = get_data_format_id(endpoint_format)
|
||||
return client_data_id == endpoint_data_id
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _alias_lookup_cache() -> Dict[str, APIFormat]:
|
||||
"""缓存 alias -> APIFormat 查找表,减少重复构建。"""
|
||||
|
||||
Reference in New Issue
Block a user