List validated chat model providers
This commit is contained in:
+79
-30
@@ -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("/")}'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user