mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 重构上游模型获取与展示逻辑
- 后端支持遍历所有活跃 API Key 聚合模型,按 model id 合并 api_formats - 前端移除按 API 格式分组,改为统一展示并内联显示格式标签 - 改进 401 错误处理,账户异常时清除认证并重定向首页
This commit is contained in:
@@ -174,9 +174,12 @@ class ApiClient {
|
||||
const errorDetail = error.response?.data?.detail || ''
|
||||
log.debug('Got 401 error, attempting token refresh', { errorDetail })
|
||||
|
||||
// 检查是否为业务相关的401错误
|
||||
// 检查是否为业务相关的401错误(用户被禁用/删除等)
|
||||
if (!isRefreshableAuthError(errorDetail)) {
|
||||
log.info('401 error but not authentication issue, keeping session', { errorDetail })
|
||||
log.info('User account issue detected, logging out and redirecting to home', { errorDetail })
|
||||
this.clearAuth()
|
||||
// 跳转到首页
|
||||
window.location.href = '/'
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
|
||||
@@ -630,12 +630,13 @@ export interface GlobalModelListResponse {
|
||||
|
||||
/**
|
||||
* 上游模型(从提供商 API 获取的原始模型)
|
||||
* 后端已按 model id 聚合,api_formats 包含该模型支持的所有 API 格式
|
||||
*/
|
||||
export interface UpstreamModel {
|
||||
id: string
|
||||
owned_by?: string
|
||||
display_name?: string
|
||||
api_format?: string
|
||||
api_formats: string[] // 该模型支持的所有 API 格式(后端保证返回数组)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,36 +119,33 @@
|
||||
</div>
|
||||
|
||||
<!-- 上游模型组 -->
|
||||
<div
|
||||
v-for="group in filteredUpstreamGroups"
|
||||
:key="group.api_format"
|
||||
>
|
||||
<div v-if="filteredUpstreamModels.length > 0">
|
||||
<div
|
||||
class="flex items-center justify-between px-3 py-2 bg-muted sticky top-0 z-10 cursor-pointer hover:bg-muted/80 transition-colors"
|
||||
@click="toggleGroupCollapse(group.api_format)"
|
||||
@click="toggleGroupCollapse('upstream')"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ChevronDown
|
||||
class="w-4 h-4 transition-transform shrink-0"
|
||||
:class="collapsedGroups.has(group.api_format) ? '-rotate-90' : ''"
|
||||
:class="collapsedGroups.has('upstream') ? '-rotate-90' : ''"
|
||||
/>
|
||||
<span class="text-xs font-medium">{{ API_FORMAT_LABELS[group.api_format] || group.api_format }}</span>
|
||||
<span class="text-xs text-muted-foreground">({{ group.models.length }})</span>
|
||||
<span class="text-xs font-medium">上游模型</span>
|
||||
<span class="text-xs text-muted-foreground">({{ filteredUpstreamModels.length }})</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-primary hover:underline shrink-0"
|
||||
@click.stop="toggleAllUpstreamGroup(group.api_format)"
|
||||
@click.stop="toggleAllUpstreamModels"
|
||||
>
|
||||
{{ isUpstreamGroupAllSelected(group.api_format) ? '取消全选' : '全选' }}
|
||||
{{ isAllUpstreamModelsSelected ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-show="!collapsedGroups.has(group.api_format)"
|
||||
v-show="!collapsedGroups.has('upstream')"
|
||||
class="space-y-1 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="model in group.models"
|
||||
v-for="model in filteredUpstreamModels"
|
||||
:key="model.id"
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-muted cursor-pointer"
|
||||
@click="toggleUpstreamModelSelection(model.id)"
|
||||
@@ -163,11 +160,23 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">
|
||||
{{ model.id }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground truncate font-mono">
|
||||
{{ model.owned_by || model.id }}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-sm font-medium truncate">
|
||||
{{ model.id }}
|
||||
</p>
|
||||
<span
|
||||
v-for="fmt in model.api_formats"
|
||||
:key="fmt"
|
||||
class="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0"
|
||||
>
|
||||
{{ API_FORMAT_LABELS[fmt] || fmt }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="model.owned_by"
|
||||
class="text-xs text-muted-foreground truncate"
|
||||
>
|
||||
{{ model.owned_by }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +185,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="filteredGlobalModels.length === 0 && filteredUpstreamGroups.length === 0"
|
||||
v-if="filteredGlobalModels.length === 0 && filteredUpstreamModels.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-muted-foreground"
|
||||
>
|
||||
<Layers class="w-10 h-10 mb-2 opacity-30" />
|
||||
@@ -319,35 +328,43 @@ const filteredGlobalModels = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 过滤后的上游模型(按 API 格式分组)
|
||||
const filteredUpstreamGroups = computed(() => {
|
||||
// 过滤后的上游模型(后端已按 id 聚合)
|
||||
const filteredUpstreamModels = computed(() => {
|
||||
if (!upstreamModelsLoaded.value) return []
|
||||
|
||||
const query = searchQuery.value.toLowerCase().trim()
|
||||
const groups: Record<string, UpstreamModel[]> = {}
|
||||
let models = upstreamModels.value
|
||||
|
||||
for (const model of upstreamModels.value) {
|
||||
if (query && !model.id.toLowerCase().includes(query)) continue
|
||||
|
||||
const format = model.api_format || 'unknown'
|
||||
if (!groups[format]) groups[format] = []
|
||||
groups[format].push(model)
|
||||
if (query) {
|
||||
models = models.filter(m => m.id.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
const order = Object.keys(API_FORMAT_LABELS)
|
||||
return Object.entries(groups)
|
||||
.map(([api_format, models]) => ({ api_format, models }))
|
||||
.filter(g => g.models.length > 0)
|
||||
.sort((a, b) => {
|
||||
const aIndex = order.indexOf(a.api_format)
|
||||
const bIndex = order.indexOf(b.api_format)
|
||||
if (aIndex === -1 && bIndex === -1) return a.api_format.localeCompare(b.api_format)
|
||||
if (aIndex === -1) return 1
|
||||
if (bIndex === -1) return -1
|
||||
return aIndex - bIndex
|
||||
})
|
||||
// 按 id 排序
|
||||
return [...models].sort((a, b) => a.id.localeCompare(b.id))
|
||||
})
|
||||
|
||||
// 上游模型是否全选
|
||||
const isAllUpstreamModelsSelected = computed(() => {
|
||||
if (filteredUpstreamModels.value.length === 0) return false
|
||||
return filteredUpstreamModels.value.every(m => selectedUpstreamModelIds.value.has(m.id))
|
||||
})
|
||||
|
||||
// 全选/取消全选上游模型
|
||||
function toggleAllUpstreamModels() {
|
||||
const allIds = filteredUpstreamModels.value.map(m => m.id)
|
||||
if (isAllUpstreamModelsSelected.value) {
|
||||
// 取消全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.delete(id)
|
||||
}
|
||||
} else {
|
||||
// 全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查全局模型是否已选中
|
||||
function isGlobalModelSelected(globalModelId: string): boolean {
|
||||
return selectedGlobalModelIds.value.has(globalModelId)
|
||||
@@ -364,13 +381,6 @@ const isAllGlobalModelsSelected = computed(() => {
|
||||
return filteredGlobalModels.value.every(m => isGlobalModelSelected(m.id))
|
||||
})
|
||||
|
||||
// 检查某个上游组是否全选
|
||||
function isUpstreamGroupAllSelected(apiFormat: string): boolean {
|
||||
const group = filteredUpstreamGroups.value.find(g => g.api_format === apiFormat)
|
||||
if (!group || group.models.length === 0) return false
|
||||
return group.models.every(m => isUpstreamModelSelected(m.id))
|
||||
}
|
||||
|
||||
// 计算待添加的全局模型
|
||||
const globalModelsToAdd = computed(() => {
|
||||
const toAdd: string[] = []
|
||||
@@ -468,26 +478,6 @@ function toggleAllGlobalModels() {
|
||||
selectedGlobalModelIds.value = new Set(selectedGlobalModelIds.value)
|
||||
}
|
||||
|
||||
// 全选/取消全选某个上游组
|
||||
function toggleAllUpstreamGroup(apiFormat: string) {
|
||||
const group = filteredUpstreamGroups.value.find(g => g.api_format === apiFormat)
|
||||
if (!group) return
|
||||
|
||||
const allIds = group.models.map(m => m.id)
|
||||
if (isUpstreamGroupAllSelected(apiFormat)) {
|
||||
// 取消全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.delete(id)
|
||||
}
|
||||
} else {
|
||||
// 全选
|
||||
for (const id of allIds) {
|
||||
selectedUpstreamModelIds.value.add(id)
|
||||
}
|
||||
}
|
||||
selectedUpstreamModelIds.value = new Set(selectedUpstreamModelIds.value)
|
||||
}
|
||||
|
||||
// 切换折叠状态
|
||||
function toggleGroupCollapse(group: string) {
|
||||
if (collapsedGroups.value.has(group)) {
|
||||
@@ -672,14 +662,8 @@ async function fetchUpstreamModels(forceRefresh = false) {
|
||||
upstreamModelsLoaded.value = true
|
||||
// 同步上游模型选择状态
|
||||
syncUpstreamModelSelection()
|
||||
// 有多个分组时全部折叠
|
||||
const allGroups = new Set(['global'])
|
||||
for (const model of result.models) {
|
||||
if (model.api_format) {
|
||||
allGroups.add(model.api_format)
|
||||
}
|
||||
}
|
||||
collapsedGroups.value = allGroups
|
||||
// 全部折叠
|
||||
collapsedGroups.value = new Set(['global', 'upstream'])
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Dialog
|
||||
:model-value="isOpen"
|
||||
title="获取上游模型"
|
||||
:description="`使用密钥 ${props.apiKey?.name || props.apiKey?.api_key_masked || ''} 从上游获取模型列表。导入的模型需要关联全局模型后才能参与路由。`"
|
||||
description="从上游获取所有密钥可用的模型列表。导入的模型需要关联全局模型后才能参与路由。"
|
||||
:icon="Layers"
|
||||
size="2xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
@@ -100,7 +100,7 @@
|
||||
<div class="max-h-[320px] overflow-y-auto pr-1 space-y-1 custom-scrollbar">
|
||||
<div
|
||||
v-for="model in upstreamModels"
|
||||
:key="`${model.id}:${model.api_format || ''}`"
|
||||
:key="model.id"
|
||||
class="group flex items-center gap-3 px-3 py-2.5 rounded-lg border transition-all duration-200 cursor-pointer select-none"
|
||||
:class="[
|
||||
selectedModels.includes(model.id)
|
||||
@@ -121,11 +121,12 @@
|
||||
{{ model.display_name || model.id }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="model.api_format"
|
||||
v-for="fmt in model.api_formats"
|
||||
:key="fmt"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
>
|
||||
{{ API_FORMAT_LABELS[model.api_format] || model.api_format }}
|
||||
{{ API_FORMAT_LABELS[fmt] || fmt }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="isModelExisting(model.id)"
|
||||
@@ -274,15 +275,16 @@ async function loadExistingModels() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取上游模型
|
||||
// 获取上游模型(获取所有 Key 的聚合结果)
|
||||
async function fetchUpstreamModels() {
|
||||
if (!props.providerId || !props.apiKey) return
|
||||
if (!props.providerId) return
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await adminApi.queryProviderModels(props.providerId, props.apiKey.id)
|
||||
// 不传 apiKeyId,后端会遍历所有 Key 并聚合结果
|
||||
const response = await adminApi.queryProviderModels(props.providerId)
|
||||
|
||||
if (response.success && response.data?.models) {
|
||||
upstreamModels.value = response.data.models
|
||||
|
||||
@@ -468,28 +468,10 @@ function isGlobalModel(modelId: string): boolean {
|
||||
return globalModelNamesSet.value.has(modelId)
|
||||
}
|
||||
|
||||
// 上游模型信息(包含 api_format)
|
||||
interface UpstreamModelInfo {
|
||||
id: string
|
||||
api_formats: string[] // 该模型支持的所有 API 格式
|
||||
}
|
||||
|
||||
// 上游模型列表(按 id 聚合,包含所有 api_format)
|
||||
// 上游模型列表(后端已按 id 聚合,包含 api_formats 数组)
|
||||
// 这里只做排序
|
||||
const upstreamModelList = computed(() => {
|
||||
const modelMap = new Map<string, Set<string>>()
|
||||
upstreamModels.value.forEach(m => {
|
||||
if (!modelMap.has(m.id)) {
|
||||
modelMap.set(m.id, new Set())
|
||||
}
|
||||
if (m.api_format) {
|
||||
modelMap.get(m.id)!.add(m.api_format)
|
||||
}
|
||||
})
|
||||
const result: UpstreamModelInfo[] = []
|
||||
modelMap.forEach((formats, id) => {
|
||||
result.push({ id, api_formats: sortApiFormats(Array.from(formats)) })
|
||||
})
|
||||
return result.sort((a, b) => a.id.localeCompare(b.id))
|
||||
return [...upstreamModels.value].sort((a, b) => a.id.localeCompare(b.id))
|
||||
})
|
||||
|
||||
// 上游模型名称列表(用于计数和全选判断)
|
||||
@@ -687,11 +669,13 @@ async function loadGlobalModels() {
|
||||
}
|
||||
|
||||
// 从提供商获取模型(使用缓存)
|
||||
// 不传 apiKeyId,获取所有 Key 的聚合结果
|
||||
async function fetchUpstreamModels(forceRefresh = false) {
|
||||
if (!props.providerId || !props.apiKey) return
|
||||
try {
|
||||
fetchingUpstreamModels.value = true
|
||||
const result = await fetchCachedModels(props.providerId, props.apiKey.id, forceRefresh)
|
||||
// 不传 apiKeyId,后端会遍历所有 Key 并聚合结果
|
||||
const result = await fetchCachedModels(props.providerId, undefined, forceRefresh)
|
||||
if (loadingCancelled) return
|
||||
if (result.models.length > 0) {
|
||||
upstreamModels.value = result.models
|
||||
|
||||
@@ -264,7 +264,8 @@ import {
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import {
|
||||
type Model,
|
||||
type ProviderModelAlias
|
||||
type ProviderModelAlias,
|
||||
type UpstreamModel,
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
||||
@@ -311,7 +312,7 @@ const searchQuery = ref('')
|
||||
const collapsedGroups = ref<Set<string>>(new Set())
|
||||
|
||||
// 上游模型
|
||||
const upstreamModels = ref<{ id: string; api_format?: string }[]>([])
|
||||
const upstreamModels = ref<UpstreamModel[]>([])
|
||||
|
||||
// 表单数据
|
||||
const formData = ref<{
|
||||
|
||||
@@ -3,6 +3,7 @@ Provider Query API 端点
|
||||
用于查询提供商的模型列表等信息
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
@@ -23,6 +24,10 @@ from src.services.model.upstream_fetcher import (
|
||||
)
|
||||
from src.utils.auth_utils import get_current_user
|
||||
from src.utils.ssl_utils import get_ssl_context
|
||||
from src.services.model.fetch_scheduler import (
|
||||
get_upstream_models_from_cache,
|
||||
set_upstream_models_to_cache,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/admin/provider-query", tags=["Provider Query"])
|
||||
@@ -66,17 +71,16 @@ async def query_available_models(
|
||||
优先从缓存获取(缓存由定时任务刷新),缓存未命中时实时调用上游 API。
|
||||
从所有 API 格式尝试获取模型,然后聚合去重。
|
||||
|
||||
行为:
|
||||
- 指定 api_key_id: 只获取该 Key 能访问的模型
|
||||
- 不指定 api_key_id: 遍历所有活跃的 Key,聚合所有模型(每个 Key 独立缓存)
|
||||
|
||||
Args:
|
||||
request: 查询请求
|
||||
|
||||
Returns:
|
||||
所有端点的模型列表(合并)
|
||||
"""
|
||||
from src.services.model.fetch_scheduler import (
|
||||
get_upstream_models_from_cache,
|
||||
set_upstream_models_to_cache,
|
||||
)
|
||||
|
||||
# 获取提供商基本信息
|
||||
provider = (
|
||||
db.query(Provider)
|
||||
@@ -91,11 +95,171 @@ async def query_available_models(
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="Provider not found")
|
||||
|
||||
# 如果指定了 api_key_id 且不是强制刷新,优先从缓存获取
|
||||
if request.api_key_id and not request.force_refresh:
|
||||
cached_models = await get_upstream_models_from_cache(
|
||||
request.provider_id, request.api_key_id
|
||||
# 构建 api_format -> endpoint 映射
|
||||
format_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||
for endpoint in provider.endpoints:
|
||||
if endpoint.is_active:
|
||||
format_to_endpoint[endpoint.api_format] = endpoint
|
||||
|
||||
if not format_to_endpoint:
|
||||
raise HTTPException(status_code=400, detail="No active endpoints found for this provider")
|
||||
|
||||
# 如果指定了 api_key_id,只获取该 Key 的模型
|
||||
if request.api_key_id:
|
||||
return await _fetch_models_for_single_key(
|
||||
provider=provider,
|
||||
api_key_id=request.api_key_id,
|
||||
format_to_endpoint=format_to_endpoint,
|
||||
force_refresh=request.force_refresh,
|
||||
)
|
||||
|
||||
# 未指定 api_key_id,遍历所有活跃的 Key 并聚合结果
|
||||
active_keys = [key for key in provider.api_keys if key.is_active]
|
||||
if not active_keys:
|
||||
raise HTTPException(status_code=400, detail="No active API Key found for this provider")
|
||||
|
||||
# 并发获取所有 Key 的模型
|
||||
async def fetch_for_key(api_key):
|
||||
# 非强制刷新时,先检查缓存
|
||||
if not request.force_refresh:
|
||||
cached_models = await get_upstream_models_from_cache(
|
||||
request.provider_id, api_key.id
|
||||
)
|
||||
if cached_models is not None:
|
||||
return cached_models, None, True # models, error, from_cache
|
||||
|
||||
# 缓存未命中或强制刷新,实时获取
|
||||
try:
|
||||
api_key_value = crypto_service.decrypt(api_key.api_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt API key {api_key.id}: {e}")
|
||||
return [], f"Key {api_key.name or api_key.id}: decrypt failed", False
|
||||
|
||||
endpoint_configs = build_all_format_configs(api_key_value, format_to_endpoint)
|
||||
models, errors, has_success = await fetch_models_from_endpoints(endpoint_configs)
|
||||
|
||||
# 写入缓存
|
||||
if models:
|
||||
await set_upstream_models_to_cache(request.provider_id, api_key.id, models)
|
||||
|
||||
error = f"Key {api_key.name or api_key.id}: {'; '.join(errors)}" if errors else None
|
||||
return models, error, False # models, error, from_cache
|
||||
|
||||
# 并发执行所有 Key 的获取
|
||||
results = await asyncio.gather(*[fetch_for_key(key) for key in active_keys])
|
||||
|
||||
# 合并结果
|
||||
all_models: list = []
|
||||
all_errors: list[str] = []
|
||||
cache_hit_count = 0
|
||||
fetch_count = 0
|
||||
for models, error, from_cache in results:
|
||||
all_models.extend(models)
|
||||
if error:
|
||||
all_errors.append(error)
|
||||
if from_cache:
|
||||
cache_hit_count += 1
|
||||
else:
|
||||
fetch_count += 1
|
||||
|
||||
# 按 model id 聚合,合并所有 api_format 到 api_formats 数组
|
||||
unique_models = _aggregate_models_by_id(all_models)
|
||||
|
||||
error = "; ".join(all_errors) if all_errors else None
|
||||
if not unique_models and not error:
|
||||
error = "No models returned from any key"
|
||||
|
||||
return {
|
||||
"success": len(unique_models) > 0,
|
||||
"data": {
|
||||
"models": unique_models,
|
||||
"error": error,
|
||||
"from_cache": fetch_count == 0 and cache_hit_count > 0,
|
||||
"keys_total": len(active_keys),
|
||||
"keys_cached": cache_hit_count,
|
||||
"keys_fetched": fetch_count,
|
||||
},
|
||||
"provider": {
|
||||
"id": provider.id,
|
||||
"name": provider.name,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _aggregate_models_by_id(models: list[dict]) -> list[dict]:
|
||||
"""
|
||||
按 model id 聚合模型,合并所有 api_format 到 api_formats 数组
|
||||
|
||||
支持两种输入格式:
|
||||
- 原始模型: 有 api_format (singular) 字段
|
||||
- 已聚合模型: 有 api_formats (array) 字段(来自缓存)
|
||||
|
||||
Args:
|
||||
models: 模型列表,每个模型可能有 api_format 或 api_formats 字段
|
||||
|
||||
Returns:
|
||||
聚合后的模型列表,每个模型有 api_formats 数组
|
||||
"""
|
||||
model_map: dict[str, dict] = {}
|
||||
|
||||
for model in models:
|
||||
model_id = model.get("id")
|
||||
if not model_id:
|
||||
continue
|
||||
|
||||
# 支持两种格式:api_format (singular) 或 api_formats (array)
|
||||
api_format = model.get("api_format", "")
|
||||
existing_formats = model.get("api_formats") or []
|
||||
|
||||
if model_id not in model_map:
|
||||
# 第一次遇到这个模型,复制基础信息
|
||||
aggregated = {
|
||||
"id": model_id,
|
||||
"api_formats": [],
|
||||
}
|
||||
# 复制其他字段(排除 api_format 和 api_formats)
|
||||
for key, value in model.items():
|
||||
if key not in ("id", "api_format", "api_formats"):
|
||||
aggregated[key] = value
|
||||
model_map[model_id] = aggregated
|
||||
|
||||
# 添加 api_format 到列表(避免重复)
|
||||
if api_format and api_format not in model_map[model_id]["api_formats"]:
|
||||
model_map[model_id]["api_formats"].append(api_format)
|
||||
|
||||
# 添加已有的 api_formats(处理缓存的聚合数据)
|
||||
for fmt in existing_formats:
|
||||
if fmt and fmt not in model_map[model_id]["api_formats"]:
|
||||
model_map[model_id]["api_formats"].append(fmt)
|
||||
|
||||
# 对每个模型的 api_formats 排序
|
||||
result = list(model_map.values())
|
||||
for model in result:
|
||||
model["api_formats"].sort()
|
||||
|
||||
# 按 model id 排序
|
||||
result.sort(key=lambda m: m["id"])
|
||||
return result
|
||||
|
||||
|
||||
async def _fetch_models_for_single_key(
|
||||
provider: Provider,
|
||||
api_key_id: str,
|
||||
format_to_endpoint: dict[str, ProviderEndpoint],
|
||||
force_refresh: bool,
|
||||
):
|
||||
"""获取单个 Key 的模型列表"""
|
||||
# 查找指定的 Key
|
||||
api_key = next(
|
||||
(key for key in provider.api_keys if key.id == api_key_id),
|
||||
None
|
||||
)
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=404, detail="API Key not found")
|
||||
|
||||
# 非强制刷新时,优先从缓存获取
|
||||
if not force_refresh:
|
||||
cached_models = await get_upstream_models_from_cache(provider.id, api_key_id)
|
||||
if cached_models is not None:
|
||||
return {
|
||||
"success": True,
|
||||
@@ -107,64 +271,25 @@ async def query_available_models(
|
||||
}
|
||||
|
||||
# 缓存未命中或强制刷新,实时获取
|
||||
|
||||
# 构建 api_format -> endpoint 映射
|
||||
format_to_endpoint: dict[str, ProviderEndpoint] = {}
|
||||
for endpoint in provider.endpoints:
|
||||
if endpoint.is_active:
|
||||
format_to_endpoint[endpoint.api_format] = endpoint
|
||||
|
||||
if not format_to_endpoint:
|
||||
raise HTTPException(status_code=400, detail="No active endpoints found for this provider")
|
||||
|
||||
# 获取 API Key
|
||||
if request.api_key_id:
|
||||
# 指定了特定的 API Key
|
||||
api_key = next(
|
||||
(key for key in provider.api_keys if key.id == request.api_key_id),
|
||||
None
|
||||
)
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=404, detail="API Key not found")
|
||||
else:
|
||||
# 使用第一个可用的 Key
|
||||
api_key = next(
|
||||
(key for key in provider.api_keys if key.is_active),
|
||||
None
|
||||
)
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=400, detail="No active API Key found for this provider")
|
||||
|
||||
try:
|
||||
api_key_value = crypto_service.decrypt(api_key.api_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt API key: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to decrypt API key")
|
||||
|
||||
# 使用公共函数构建所有格式的端点配置并获取模型
|
||||
endpoint_configs = build_all_format_configs(api_key_value, format_to_endpoint) # type: ignore[arg-type]
|
||||
endpoint_configs = build_all_format_configs(api_key_value, format_to_endpoint)
|
||||
all_models, errors, has_success = await fetch_models_from_endpoints(endpoint_configs)
|
||||
|
||||
# 按 model id + api_format 去重(保留第一个)
|
||||
seen_keys: set[str] = set()
|
||||
unique_models: list = []
|
||||
for model in all_models:
|
||||
model_id = model.get("id")
|
||||
api_format = model.get("api_format", "")
|
||||
unique_key = f"{model_id}:{api_format}"
|
||||
if model_id and unique_key not in seen_keys:
|
||||
seen_keys.add(unique_key)
|
||||
unique_models.append(model)
|
||||
# 按 model id 聚合,合并所有 api_format
|
||||
unique_models = _aggregate_models_by_id(all_models)
|
||||
|
||||
error = "; ".join(errors) if errors else None
|
||||
if not unique_models and not error:
|
||||
error = "No models returned from any endpoint"
|
||||
|
||||
# 如果指定了 api_key_id 且获取成功,写入缓存
|
||||
if request.api_key_id and unique_models:
|
||||
await set_upstream_models_to_cache(
|
||||
request.provider_id, request.api_key_id, unique_models
|
||||
)
|
||||
# 获取成功时写入缓存
|
||||
if unique_models:
|
||||
await set_upstream_models_to_cache(provider.id, api_key_id, unique_models)
|
||||
|
||||
return {
|
||||
"success": len(unique_models) > 0,
|
||||
|
||||
Reference in New Issue
Block a user