feat: 为 GlobalModel 添加关联提供商查询 API

添加新的 API 端点 GET /api/admin/models/global/{global_model_id}/providers,用于获取 GlobalModel 的所有关联提供商(包括非活跃的)。

- 后端:实现 AdminGetGlobalModelProvidersAdapter 适配器
- 前端:使用新 API 替换原有的 ModelCatalog 获取方式
- 数据库:改进初始化时的错误提示和连接异常处理
This commit is contained in:
fawney19
2025-12-30 14:47:35 +08:00
parent 4a35d78c8d
commit a56854af43
6 changed files with 156 additions and 37 deletions

View File

@@ -4,7 +4,8 @@ import type {
GlobalModelUpdate,
GlobalModelResponse,
GlobalModelWithStats,
GlobalModelListResponse
GlobalModelListResponse,
ModelCatalogProviderDetail,
} from './types'
/**
@@ -83,3 +84,16 @@ export async function batchAssignToProviders(
)
return response.data
}
/**
* 获取 GlobalModel 的所有关联提供商(包括非活跃的)
*/
export async function getGlobalModelProviders(globalModelId: string): Promise<{
providers: ModelCatalogProviderDetail[]
total: number
}> {
const response = await client.get(
`/api/admin/models/global/${globalModelId}/providers`
)
return response.data
}

View File

@@ -20,4 +20,5 @@ export {
updateGlobalModel,
deleteGlobalModel,
batchAssignToProviders,
getGlobalModelProviders,
} from './endpoints/global-models'

View File

