mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 08:12:26 +08:00
refactor(global-model): migrate model metadata to flexible config structure
将模型配置从多个固定字段(description, official_url, icon_url, default_supports_* 等) 统一为灵活的 config JSON 字段,提高扩展性。同时优化前端模型创建表单,支持从 models-dev 列表直接选择模型快速填充。 主要变更: - 后端:模型表迁移,支持 config JSON 存储模型能力和元信息 - 前端:GlobalModelFormDialog 支持两种创建方式(列表选择/手动填写) - API 类型更新,对齐新的数据结构
This commit is contained in:
244
frontend/src/api/models-dev.ts
Normal file
244
frontend/src/api/models-dev.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Models.dev API 服务
|
||||
* 通过后端代理获取 models.dev 数据(解决跨域问题)
|
||||
*/
|
||||
|
||||
import api from './client'
|
||||
|
||||
// 缓存配置
|
||||
const CACHE_KEY = 'models_dev_cache'
|
||||
const CACHE_DURATION = 15 * 60 * 1000 // 15 分钟
|
||||
|
||||
// Models.dev API 数据结构
|
||||
export interface ModelsDevCost {
|
||||
input?: number
|
||||
output?: number
|
||||
reasoning?: number
|
||||
cache_read?: number
|
||||
}
|
||||
|
||||
export interface ModelsDevLimit {
|
||||
context?: number
|
||||
output?: number
|
||||
}
|
||||
|
||||
export interface ModelsDevModel {
|
||||
id: string
|
||||
name: string
|
||||
family?: string
|
||||
reasoning?: boolean
|
||||
tool_call?: boolean
|
||||
structured_output?: boolean
|
||||
temperature?: boolean
|
||||
attachment?: boolean
|
||||
knowledge?: string
|
||||
release_date?: string
|
||||
last_updated?: string
|
||||
input?: string[] // 输入模态: text, image, audio, video, pdf
|
||||
output?: string[] // 输出模态: text, image, audio
|
||||
open_weights?: boolean
|
||||
cost?: ModelsDevCost
|
||||
limit?: ModelsDevLimit
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface ModelsDevProvider {
|
||||
id: string
|
||||
env?: string[]
|
||||
npm?: string
|
||||
api?: string
|
||||
name: string
|
||||
doc?: string
|
||||
models: Record<string, ModelsDevModel>
|
||||
}
|
||||
|
||||
export type ModelsDevData = Record<string, ModelsDevProvider>
|
||||
|
||||
// 扁平化的模型列表项(用于搜索和选择)
|
||||
export interface ModelsDevModelItem {
|
||||
providerId: string
|
||||
providerName: string
|
||||
modelId: string
|
||||
modelName: string
|
||||
family?: string
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
contextLimit?: number
|
||||
outputLimit?: number
|
||||
supportsVision?: boolean
|
||||
supportsToolCall?: boolean
|
||||
supportsReasoning?: boolean
|
||||
deprecated?: boolean
|
||||
// 用于 display_metadata 的额外字段
|
||||
knowledgeCutoff?: string
|
||||
releaseDate?: string
|
||||
inputModalities?: string[]
|
||||
outputModalities?: string[]
|
||||
}
|
||||
|
||||
interface CacheData {
|
||||
timestamp: number
|
||||
data: ModelsDevData
|
||||
}
|
||||
|
||||
// 内存缓存
|
||||
let memoryCache: CacheData | null = null
|
||||
|
||||
/**
|
||||
* 获取 models.dev 数据(带缓存)
|
||||
*/
|
||||
export async function getModelsDevData(): Promise<ModelsDevData> {
|
||||
// 1. 检查内存缓存
|
||||
if (memoryCache && Date.now() - memoryCache.timestamp < CACHE_DURATION) {
|
||||
return memoryCache.data
|
||||
}
|
||||
|
||||
// 2. 检查 localStorage 缓存
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_KEY)
|
||||
if (cached) {
|
||||
const cacheData: CacheData = JSON.parse(cached)
|
||||
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
|
||||
memoryCache = cacheData
|
||||
return cacheData.data
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 缓存解析失败,忽略
|
||||
}
|
||||
|
||||
// 3. 从后端代理获取新数据
|
||||
const response = await api.get<ModelsDevData>('/api/admin/models/external')
|
||||
const data = response.data
|
||||
|
||||
// 4. 更新缓存
|
||||
const cacheData: CacheData = {
|
||||
timestamp: Date.now(),
|
||||
data,
|
||||
}
|
||||
memoryCache = cacheData
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData))
|
||||
} catch {
|
||||
// localStorage 写入失败,忽略
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扁平化的模型列表
|
||||
*/
|
||||
export async function getModelsDevList(): Promise<ModelsDevModelItem[]> {
|
||||
const data = await getModelsDevData()
|
||||
const items: ModelsDevModelItem[] = []
|
||||
|
||||
for (const [providerId, provider] of Object.entries(data)) {
|
||||
if (!provider.models) continue
|
||||
|
||||
for (const [modelId, model] of Object.entries(provider.models)) {
|
||||
items.push({
|
||||
providerId,
|
||||
providerName: provider.name,
|
||||
modelId,
|
||||
modelName: model.name || modelId,
|
||||
family: model.family,
|
||||
inputPrice: model.cost?.input,
|
||||
outputPrice: model.cost?.output,
|
||||
contextLimit: model.limit?.context,
|
||||
outputLimit: model.limit?.output,
|
||||
supportsVision: model.input?.includes('image'),
|
||||
supportsToolCall: model.tool_call,
|
||||
supportsReasoning: model.reasoning,
|
||||
deprecated: model.deprecated,
|
||||
// display_metadata 相关字段
|
||||
knowledgeCutoff: model.knowledge,
|
||||
releaseDate: model.release_date,
|
||||
inputModalities: model.input,
|
||||
outputModalities: model.output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 按 provider 名称和模型名称排序
|
||||
items.sort((a, b) => {
|
||||
const providerCompare = a.providerName.localeCompare(b.providerName)
|
||||
if (providerCompare !== 0) return providerCompare
|
||||
return a.modelName.localeCompare(b.modelName)
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索模型
|
||||
*/
|
||||
export async function searchModelsDevModels(
|
||||
query: string,
|
||||
options?: {
|
||||
limit?: number
|
||||
excludeDeprecated?: boolean
|
||||
}
|
||||
): Promise<ModelsDevModelItem[]> {
|
||||
const allModels = await getModelsDevList()
|
||||
const { limit = 50, excludeDeprecated = true } = options || {}
|
||||
|
||||
const queryLower = query.toLowerCase()
|
||||
|
||||
const filtered = allModels.filter((model) => {
|
||||
if (excludeDeprecated && model.deprecated) return false
|
||||
|
||||
// 搜索模型 ID、名称、provider 名称、family
|
||||
return (
|
||||
model.modelId.toLowerCase().includes(queryLower) ||
|
||||
model.modelName.toLowerCase().includes(queryLower) ||
|
||||
model.providerName.toLowerCase().includes(queryLower) ||
|
||||
model.family?.toLowerCase().includes(queryLower)
|
||||
)
|
||||
})
|
||||
|
||||
// 排序:精确匹配优先
|
||||
filtered.sort((a, b) => {
|
||||
const aExact =
|
||||
a.modelId.toLowerCase() === queryLower ||
|
||||
a.modelName.toLowerCase() === queryLower
|
||||
const bExact =
|
||||
b.modelId.toLowerCase() === queryLower ||
|
||||
b.modelName.toLowerCase() === queryLower
|
||||
if (aExact && !bExact) return -1
|
||||
if (!aExact && bExact) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return filtered.slice(0, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定模型详情
|
||||
*/
|
||||
export async function getModelsDevModel(
|
||||
providerId: string,
|
||||
modelId: string
|
||||
): Promise<ModelsDevModel | null> {
|
||||
const data = await getModelsDevData()
|
||||
return data[providerId]?.models?.[modelId] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 provider logo URL
|
||||
*/
|
||||
export function getProviderLogoUrl(providerId: string): string {
|
||||
return `https://models.dev/logos/${providerId}.svg`
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export function clearModelsDevCache(): void {
|
||||
memoryCache = null
|
||||
try {
|
||||
localStorage.removeItem(CACHE_KEY)
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user