refactor: 将上游模型缓存从前端迁移到后端 Redis

- 后端:定时任务刷新时将上游模型写入 Redis 缓存
- 后端:provider_query 优先从缓存读取,支持 force_refresh 参数
- 后端:auto_fetch_models 开启时改为同步获取,确保前端能立即看到数据
- 前端:移除本地缓存,只保留并发请求去重逻辑
- 前端:KeyAllowedModelsEditDialog 添加刷新上游模型按钮
This commit is contained in:
fawney19
2026-01-16 19:10:59 +08:00
parent b2d08f964d
commit 019e148ddb
7 changed files with 148 additions and 112 deletions

View File

@@ -219,6 +219,7 @@ export interface ProviderModelsQueryResponse {
api_format?: string api_format?: string
}> }>
error?: string error?: string
from_cache?: boolean
} }
provider: { provider: {
id: string id: string
@@ -478,10 +479,10 @@ export const adminApi = {
}, },
// 查询 Provider 可用模型(从上游 API 获取) // 查询 Provider 可用模型(从上游 API 获取)
async queryProviderModels(providerId: string, apiKeyId?: string): Promise<ProviderModelsQueryResponse> { async queryProviderModels(providerId: string, apiKeyId?: string, forceRefresh = false): Promise<ProviderModelsQueryResponse> {
const response = await apiClient.post<ProviderModelsQueryResponse>( const response = await apiClient.post<ProviderModelsQueryResponse>(
'/api/admin/provider-query/models', '/api/admin/provider-query/models',
{ provider_id: providerId, api_key_id: apiKeyId } { provider_id: providerId, api_key_id: apiKeyId, force_refresh: forceRefresh }
) )
return response.data return response.data
}, },

View File

@@ -257,7 +257,7 @@ const emit = defineEmits<{
'changed': [] 'changed': []
}>() }>()
const { fetchModels: fetchCachedModels, clearCache, getCachedModels } = useUpstreamModelsCache() const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
const { error: showError, success } = useToast() const { error: showError, success } = useToast()
const { confirmWarning } = useConfirm() const { confirmWarning } = useConfirm()
@@ -633,24 +633,8 @@ async function loadData() {
// 同步全局模型选择状态 // 同步全局模型选择状态
syncGlobalModelSelection() syncGlobalModelSelection()
// 检查缓存 // 初始折叠状态
const cachedModels = getCachedModels(props.providerId) collapsedGroups.value = new Set()
if (cachedModels && cachedModels.length > 0) {
upstreamModels.value = cachedModels
upstreamModelsLoaded.value = true
// 同步上游模型选择状态
syncUpstreamModelSelection()
// 有多个分组时全部折叠
const allGroups = new Set(['global'])
for (const model of cachedModels) {
if (model.api_format) {
allGroups.add(model.api_format)
}
}
collapsedGroups.value = allGroups
} else {
collapsedGroups.value = new Set()
}
} }
// 加载全局模型列表 // 加载全局模型列表
@@ -677,13 +661,9 @@ async function loadExistingModels() {
// 从提供商获取模型 // 从提供商获取模型
async function fetchUpstreamModels(forceRefresh = false) { async function fetchUpstreamModels(forceRefresh = false) {
if (forceRefresh) {
clearCache(props.providerId)
}
try { try {
fetchingUpstreamModels.value = true fetchingUpstreamModels.value = true
const result = await fetchCachedModels(props.providerId, forceRefresh) const result = await fetchCachedModels(props.providerId, undefined, forceRefresh)
if (result) { if (result) {
if (result.error) { if (result.error) {
showError(result.error, '错误') showError(result.error, '错误')

View File

@@ -40,10 +40,23 @@
> >
已选 {{ selectedModels.length }} 已选 {{ selectedModels.length }}
</span> </span>
<Loader2 <!-- 刷新上游模型按钮 -->
v-if="fetchingUpstreamModels" <button
class="w-4 h-4 animate-spin text-muted-foreground shrink-0" type="button"
/> class="h-6 w-6 flex items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
:disabled="fetchingUpstreamModels"
title="刷新上游模型"
@click="refreshUpstreamModels"
>
<RefreshCw
v-if="!fetchingUpstreamModels"
class="w-3.5 h-3.5"
/>
<Loader2
v-else
class="w-3.5 h-3.5 animate-spin"
/>
</button>
</div> </div>
<!-- 分组列表 --> <!-- 分组列表 -->
@@ -318,7 +331,8 @@ import {
Check, Check,
ChevronDown, ChevronDown,
Lock, Lock,
LockOpen LockOpen,
RefreshCw
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { Dialog, Button, Input } from '@/components/ui' import { Dialog, Button, Input } from '@/components/ui'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
@@ -627,11 +641,11 @@ async function loadGlobalModels() {
} }
// 从提供商获取模型(使用缓存) // 从提供商获取模型(使用缓存)
async function fetchUpstreamModels() { async function fetchUpstreamModels(forceRefresh = false) {
if (!props.providerId || !props.apiKey) return if (!props.providerId || !props.apiKey) return
try { try {
fetchingUpstreamModels.value = true fetchingUpstreamModels.value = true
const result = await fetchCachedModels(props.providerId, props.apiKey.id) const result = await fetchCachedModels(props.providerId, props.apiKey.id, forceRefresh)
if (loadingCancelled) return if (loadingCancelled) return
if (result.models.length > 0) { if (result.models.length > 0) {
upstreamModels.value = result.models upstreamModels.value = result.models
@@ -647,6 +661,14 @@ async function fetchUpstreamModels() {
} }
} }
// 手动刷新上游模型(强制跳过缓存)
async function refreshUpstreamModels() {
await fetchUpstreamModels(true)
if (upstreamModels.value.length > 0) {
success('上游模型已刷新')
}
}
// 解析 allowed_models // 解析 allowed_models
function parseAllowedModels(allowed: AllowedModels): string[] { function parseAllowedModels(allowed: AllowedModels): string[] {
if (allowed === null || allowed === undefined) { if (allowed === null || allowed === undefined) {

View File

@@ -1,25 +1,16 @@
/** /**
* 上游模型缓存 - 共享缓存,避免重复请求 * 上游模型获取服务
*
* 缓存已移至后端Redis前端只保留并发请求去重避免同时发多个相同请求。
*/ */
import { ref } from 'vue' import { ref } from 'vue'
import { adminApi } from '@/api/admin' import { adminApi } from '@/api/admin'
import { parseUpstreamModelError } from '@/utils/errorParser' import { parseUpstreamModelError } from '@/utils/errorParser'
import type { UpstreamModel } from '@/api/endpoints/types' import type { UpstreamModel } from '@/api/endpoints/types'
// 扩展类型,包含可能的额外字段
export type { UpstreamModel } export type { UpstreamModel }
interface CacheEntry { type FetchResult = { models: UpstreamModel[]; error?: string; fromCache?: boolean }
models: UpstreamModel[]
timestamp: number
}
type FetchResult = { models: UpstreamModel[]; error?: string }
// 全局缓存(模块级别,所有组件共享)
// 支持两种 key: providerId 或 providerId:apiKeyId
const cache = new Map<string, CacheEntry>()
const CACHE_TTL = 5 * 60 * 1000 // 5分钟
// 进行中的请求(用于去重并发请求) // 进行中的请求(用于去重并发请求)
const pendingRequests = new Map<string, Promise<FetchResult>>() const pendingRequests = new Map<string, Promise<FetchResult>>()
@@ -28,9 +19,9 @@ const pendingRequests = new Map<string, Promise<FetchResult>>()
const loadingMap = ref<Map<string, boolean>>(new Map()) const loadingMap = ref<Map<string, boolean>>(new Map())
/** /**
* 生成缓存 key * 生成请求 key
*/ */
function getCacheKey(providerId: string, apiKeyId?: string): string { function getRequestKey(providerId: string, apiKeyId?: string): string {
return apiKeyId ? `${providerId}:${apiKeyId}` : providerId return apiKeyId ? `${providerId}:${apiKeyId}` : providerId
} }
@@ -39,93 +30,59 @@ export function useUpstreamModelsCache() {
* 获取上游模型列表 * 获取上游模型列表
* @param providerId 提供商ID * @param providerId 提供商ID
* @param apiKeyId 可选的 API Key ID用于获取特定 Key 支持的模型) * @param apiKeyId 可选的 API Key ID用于获取特定 Key 支持的模型)
* @param forceRefresh 是否强制刷新 * @param forceRefresh 是否强制刷新(跳过后端缓存)
* @returns 模型列表或 null如果请求失败 * @returns 模型列表或错误信息
*/ */
async function fetchModels( async function fetchModels(
providerId: string, providerId: string,
apiKeyId?: string, apiKeyId?: string,
forceRefresh = false forceRefresh = false
): Promise<FetchResult> { ): Promise<FetchResult> {
const cacheKey = getCacheKey(providerId, apiKeyId) const requestKey = getRequestKey(providerId, apiKeyId)
// 检查缓存 // 强制刷新时不复用进行中的请求
if (!forceRefresh) { if (!forceRefresh && pendingRequests.has(requestKey)) {
const cached = cache.get(cacheKey) return pendingRequests.get(requestKey)!
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return { models: cached.models }
}
}
// 检查是否有进行中的请求(非强制刷新时复用)
if (!forceRefresh && pendingRequests.has(cacheKey)) {
return pendingRequests.get(cacheKey)!
} }
// 创建新请求 // 创建新请求
const requestPromise = (async (): Promise<FetchResult> => { const requestPromise = (async (): Promise<FetchResult> => {
try { try {
loadingMap.value.set(cacheKey, true) loadingMap.value.set(requestKey, true)
const response = await adminApi.queryProviderModels(providerId, apiKeyId) const response = await adminApi.queryProviderModels(providerId, apiKeyId, forceRefresh)
if (response.success && response.data?.models) { if (response.success && response.data?.models) {
// 存入缓存 return {
cache.set(cacheKey, {
models: response.data.models, models: response.data.models,
timestamp: Date.now() fromCache: response.data.from_cache
}) }
return { models: response.data.models }
} else { } else {
// 使用友好的错误解析
const rawError = response.data?.error || '获取上游模型失败' const rawError = response.data?.error || '获取上游模型失败'
return { models: [], error: parseUpstreamModelError(rawError) } return { models: [], error: parseUpstreamModelError(rawError) }
} }
} catch (err: any) { } catch (err: any) {
// 使用友好的错误解析
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败' const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
return { models: [], error: parseUpstreamModelError(rawError) } return { models: [], error: parseUpstreamModelError(rawError) }
} finally { } finally {
loadingMap.value.set(cacheKey, false) loadingMap.value.set(requestKey, false)
pendingRequests.delete(cacheKey) pendingRequests.delete(requestKey)
} }
})() })()
pendingRequests.set(cacheKey, requestPromise) pendingRequests.set(requestKey, requestPromise)
return requestPromise return requestPromise
} }
/**
* 获取缓存的模型(不发起请求)
*/
function getCachedModels(providerId: string, apiKeyId?: string): UpstreamModel[] | null {
const cacheKey = getCacheKey(providerId, apiKeyId)
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.models
}
return null
}
/**
* 清除指定提供商/Key的缓存
*/
function clearCache(providerId: string, apiKeyId?: string) {
const cacheKey = getCacheKey(providerId, apiKeyId)
cache.delete(cacheKey)
}
/** /**
* 检查是否正在加载 * 检查是否正在加载
*/ */
function isLoading(providerId: string, apiKeyId?: string): boolean { function isLoading(providerId: string, apiKeyId?: string): boolean {
const cacheKey = getCacheKey(providerId, apiKeyId) const requestKey = getRequestKey(providerId, apiKeyId)
return loadingMap.value.get(cacheKey) || false return loadingMap.value.get(requestKey) || false
} }
return { return {
fetchModels, fetchModels,
getCachedModels,
clearCache,
isLoading, isLoading,
loadingMap loadingMap
} }

View File

@@ -256,16 +256,14 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
# 处理 auto_fetch_models 的开启和关闭 # 处理 auto_fetch_models 的开启和关闭
if not auto_fetch_enabled_before and auto_fetch_enabled_after: if not auto_fetch_enabled_before and auto_fetch_enabled_after:
# 刚刚开启了 auto_fetch_models立即触发一次模型获取 # 刚刚开启了 auto_fetch_models同步执行模型获取
logger.info("[AUTO_FETCH] Key %s 开启自动获取模型,立即触发模型获取", self.key_id) logger.info("[AUTO_FETCH] Key %s 开启自动获取模型,同步执行模型获取", self.key_id)
try: try:
from src.services.model.fetch_scheduler import get_model_fetch_scheduler from src.services.model.fetch_scheduler import get_model_fetch_scheduler
scheduler = get_model_fetch_scheduler() scheduler = get_model_fetch_scheduler()
# 在后台异步执行,不阻塞当前请求 # 同步等待模型获取完成,确保前端刷新时能看到最新数据
import asyncio await scheduler._fetch_models_for_key_by_id(self.key_id)
asyncio.create_task(scheduler._fetch_models_for_key_by_id(self.key_id))
except Exception as e: except Exception as e:
logger.error(f"触发模型获取失败: {e}") logger.error(f"触发模型获取失败: {e}")
# 不抛出异常,避免影响 Key 更新操作 # 不抛出异常,避免影响 Key 更新操作
@@ -630,17 +628,15 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
f"Formats={self.key_data.api_formats}, Key=***{self.key_data.api_key[-4:]}, ID={new_key.id}" f"Formats={self.key_data.api_formats}, Key=***{self.key_data.api_key[-4:]}, ID={new_key.id}"
) )
# 如果开启了 auto_fetch_models立即触发一次模型获取 # 如果开启了 auto_fetch_models同步执行模型获取
if self.key_data.auto_fetch_models: if self.key_data.auto_fetch_models:
logger.info("[AUTO_FETCH] 新 Key %s 开启自动获取模型,立即触发模型获取", new_key.id) logger.info("[AUTO_FETCH] 新 Key %s 开启自动获取模型,同步执行模型获取", new_key.id)
try: try:
from src.services.model.fetch_scheduler import get_model_fetch_scheduler from src.services.model.fetch_scheduler import get_model_fetch_scheduler
scheduler = get_model_fetch_scheduler() scheduler = get_model_fetch_scheduler()
# 在后台异步执行,不阻塞当前请求 # 同步等待模型获取完成,确保前端刷新时能看到最新数据
import asyncio await scheduler._fetch_models_for_key_by_id(new_key.id)
asyncio.create_task(scheduler._fetch_models_for_key_by_id(new_key.id))
except Exception as e: except Exception as e:
logger.error(f"触发模型获取失败: {e}") logger.error(f"触发模型获取失败: {e}")
# 不抛出异常,避免影响 Key 创建操作 # 不抛出异常,避免影响 Key 创建操作

View File

@@ -33,6 +33,7 @@ class ModelsQueryRequest(BaseModel):
provider_id: str provider_id: str
api_key_id: Optional[str] = None api_key_id: Optional[str] = None
force_refresh: bool = False # 强制刷新,跳过缓存
class TestModelRequest(BaseModel): class TestModelRequest(BaseModel):
@@ -73,6 +74,8 @@ async def query_available_models(
""" """
查询提供商可用模型 查询提供商可用模型
优先从缓存获取(缓存由定时任务刷新),缓存未命中时实时调用上游 API。
遍历所有活跃端点,根据端点的 API 格式选择正确的 Adapter 进行请求: 遍历所有活跃端点,根据端点的 API 格式选择正确的 Adapter 进行请求:
- OPENAI/OPENAI_CLI: 使用 OpenAIChatAdapter.fetch_models - OPENAI/OPENAI_CLI: 使用 OpenAIChatAdapter.fetch_models
- CLAUDE/CLAUDE_CLI: 使用 ClaudeChatAdapter.fetch_models - CLAUDE/CLAUDE_CLI: 使用 ClaudeChatAdapter.fetch_models
@@ -84,7 +87,12 @@ async def query_available_models(
Returns: Returns:
所有端点的模型列表(合并) 所有端点的模型列表(合并)
""" """
# 获取提供商及其端点和 API Keys from src.services.model.fetch_scheduler import (
get_upstream_models_from_cache,
set_upstream_models_to_cache,
)
# 获取提供商基本信息
provider = ( provider = (
db.query(Provider) db.query(Provider)
.options( .options(
@@ -98,6 +106,26 @@ async def query_available_models(
if not provider: if not provider:
raise HTTPException(status_code=404, detail="Provider not found") raise HTTPException(status_code=404, detail="Provider not found")
# 如果指定了 api_key_id 且不是强制刷新,优先从缓存获取
# 注:不指定 api_key_id 时Provider 级别查询)不使用缓存,因为:
# 1. Provider 级别查询会遍历多个 Key结果不稳定
# 2. 缓存按 Key 粒度存储,与定时任务的刷新逻辑一致
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
)
if cached_models is not None:
return {
"success": True,
"data": {"models": cached_models, "error": None, "from_cache": True},
"provider": {
"id": provider.id,
"name": provider.name,
},
}
# 缓存未命中或强制刷新,实时获取
# 收集所有活跃端点的配置 # 收集所有活跃端点的配置
endpoint_configs: list[dict] = [] endpoint_configs: list[dict] = []
@@ -235,9 +263,15 @@ async def query_available_models(
if not unique_models and not error: if not unique_models and not error:
error = "No models returned from any endpoint" 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
)
return { return {
"success": len(unique_models) > 0, "success": len(unique_models) > 0,
"data": {"models": unique_models, "error": error}, "data": {"models": unique_models, "error": error, "from_cache": False},
"provider": { "provider": {
"id": provider.id, "id": provider.id,
"name": provider.name, "name": provider.name,

View File

@@ -18,6 +18,7 @@ from typing import Optional
import httpx import httpx
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from src.core.cache_service import CacheService
from src.core.crypto import crypto_service from src.core.crypto import crypto_service
from src.core.headers import get_extra_headers_from_endpoint from src.core.headers import get_extra_headers_from_endpoint
from src.core.logger import logger from src.core.logger import logger
@@ -35,6 +36,35 @@ MAX_CONCURRENT_REQUESTS = 5
# 单个 Key 处理的超时时间(秒) # 单个 Key 处理的超时时间(秒)
KEY_FETCH_TIMEOUT_SECONDS = 120 KEY_FETCH_TIMEOUT_SECONDS = 120
# 上游模型缓存 TTL与定时任务间隔保持一致
UPSTREAM_MODELS_CACHE_TTL_SECONDS = MODEL_FETCH_INTERVAL_MINUTES * 60
def _get_upstream_models_cache_key(provider_id: str, api_key_id: str) -> str:
"""生成上游模型缓存的 key"""
return f"upstream_models:{provider_id}:{api_key_id}"
async def get_upstream_models_from_cache(
provider_id: str, api_key_id: str
) -> Optional[list[dict]]:
"""从缓存获取上游模型列表"""
cache_key = _get_upstream_models_cache_key(provider_id, api_key_id)
cached = await CacheService.get(cache_key)
if cached is not None:
logger.debug(f"上游模型缓存命中: {cache_key}")
return cached # type: ignore[no-any-return]
return None
async def set_upstream_models_to_cache(
provider_id: str, api_key_id: str, models: list[dict]
) -> None:
"""将上游模型列表写入缓存"""
cache_key = _get_upstream_models_cache_key(provider_id, api_key_id)
await CacheService.set(cache_key, models, UPSTREAM_MODELS_CACHE_TTL_SECONDS)
logger.debug(f"上游模型已缓存: {cache_key}, 数量={len(models)}")
def _get_adapter_for_format(api_format: str) -> Optional[type]: def _get_adapter_for_format(api_format: str) -> Optional[type]:
"""根据 API 格式获取对应的 Adapter 类""" """根据 API 格式获取对应的 Adapter 类"""
@@ -307,6 +337,22 @@ class ModelFetchScheduler:
f"Provider {provider.name} Key {key.id} 获取到 {len(fetched_model_ids)} 个唯一模型" f"Provider {provider.name} Key {key.id} 获取到 {len(fetched_model_ids)} 个唯一模型"
) )
# 写入上游模型缓存(按 model id + api_format 去重后的完整模型信息)
seen_keys: set[str] = set()
unique_models: list[dict] = []
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)
await set_upstream_models_to_cache(
provider_id, # type: ignore[arg-type]
key.id, # type: ignore[arg-type]
unique_models,
)
# 更新 allowed_models保留 locked_models # 更新 allowed_models保留 locked_models
has_changed = self._update_key_allowed_models(key, fetched_model_ids) has_changed = self._update_key_allowed_models(key, fetched_model_ids)