feat(models): 添加全局 Key 白名单数据支持模型映射实时匹配

- 新增 GlobalKeyWhitelistItem 类型定义
- 路由预览 API 返回 all_keys_whitelist 字段,包含所有活跃 Provider 的 Key 白名单
- 前端 ModelMappingsTab 改用全局白名单数据进行实时匹配
This commit is contained in:
fawney19
2026-01-15 00:35:28 +08:00
parent 154ec94c58
commit 19209181dc
3 changed files with 125 additions and 46 deletions

View File

@@ -687,6 +687,18 @@ export interface RoutingProviderInfo {
active_endpoints: number active_endpoints: number
} }
/**
* 全局 Key 白名单项(用于前端实时匹配)
*/
export interface GlobalKeyWhitelistItem {
key_id: string
key_name: string
masked_key: string
provider_id: string
provider_name: string
allowed_models: string[]
}
/** /**
* 模型请求链路预览响应 * 模型请求链路预览响应
*/ */
@@ -700,4 +712,5 @@ export interface ModelRoutingPreviewResponse {
active_providers: number active_providers: number
scheduling_mode: string scheduling_mode: string
priority_mode: string priority_mode: string
all_keys_whitelist: GlobalKeyWhitelistItem[]
} }

View File

@@ -382,41 +382,36 @@ function matchPattern(pattern: string, text: string): boolean {
} }
} }
// 获取指定映射匹配的 Key 列表 // 获取指定映射匹配的 Key 列表(使用全局 Key 白名单数据做实时匹配)
function getMatchedKeysForMapping(mapping: string): MatchedKeyForMapping[] { function getMatchedKeysForMapping(mapping: string): MatchedKeyForMapping[] {
if (!routingData.value || !mapping.trim()) return [] if (!routingData.value || !mapping.trim()) return []
// 使用 Map 按 keyId 去重并合并匹配结果
const keyMap = new Map<string, MatchedKeyForMapping>() const keyMap = new Map<string, MatchedKeyForMapping>()
for (const provider of routingData.value.providers) { // 使用 all_keys_whitelist 进行实时匹配(包含所有 Provider 的 Key
for (const endpoint of provider.endpoints) { for (const keyItem of routingData.value.all_keys_whitelist || []) {
for (const key of endpoint.keys) { if (!keyItem.allowed_models || keyItem.allowed_models.length === 0) continue
if (!key.allowed_models || key.allowed_models.length === 0) continue
const matchedModels: string[] = [] const matchedModels: string[] = []
for (const allowedModel of key.allowed_models) { for (const allowedModel of keyItem.allowed_models) {
if (matchPattern(mapping, allowedModel)) { if (matchPattern(mapping, allowedModel)) {
matchedModels.push(allowedModel) matchedModels.push(allowedModel)
} }
} }
if (matchedModels.length > 0) { if (matchedModels.length > 0) {
const existing = keyMap.get(key.id) const existing = keyMap.get(keyItem.key_id)
if (existing) { if (existing) {
// 合并匹配结果(去重) const mergedModels = new Set([...existing.matchedModels, ...matchedModels])
const mergedModels = new Set([...existing.matchedModels, ...matchedModels]) existing.matchedModels = Array.from(mergedModels)
existing.matchedModels = Array.from(mergedModels) } else {
} else { keyMap.set(keyItem.key_id, {
keyMap.set(key.id, { keyId: keyItem.key_id,
keyId: key.id, keyName: keyItem.key_name,
keyName: key.name, maskedKey: keyItem.masked_key,
maskedKey: key.masked_key, providerName: keyItem.provider_name,
providerName: provider.name, matchedModels,
matchedModels, })
})
}
}
} }
} }
} }

View File

@@ -17,11 +17,13 @@ from sqlalchemy.orm import Session, selectinload
from src.api.base.admin_adapter import AdminApiAdapter from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.pipeline import ApiRequestPipeline from src.api.base.pipeline import ApiRequestPipeline
from src.core.crypto import CryptoService
from src.core.model_permissions import parse_allowed_models_to_list from src.core.model_permissions import parse_allowed_models_to_list
from src.database import get_db from src.database import get_db
from src.models.database import ( from src.models.database import (
GlobalModel, GlobalModel,
Model, Model,
Provider,
ProviderAPIKey, ProviderAPIKey,
ProviderEndpoint, ProviderEndpoint,
) )
@@ -54,7 +56,9 @@ class RoutingKeyInfo(BaseModel):
allowed_models: Optional[List[str]] = Field(None, description="允许的模型列表null 表示不限制") allowed_models: Optional[List[str]] = Field(None, description="允许的模型列表null 表示不限制")
# 熔断状态 # 熔断状态
circuit_breaker_open: bool = Field(False, description="熔断器是否打开") circuit_breaker_open: bool = Field(False, description="熔断器是否打开")
circuit_breaker_formats: List[str] = Field(default_factory=list, description="熔断的 API 格式列表") circuit_breaker_formats: List[str] = Field(
default_factory=list, description="熔断的 API 格式列表"
)
next_probe_at: Optional[str] = Field(None, description="下次探测时间ISO格式") next_probe_at: Optional[str] = Field(None, description="下次探测时间ISO格式")
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -108,6 +112,19 @@ class RoutingProviderInfo(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
class GlobalKeyWhitelistItem(BaseModel):
"""全局 Key 白名单项(用于前端实时匹配)"""
key_id: str = Field(..., description="Key ID")
key_name: str = Field(..., description="Key 名称")
masked_key: str = Field(..., description="脱敏的 API Key")
provider_id: str = Field(..., description="Provider ID")
provider_name: str = Field(..., description="Provider 名称")
allowed_models: List[str] = Field(default_factory=list, description="Key 白名单模型列表")
model_config = ConfigDict(from_attributes=True)
class ModelRoutingPreviewResponse(BaseModel): class ModelRoutingPreviewResponse(BaseModel):
"""模型请求链路预览响应""" """模型请求链路预览响应"""
@@ -124,6 +141,10 @@ class ModelRoutingPreviewResponse(BaseModel):
# 调度配置 # 调度配置
scheduling_mode: str = Field("cache_affinity", description="调度模式") scheduling_mode: str = Field("cache_affinity", description="调度模式")
priority_mode: str = Field("provider", description="优先级模式") priority_mode: str = Field("provider", description="优先级模式")
# 全局 Key 白名单数据(供前端实时匹配,包含所有 Provider 的 Key
all_keys_whitelist: List[GlobalKeyWhitelistItem] = Field(
default_factory=list, description="所有 Provider 的 Key 白名单数据"
)
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -181,9 +202,7 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
db = context.db db = context.db
# 获取 GlobalModel # 获取 GlobalModel
global_model = ( global_model = db.query(GlobalModel).filter(GlobalModel.id == self.global_model_id).first()
db.query(GlobalModel).filter(GlobalModel.id == self.global_model_id).first()
)
if not global_model: if not global_model:
from fastapi import HTTPException from fastapi import HTTPException
@@ -217,9 +236,7 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
keys_by_provider: Dict[str, List[ProviderAPIKey]] = {} keys_by_provider: Dict[str, List[ProviderAPIKey]] = {}
if provider_ids: if provider_ids:
keys = ( keys = (
db.query(ProviderAPIKey) db.query(ProviderAPIKey).filter(ProviderAPIKey.provider_id.in_(provider_ids)).all()
.filter(ProviderAPIKey.provider_id.in_(provider_ids))
.all()
) )
for key in keys: for key in keys:
if key.provider_id not in keys_by_provider: if key.provider_id not in keys_by_provider:
@@ -282,7 +299,6 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
# 生成脱敏 SK先解密再脱敏 # 生成脱敏 SK先解密再脱敏
masked_key = "" masked_key = ""
if key.api_key: if key.api_key:
from src.core.crypto import CryptoService
crypto = CryptoService() crypto = CryptoService()
try: try:
decrypted_key = crypto.decrypt(key.api_key, silent=True) decrypted_key = crypto.decrypt(key.api_key, silent=True)
@@ -384,16 +400,70 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
active_providers = sum(1 for p in provider_infos if p.is_active and p.model_is_active) active_providers = sum(1 for p in provider_infos if p.is_active and p.model_is_active)
# 从数据库获取当前调度配置 # 从数据库获取当前调度配置
scheduling_mode = SystemConfigService.get_config( scheduling_mode = (
db, SystemConfigService.get_config(
"scheduling_mode", db,
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY, "scheduling_mode",
) or CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
priority_mode = SystemConfigService.get_config( )
db, or CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY
"provider_priority_mode", )
CacheAwareScheduler.PRIORITY_MODE_PROVIDER, priority_mode = (
) or CacheAwareScheduler.PRIORITY_MODE_PROVIDER SystemConfigService.get_config(
db,
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
or CacheAwareScheduler.PRIORITY_MODE_PROVIDER
)
# 获取所有活跃 Provider 的 Key 白名单数据(供前端实时匹配)
all_keys_whitelist: List[GlobalKeyWhitelistItem] = []
crypto = CryptoService()
# 获取所有活跃的 Key带白名单使用 selectinload 避免 N+1 查询
all_keys = (
db.query(ProviderAPIKey)
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
.options(selectinload(ProviderAPIKey.provider))
.filter(ProviderAPIKey.is_active == True)
.filter(Provider.is_active == True)
.filter(ProviderAPIKey.allowed_models.isnot(None)) # 只获取有白名单的 Key
.all()
)
# 转换为白名单数据
for key in all_keys:
if not key.allowed_models:
continue
# 解析白名单
allowed_models_list = parse_allowed_models_to_list(key.allowed_models)
if not allowed_models_list:
continue
# 生成脱敏 Key
masked = ""
if key.api_key:
try:
decrypted = crypto.decrypt(key.api_key, silent=True)
except Exception:
decrypted = key.api_key
if len(decrypted) > 8:
masked = f"{decrypted[:4]}***{decrypted[-4:]}"
else:
masked = f"{decrypted[:2]}***"
all_keys_whitelist.append(
GlobalKeyWhitelistItem(
key_id=key.id or "",
key_name=key.name or "",
masked_key=masked,
provider_id=key.provider_id or "",
provider_name=key.provider.name if key.provider else "",
allowed_models=allowed_models_list,
)
)
return ModelRoutingPreviewResponse( return ModelRoutingPreviewResponse(
global_model_id=global_model.id, global_model_id=global_model.id,
@@ -405,4 +475,5 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
active_providers=active_providers, active_providers=active_providers,
scheduling_mode=scheduling_mode, scheduling_mode=scheduling_mode,
priority_mode=priority_mode, priority_mode=priority_mode,
all_keys_whitelist=all_keys_whitelist,
) )