114 lines
5.0 KiB
Python
114 lines
5.0 KiB
Python
"""模型清单路由:/v1/models、/v1/image_models、/v1/video_models(按用户档位过滤)。"""
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import Depends
|
|
|
|
from ..model_gate import (
|
|
default_image_variant,
|
|
list_image_variants,
|
|
list_video_variants,
|
|
user_plan_role,
|
|
)
|
|
|
|
|
|
def register_model_routes(app, *, require_user) -> None:
|
|
@app.get("/v1/models", tags=["misc"])
|
|
def list_models(user_id: UUID = Depends(require_user)):
|
|
"""列出所有可用 LLM 模型(扫 config/models/*.yaml)。
|
|
|
|
前端顶栏 / 新建对话框的模型下拉拉这个。is_default 标记 cfg["default_model"]
|
|
命中项。开发期不缓存,每次扫一遍(几个文件 IO);改 YAML 立即生效。
|
|
"""
|
|
from core.agent_builder import load_config
|
|
from core.capabilities import ModelCapabilities
|
|
from core.model_access import allowed_set
|
|
from core.paths import ROOT
|
|
import yaml as _yaml
|
|
cfg = load_config()
|
|
default = cfg["default_model"]
|
|
models_dir = ROOT / cfg["models_dir"]
|
|
# 按用户档位过滤:allowed=None → 全开(admin / '*' 档),否则只留集合内 profile。
|
|
plan, role = user_plan_role(user_id)
|
|
allowed = allowed_set(plan, role)
|
|
|
|
out: list[dict] = []
|
|
if models_dir.is_dir():
|
|
for path in sorted(models_dir.glob("*.yaml")):
|
|
try:
|
|
data = _yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
except Exception:
|
|
continue
|
|
family = data.get("family") or path.stem
|
|
for variant in (data.get("variants") or {}).keys():
|
|
profile = f"{family}.{variant}"
|
|
if allowed is not None and profile not in allowed:
|
|
continue
|
|
try:
|
|
caps = ModelCapabilities.load(profile, models_dir)
|
|
except (ValueError, FileNotFoundError):
|
|
continue
|
|
out.append({
|
|
"profile": profile,
|
|
"display_name": caps.display_name or profile,
|
|
"family": caps.family,
|
|
"variant": caps.variant,
|
|
"thinking_enabled": caps.thinking_enabled,
|
|
"is_default": profile == default,
|
|
})
|
|
return {"models": out}
|
|
|
|
@app.get("/v1/image_models", tags=["misc"])
|
|
def list_image_models(user_id: UUID = Depends(require_user)):
|
|
"""图像生成模型清单(扫 config/media/*.yaml image 段,跨 provider)。
|
|
|
|
前端顶栏第二个下拉拉这个;空列表 → 没配 image variant 或本档无授权,UI 隐藏下拉。
|
|
按用户档位过滤;`is_default` 见 default_image_variant(有 gpt_image 权限 →
|
|
默认 GPT 生图,否则第一个)。开发期不缓存,改 YAML 立即生效。
|
|
"""
|
|
from core.model_access import allowed_set
|
|
plan, role = user_plan_role(user_id)
|
|
allowed = allowed_set(plan, role)
|
|
default_key = default_image_variant(allowed)
|
|
out: list[dict] = []
|
|
for key, cfg in list_image_variants():
|
|
if allowed is not None and key not in allowed:
|
|
continue
|
|
out.append({
|
|
"variant": key,
|
|
"display_name": cfg.get("display_name") or key,
|
|
"model_id": cfg.get("model_id") or "",
|
|
"price_cny_per_image": cfg.get("price_cny_per_image"),
|
|
"is_default": key == default_key,
|
|
})
|
|
return {"models": out}
|
|
|
|
@app.get("/v1/video_models", tags=["misc"])
|
|
def list_video_models(user_id: UUID = Depends(require_user)):
|
|
"""视频生成模型清单(扫 config/media/doubao.yaml video 段)。
|
|
|
|
与 /v1/image_models 同范式;空列表 → UI 隐藏第三下拉。展示信息包括默认分辨率
|
|
与 token 单价(¥/Mtok 文生视频路径),方便用户在下拉选项里直接看到 cost 量级。
|
|
按用户档位过滤;`is_default` 标过滤后第一个 variant。
|
|
"""
|
|
from core.model_access import allowed_set
|
|
plan, role = user_plan_role(user_id)
|
|
allowed = allowed_set(plan, role)
|
|
out: list[dict] = []
|
|
for key, cfg in list_video_variants():
|
|
if allowed is not None and key not in allowed:
|
|
continue
|
|
out.append({
|
|
"variant": key,
|
|
"display_name": cfg.get("display_name") or key,
|
|
"model_id": cfg.get("model_id") or "",
|
|
"default_resolution": cfg.get("default_resolution"),
|
|
"default_duration": cfg.get("default_duration"),
|
|
"default_ratio": cfg.get("default_ratio"),
|
|
"price_cny_per_mtoken_text2video": cfg.get("price_cny_per_mtoken_text2video"),
|
|
"price_cny_per_mtoken_video2video": cfg.get("price_cny_per_mtoken_video2video"),
|
|
"is_default": not out, # 过滤后第一个
|
|
})
|
|
return {"models": out}
|