List validated chat model providers

This commit is contained in:
武阳
2026-05-08 11:51:53 +08:00
parent 37fc367304
commit e4c3b973d4
4 changed files with 264 additions and 34 deletions
+79 -30
View File
@@ -49,6 +49,22 @@ from src.token_budget import calculate_token_budget
STATIC_DIR = Path(__file__).resolve().parents[2] / 'frontend' / 'legacy-static'
VALIDATED_CHAT_MODEL_PROVIDERS = {
# 这些 provider 已用 owned_by/id 形式实际请求 /chat/completions 验证通过。
'azure_openai',
'hunyuan',
'minimax',
'moonshot',
'ppio',
'siliconflow',
'tongyi',
'vertex_ai',
'volcengine_maas',
'wenxin',
'xiaomi',
'zhipuai',
}
# WebUI 的快速入口只展示可以通过对话输入框安全执行的 slash command。
# 这些命令仍然保留在终端 `/` 列表里,但不适合作为 WebUI 快捷项。
WEBUI_HIDDEN_SLASH_COMMANDS = {
@@ -1476,6 +1492,7 @@ def _list_backend_models(base_url: str, api_key: str) -> dict[str, Any]:
_join_url(base_url, '/models'),
headers={
'Authorization': f'Bearer {api_key}',
'api-key': api_key,
'Content-Type': 'application/json',
},
method='GET',
@@ -1496,60 +1513,92 @@ def _list_backend_models(base_url: str, api_key: str) -> dict[str, Any]:
) from exc
models = _normalize_model_list(payload)
return {'models': models, 'raw_count': len(models)}
return {
'models': models,
'raw_count': _raw_model_count(payload),
'filtered_count': len(models),
}
def _normalize_model_list(payload: Any) -> list[dict[str, str]]:
data = payload.get('data') if isinstance(payload, dict) else payload
if not isinstance(data, list):
return []
model_ids: dict[str, str] = {}
models_by_key: dict[str, dict[str, str]] = {}
for item in data:
if isinstance(item, str):
normalized = _normalize_model_option(item)
normalized = _normalize_model_id(item)
if normalized is not None:
model_ids.setdefault(normalized.lower(), normalized)
models_by_key.setdefault(normalized.lower(), {'id': normalized})
continue
if not isinstance(item, dict):
continue
model_id = item.get('id') or item.get('model') or item.get('name')
if not isinstance(model_id, str) or not model_id:
continue
normalized = _normalize_model_option(model_id)
model_type = _optional_model_string(item.get('model_type') or item.get('type'))
if not _is_llm_model(model_id, model_type):
continue
provider = _optional_model_string(
item.get('owned_by') or item.get('provider') or item.get('owner')
)
if provider and provider not in VALIDATED_CHAT_MODEL_PROVIDERS:
continue
normalized = _normalize_model_id(model_id, provider=provider)
if normalized is not None:
model_ids.setdefault(normalized.lower(), normalized)
return [{'id': model_id} for model_id in sorted(model_ids.values(), key=str.lower)]
entry = {'id': normalized}
if provider:
entry['provider'] = provider
if model_type:
entry['model_type'] = model_type
models_by_key.setdefault(normalized.lower(), entry)
return sorted(models_by_key.values(), key=lambda item: item['id'].lower())
def _normalize_model_option(model_id: str) -> str | None:
normalized = _normalize_chat_model_name(model_id)
lower = normalized.lower()
if lower.startswith('xiaomi/mimo-v2') and not any(
blocked in lower for blocked in ('audio', 'asr', 'tts', 'voiceclone', 'voicedesign')
):
return normalized
known_chat_models = {
'xiaomi/deepseek-r1-0528',
'xiaomi/minimax-m2.5',
'xiaomi/qwen3-32b',
}
if lower in known_chat_models:
return normalized
return None
def _raw_model_count(payload: Any) -> int:
data = payload.get('data') if isinstance(payload, dict) else payload
return len(data) if isinstance(data, list) else 0
def _normalize_chat_model_name(model: str) -> str:
def _optional_model_string(value: Any) -> str | None:
if not isinstance(value, str):
return None
stripped = value.strip()
return stripped or None
def _normalize_model_id(model: str, *, provider: str | None = None) -> str | None:
value = model.strip()
lower = value.lower()
if lower.startswith('mimo-v2') or lower in {
'deepseek-r1-0528',
'minimax-m2.5',
'qwen3-32b',
}:
return f'xiaomi/{value}'
if not value:
return None
if provider and not value.lower().startswith(f'{provider.lower()}/'):
return f'{provider}/{value}'
return value
def _is_llm_model(model_id: str, model_type: str | None) -> bool:
lower = model_id.strip().lower()
blocked_fragments = (
'asr',
'audio',
'embedding',
'image_generation',
'rerank',
'speech',
'text2image',
'transcribe',
'translation',
'tts',
'voiceclone',
'voicedesign',
)
if any(fragment in lower for fragment in blocked_fragments):
return False
if model_type:
return model_type.strip().lower() == 'llm'
return True
def _join_url(base_url: str, suffix: str) -> str:
return f'{base_url.rstrip("/")}/{suffix.lstrip("/")}'