mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat(models): 添加全局 Key 白名单数据支持模型映射实时匹配
- 新增 GlobalKeyWhitelistItem 类型定义 - 路由预览 API 返回 all_keys_whitelist 字段,包含所有活跃 Provider 的 Key 白名单 - 前端 ModelMappingsTab 改用全局白名单数据进行实时匹配
This commit is contained in:
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user