refactor: 增强跨格式转换系统,支持 thinking/文件/音频/响应格式等完整转换

- 新增 ThinkingConfig/ResponseFormatConfig/FileBlock/AudioBlock 内部表示
- 实现 OpenAI reasoning_effort <-> Claude thinking <-> Gemini thinkingConfig 互转
- 支持 OpenAI web_search_options -> Claude web_search tool 转换
- 新增异步 convert_request_async,在转换阶段解析图片 URL 为 base64
- 将 GlobalModel.output_limit 传播至跨格式转换用于 max_tokens 默认值
- 前端模型表单增加最大输出 Token 和上下文窗口配置
- Claude normalizer 保留 cache_control 透传(system 数组和 content block)
- Gemini normalizer 支持内置工具(googleSearch/codeExecution/urlContext)
- OpenAI normalizer 补全采样参数、response_format、usage details 转换
- 各 normalizer 工具方法提取至基类消除重复代码
- 简化 Kiro model mapping 为直接透传
- 预置模型列表添加 claude-sonnet-4.6
This commit is contained in:
fawney19
2026-02-19 10:40:49 +08:00
parent 11f2ddbba2
commit 0d2cafaec3
20 changed files with 1405 additions and 234 deletions

View File

@@ -20,41 +20,9 @@ from src.services.provider.adapters.kiro.constants import (
def map_model(model: str) -> str | None:
"""Map an Anthropic model name to a Kiro-compatible model ID.
Kiro upstream expects specific model IDs (e.g. ``claude-sonnet-4.5``).
If *model* already matches a Kiro ID it is returned as-is; otherwise we
attempt a best-effort fuzzy mapping. Returns ``None`` when the model is
unrecognised.
"""
"""Pass through the model name as-is to Kiro upstream."""
raw = str(model or "").strip()
if not raw:
return None
model_lower = raw.lower()
# Already a Kiro-native ID?
_KIRO_NATIVE_IDS = {
"claude-sonnet-4.5",
"claude-opus-4.5",
"claude-opus-4.6",
"claude-haiku-4.5",
}
if model_lower in _KIRO_NATIVE_IDS:
return model_lower
# Fuzzy mapping from Anthropic-style model names
if "sonnet" in model_lower:
return "claude-sonnet-4.5"
if "opus" in model_lower:
if "4-5" in model_lower or "4.5" in model_lower:
return "claude-opus-4.5"
return "claude-opus-4.6"
if "haiku" in model_lower:
return "claude-haiku-4.5"
# Unrecognised — pass through as-is (let the upstream decide)
return raw
return raw or None
def _extract_session_id(user_id: str) -> str | None:

View File

@@ -35,6 +35,12 @@ PRESET_MODELS: dict[str, list[dict[str, Any]]] = {
"owned_by": "anthropic",
"display_name": "Claude Sonnet 4.5",
},
{
"id": "claude-sonnet-4.6",
"object": "model",
"owned_by": "anthropic",
"display_name": "Claude Sonnet 4.6",
},
{
"id": "claude-opus-4.5",
"object": "model",

View File

@@ -386,6 +386,17 @@ class CandidateBuilder:
client_format_str = normalize_endpoint_signature(client_format)
client_sig = parse_signature_key(client_format_str)
client_family, client_kind = client_sig.api_family, client_sig.endpoint_kind
# 提取 GlobalModel 配置的 output_limit用于跨格式转换时的 max_tokens 默认值)
output_limit: int | None = None
normalized_name = model_name.strip() if isinstance(model_name, str) else ""
if normalized_name:
gm = await ModelCacheService.get_global_model_by_name(db, normalized_name)
if gm and isinstance(gm.config, dict):
raw = gm.config.get("output_limit")
if isinstance(raw, int) and raw > 0:
output_limit = raw
# chat/cli 互相可回退用于同协议族下的端点变体video/image 等不跨类回退
if client_kind in {EndpointKind.CHAT, EndpointKind.CLI}:
allowed_kinds = {EndpointKind.CHAT, EndpointKind.CLI}
@@ -581,6 +592,7 @@ class CandidateBuilder:
mapping_matched_model=mapping_matched_model,
needs_conversion=needs_conversion,
provider_api_format=str(endpoint_format_str or ""),
output_limit=output_limit,
)
if needs_conversion:

View File

@@ -28,6 +28,7 @@ class ProviderCandidate:
mapping_matched_model: str | None = None # 通过映射匹配到的模型名(用于实际请求)
needs_conversion: bool = False # 是否需要格式转换
provider_api_format: str = "" # Provider 端点实际格式(用于健康度/熔断 bucket
output_limit: int | None = None # GlobalModel 配置的模型输出上限
def _stable_order_key(self) -> tuple[int, int, str, str, str]:
"""