@@ -737,6 +737,7 @@ import {
updateGlobalModel,
deleteGlobalModel,
batchAssignToProviders,
getGlobalModelProviders,
type GlobalModelResponse,
} from '@/api/global-models'
import { log } from '@/utils/logger'
@@ -1080,18 +1081,11 @@ async function selectModel(model: GlobalModelResponse) {
async function loadModelProviders(_globalModelId: string) {
loadingModelProviders.value = true
try {
// 使用 ModelCatalog API 获取详细的关联提供商信息
const { getModelCatalog } = await import('@/api/endpoints')
const catalogResponse = await getModelCatalog()
// 使用新的 API 获取所有关联提供商(包括非活跃的)
const response = await getGlobalModelProviders(_globalModelId)
// 查找当前 GlobalModel 对应的 catalog item
const catalogItem = catalogResponse.models.find(
m => m.global_model_name === selectedModel.value?.name
)
if (catalogItem) {
// 转换为展示格式,包含完整的模型实现信息
selectedModelProviders.value = catalogItem.providers.map(p => ({
// 转换为展示格式
selectedModelProviders.value = response.providers.map(p => ({
id: p.provider_id,
model_id: p.model_id,
display_name: p.provider_display_name || p.provider_name,
@@ -1113,9 +1107,6 @@ async function loadModelProviders(_globalModelId: string) {
supports_function_calling: p.supports_function_calling,
supports_streaming: p.supports_streaming
}))
} else {
selectedModelProviders.value = []
}
} catch (err: any) {
log.error('加载关联提供商失败:', err)
showError(parseApiError(err, '加载关联提供商失败'), '错误')

View File

@@ -5,7 +5,7 @@ GlobalModel Admin API
"""
from dataclasses import dataclass
from typing import List, Optional
from typing import Optional
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session
@@ -19,9 +19,11 @@ from src.models.pydantic_models import (
BatchAssignToProvidersResponse,
GlobalModelCreate,
GlobalModelListResponse,
GlobalModelProvidersResponse,
GlobalModelResponse,
GlobalModelUpdate,
GlobalModelWithStats,
ModelCatalogProviderDetail,
)
from src.services.model.global_model import GlobalModelService
@@ -108,6 +110,17 @@ async def batch_assign_to_providers(
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{global_model_id}/providers", response_model=GlobalModelProvidersResponse)
async def get_global_model_providers(
request: Request,
global_model_id: str,
db: Session = Depends(get_db),
) -> GlobalModelProvidersResponse:
"""获取 GlobalModel 的所有关联提供商(包括非活跃的)"""
adapter = AdminGetGlobalModelProvidersAdapter(global_model_id=global_model_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
# ========== Adapters ==========
@@ -275,3 +288,61 @@ class AdminBatchAssignToProvidersAdapter(AdminApiAdapter):
logger.info(f"批量为 Provider 添加 GlobalModel: global_model_id={self.global_model_id} success={len(result['success'])} errors={len(result['errors'])}")
return BatchAssignToProvidersResponse(**result)
@dataclass
class AdminGetGlobalModelProvidersAdapter(AdminApiAdapter):
"""获取 GlobalModel 的所有关联提供商(包括非活跃的)"""
global_model_id: str
async def handle(self, context): # type: ignore[override]
from sqlalchemy.orm import joinedload
from src.models.database import Model
global_model = GlobalModelService.get_global_model(context.db, self.global_model_id)
# 获取所有关联的 Model包括非活跃的
models = (
context.db.query(Model)
.options(joinedload(Model.provider), joinedload(Model.global_model))
.filter(Model.global_model_id == global_model.id)
.all()
)
provider_entries = []
for model in models:
provider = model.provider
if not provider:
continue
effective_tiered = model.get_effective_tiered_pricing()
tier_count = len(effective_tiered.get("tiers", [])) if effective_tiered else 1
provider_entries.append(
ModelCatalogProviderDetail(
provider_id=provider.id,
provider_name=provider.name,
provider_display_name=provider.display_name,
model_id=model.id,
target_model=model.provider_model_name,
input_price_per_1m=model.get_effective_input_price(),
output_price_per_1m=model.get_effective_output_price(),
cache_creation_price_per_1m=model.get_effective_cache_creation_price(),
cache_read_price_per_1m=model.get_effective_cache_read_price(),
cache_1h_creation_price_per_1m=model.get_effective_1h_cache_creation_price(),
price_per_request=model.get_effective_price_per_request(),
effective_tiered_pricing=effective_tiered,
tier_count=tier_count,
supports_vision=model.get_effective_supports_vision(),
supports_function_calling=model.get_effective_supports_function_calling(),
supports_streaming=model.get_effective_supports_streaming(),
is_active=bool(model.is_active),
)
)
return GlobalModelProvidersResponse(
providers=provider_entries,
total=len(provider_entries),
)

View File

@@ -360,6 +360,9 @@ def init_db():
注意:数据库表结构由 Alembic 管理,部署时请运行 ./migrate.sh
"""
import sys
from sqlalchemy.exc import OperationalError
logger.info("初始化数据库...")
# 确保引擎已创建
@@ -382,6 +385,38 @@ def init_db():
db.commit()
logger.info("数据库初始化完成")
except OperationalError as e:
db.rollback()
# 提取数据库连接信息用于提示
db_url = config.database_url
# 隐藏密码,只显示 host:port/database
if "@" in db_url:
db_info = db_url.split("@")[-1]
else:
db_info = db_url
import os
# 直接打印到 stderr确保消息显示
print("", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("数据库连接失败", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("", file=sys.stderr)
print(f"无法连接到数据库: {db_info}", file=sys.stderr)
print("", file=sys.stderr)
print("请检查以下事项:", file=sys.stderr)
print(" 1. PostgreSQL 服务是否正在运行", file=sys.stderr)
print(" 2. 数据库连接配置是否正确 (DATABASE_URL)", file=sys.stderr)
print(" 3. 数据库用户名和密码是否正确", file=sys.stderr)
print("", file=sys.stderr)
print("如果使用 Docker请先运行:", file=sys.stderr)
print(" docker-compose up -d postgres redis", file=sys.stderr)
print("", file=sys.stderr)
print("=" * 60, file=sys.stderr)
# 使用 os._exit 直接退出,避免 uvicorn 捕获并打印堆栈
os._exit(1)
except Exception as e:
logger.error(f"数据库初始化失败: {e}")
db.rollback()

View File

@@ -274,6 +274,13 @@ class GlobalModelListResponse(BaseModel):
total: int
class GlobalModelProvidersResponse(BaseModel):
"""GlobalModel 关联提供商列表响应"""
providers: List[ModelCatalogProviderDetail]
total: int
class BatchAssignToProvidersRequest(BaseModel):
"""批量为 Provider 添加 GlobalModel 实现"""