refactor: add scheduling mode support and optimize system settings UI

- Add fixed_order and cache_affinity scheduling modes to CacheAwareScheduler
- Only apply cache affinity in cache_affinity mode; use fixed order otherwise
- Simplify Dialog components with title/description props
- Remove unnecessary button shadows in SystemSettings
- Optimize import dialog UI structure
- Update ModelAliasesTab shadow styling
- Fix fallback orchestrator type hints
- Add scheduling_mode configuration in system config
This commit is contained in:
fawney19
2025-12-17 19:15:08 +08:00
parent 53e6a82480
commit 9d5c84f9d3
12 changed files with 468 additions and 343 deletions

View File

@@ -34,11 +34,10 @@ const buttonClass = computed(() => {
'inline-flex items-center justify-center rounded-xl text-sm font-semibold transition-all duration-200 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]'
const variantClasses = {
default:
'bg-primary text-white shadow-[0_20px_35px_rgba(204,120,92,0.35)] hover:bg-primary/90 hover:shadow-[0_25px_45px_rgba(204,120,92,0.45)]',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85 shadow-sm',
default: 'bg-primary text-white hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/85',
outline:
'border border-border/60 bg-card/60 text-foreground hover:border-primary/60 hover:text-primary hover:bg-primary/10 shadow-sm backdrop-blur transition-all',
'border border-border/60 bg-card/60 text-foreground hover:border-primary/60 hover:text-primary hover:bg-primary/10 backdrop-blur transition-all',
secondary:
'bg-secondary text-secondary-foreground shadow-inner hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',

View File

@@ -2,7 +2,7 @@
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 overflow-y-auto"
class="fixed inset-0 overflow-y-auto pointer-events-none"
:style="{ zIndex: containerZIndex }"
>
<!-- 背景遮罩 -->
@@ -16,7 +16,7 @@
>
<div
v-if="isOpen"
class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity"
class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity pointer-events-auto"
:style="{ zIndex: backdropZIndex }"
@click="handleClose"
/>
@@ -34,7 +34,7 @@
>
<div
v-if="isOpen"
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border"
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border pointer-events-auto"
:style="{ zIndex: contentZIndex }"
:class="maxWidthClass"
@click.stop

View File

@@ -45,7 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
const contentClass = computed(() =>
cn(
'z-[100] max-h-96 min-w-[8rem] overflow-hidden rounded-2xl border border-border bg-card text-foreground shadow-2xl backdrop-blur-xl pointer-events-auto',
'z-[200] max-h-96 min-w-[8rem] overflow-hidden rounded-2xl border border-border bg-card text-foreground shadow-2xl backdrop-blur-xl pointer-events-auto',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class

View File

@@ -312,9 +312,42 @@
<template #footer>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-4">
<div class="text-xs text-muted-foreground">
当前模式: <span class="font-medium">{{ activeMainTab === 'provider' ? '提供商优先' : 'Key 优先' }}</span>
</div>
<div class="flex items-center gap-2 pl-4 border-l border-border">
<span class="text-xs text-muted-foreground">调度:</span>
<div class="flex gap-0.5 p-0.5 bg-muted/40 rounded-md">
<button
type="button"
class="px-2 py-1 text-xs font-medium rounded transition-all"
:class="[
schedulingMode === 'fixed_order'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
]"
title="严格按优先级顺序,不考虑缓存"
@click="schedulingMode = 'fixed_order'"
>
固定顺序
</button>
<button
type="button"
class="px-2 py-1 text-xs font-medium rounded transition-all"
:class="[
schedulingMode === 'cache_affinity'
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
]"
title="优先使用已缓存的Provider利用Prompt Cache"
@click="schedulingMode = 'cache_affinity'"
>
缓存亲和
</button>
</div>
</div>
</div>
<div class="flex gap-2">
<Button
size="sm"
@@ -410,6 +443,9 @@ const saving = ref(false)
// Key 优先级编辑状态
const editingKeyPriority = ref<Record<string, string | null>>({}) // format -> keyId
// 调度模式状态
const schedulingMode = ref<'fixed_order' | 'cache_affinity'>('cache_affinity')
// 可用的 API 格式
const availableFormats = computed(() => {
return Object.keys(keysByFormat.value).sort()
@@ -433,11 +469,18 @@ watch(internalOpen, async (open) => {
// 加载当前的优先级模式配置
async function loadCurrentPriorityMode() {
try {
const response = await adminApi.getSystemConfig('provider_priority_mode')
const currentMode = response.value || 'provider'
const [priorityResponse, schedulingResponse] = await Promise.all([
adminApi.getSystemConfig('provider_priority_mode'),
adminApi.getSystemConfig('scheduling_mode')
])
const currentMode = priorityResponse.value || 'provider'
activeMainTab.value = currentMode === 'global_key' ? 'key' : 'provider'
const currentSchedulingMode = schedulingResponse.value || 'cache_affinity'
schedulingMode.value = currentSchedulingMode === 'fixed_order' ? 'fixed_order' : 'cache_affinity'
} catch {
activeMainTab.value = 'provider'
schedulingMode.value = 'cache_affinity'
}
}
@@ -611,11 +654,19 @@ async function save() {
const newMode = activeMainTab.value === 'key' ? 'global_key' : 'provider'
await adminApi.updateSystemConfig(
// 保存优先级模式和调度模式
await Promise.all([
adminApi.updateSystemConfig(
'provider_priority_mode',
newMode,
'Provider/Key 优先级策略provider(提供商优先模式) 或 global_key(全局Key优先模式)'
),
adminApi.updateSystemConfig(
'scheduling_mode',
schedulingMode.value,
'调度模式fixed_order(固定顺序模式) 或 cache_affinity(缓存亲和模式)'
)
])
const providerUpdates = sortedProviders.value.map((provider, index) =>
updateProvider(provider.id, { provider_priority: index + 1 })

View File

@@ -286,7 +286,9 @@
@click="addUpstreamModel(model.id)"
>
<div class="flex-1 min-w-0">
<div class="font-mono text-xs truncate">{{ model.id }}</div>
<div class="font-mono text-xs truncate">
{{ model.id }}
</div>
<div
v-if="model.owned_by"
class="text-xs text-muted-foreground truncate"

View File

@@ -7,6 +7,7 @@
<template #actions>
<Button
:disabled="loading"
class="shadow-none hover:shadow-none"
@click="saveSystemConfig"
>
{{ loading ? '保存中...' : '保存所有配置' }}
@@ -465,16 +466,12 @@
</div>
<!-- 导入配置对话框 -->
<Dialog v-model:open="importDialogOpen">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle>导入配置</DialogTitle>
<DialogDescription>
选择冲突处理模式并确认导入
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<Dialog
v-model:open="importDialogOpen"
title="导入配置"
description="选择冲突处理模式并确认导入"
>
<div class="space-y-4">
<div
v-if="importPreview"
class="p-3 bg-muted rounded-lg text-sm"
@@ -496,7 +493,10 @@
<div>
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
<Select v-model="mergeMode">
<Select
v-model="mergeMode"
v-model:open="mergeModeSelectOpen"
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -525,17 +525,15 @@
</p>
</div>
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<p class="text-sm text-yellow-600 dark:text-yellow-400">
<p class="text-xs text-muted-foreground">
注意相同的 API Keys 会自动跳过不会创建重复记录
</p>
</div>
</div>
<DialogFooter>
<template #footer>
<Button
variant="outline"
@click="importDialogOpen = false"
@click="importDialogOpen = false; mergeModeSelectOpen = false"
>
取消
</Button>
@@ -545,20 +543,17 @@
>
{{ importLoading ? '导入中...' : '确认导入' }}
</Button>
</DialogFooter>
</DialogContent>
</template>
</Dialog>
<!-- 导入结果对话框 -->
<Dialog v-model:open="importResultDialogOpen">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle>导入完成</DialogTitle>
</DialogHeader>
<Dialog
v-model:open="importResultDialogOpen"
title="导入完成"
>
<div
v-if="importResult"
class="space-y-4 py-4"
class="space-y-4"
>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="p-3 bg-muted rounded-lg">
@@ -614,12 +609,12 @@
<div
v-if="importResult.stats.errors.length > 0"
class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg"
class="p-3 bg-destructive/10 rounded-lg"
>
<p class="font-medium text-red-600 dark:text-red-400 mb-2">
<p class="font-medium text-destructive mb-2">
警告信息
</p>
<ul class="text-sm text-red-600 dark:text-red-400 space-y-1">
<ul class="text-sm text-destructive space-y-1">
<li
v-for="(err, index) in importResult.stats.errors"
:key="index"
@@ -630,25 +625,20 @@
</div>
</div>
<DialogFooter>
<template #footer>
<Button @click="importResultDialogOpen = false">
确定
</Button>
</DialogFooter>
</DialogContent>
</template>
</Dialog>
<!-- 用户数据导入对话框 -->
<Dialog v-model:open="importUsersDialogOpen">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle>导入用户数据</DialogTitle>
<DialogDescription>
选择冲突处理模式并确认导入
</DialogDescription>
</DialogHeader>
<div class="space-y-4 py-4">
<Dialog
v-model:open="importUsersDialogOpen"
title="导入用户数据"
description="选择冲突处理模式并确认导入"
>
<div class="space-y-4">
<div
v-if="importUsersPreview"
class="p-3 bg-muted rounded-lg text-sm"
@@ -666,7 +656,10 @@
<div>
<Label class="block text-sm font-medium mb-2">冲突处理模式</Label>
<Select v-model="usersMergeMode">
<Select
v-model="usersMergeMode"
v-model:open="usersMergeModeSelectOpen"
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -695,17 +688,15 @@
</p>
</div>
<div class="p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<p class="text-sm text-yellow-600 dark:text-yellow-400">
<p class="text-xs text-muted-foreground">
注意用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY 环境变量才能正常工作
</p>
</div>
</div>
<DialogFooter>
<template #footer>
<Button
variant="outline"
@click="importUsersDialogOpen = false"
@click="importUsersDialogOpen = false; usersMergeModeSelectOpen = false"
>
取消
</Button>
@@ -715,20 +706,17 @@
>
{{ importUsersLoading ? '导入中...' : '确认导入' }}
</Button>
</DialogFooter>
</DialogContent>
</template>
</Dialog>
<!-- 用户数据导入结果对话框 -->
<Dialog v-model:open="importUsersResultDialogOpen">
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle>用户数据导入完成</DialogTitle>
</DialogHeader>
<Dialog
v-model:open="importUsersResultDialogOpen"
title="用户数据导入完成"
>
<div
v-if="importUsersResult"
class="space-y-4 py-4"
class="space-y-4"
>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="p-3 bg-muted rounded-lg">
@@ -754,12 +742,12 @@
<div
v-if="importUsersResult.stats.errors.length > 0"
class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg"
class="p-3 bg-destructive/10 rounded-lg"
>
<p class="font-medium text-red-600 dark:text-red-400 mb-2">
<p class="font-medium text-destructive mb-2">
警告信息
</p>
<ul class="text-sm text-red-600 dark:text-red-400 space-y-1">
<ul class="text-sm text-destructive space-y-1">
<li
v-for="(err, index) in importUsersResult.stats.errors"
:key="index"
@@ -770,12 +758,11 @@
</div>
</div>
<DialogFooter>
<template #footer>
<Button @click="importUsersResultDialogOpen = false">
确定
</Button>
</DialogFooter>
</DialogContent>
</template>
</Dialog>
</PageContainer>
</template>
@@ -794,11 +781,6 @@ import SelectContent from '@/components/ui/select-content.vue'
import SelectItem from '@/components/ui/select-item.vue'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from '@/components/ui'
import { PageHeader, PageContainer, CardSection } from '@/components/layout'
import { useToast } from '@/composables/useToast'
@@ -843,6 +825,7 @@ const configFileInput = ref<HTMLInputElement | null>(null)
const importPreview = ref<ConfigExportData | null>(null)
const importResult = ref<ConfigImportResponse | null>(null)
const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
const mergeModeSelectOpen = ref(false)
// 用户数据导出/导入相关
const exportUsersLoading = ref(false)
@@ -853,6 +836,7 @@ const usersFileInput = ref<HTMLInputElement | null>(null)
const importUsersPreview = ref<UsersExportData | null>(null)
const importUsersResult = ref<UsersImportResponse | null>(null)
const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
const usersMergeModeSelectOpen = ref(false)
const systemConfig = ref<SystemConfig>({
// 基础配置
@@ -1136,6 +1120,7 @@ async function confirmImport() {
})
importResult.value = result
importDialogOpen.value = false
mergeModeSelectOpen.value = false
importResultDialogOpen.value = true
success('配置导入成功')
} catch (err: any) {
@@ -1224,6 +1209,7 @@ async function confirmImportUsers() {
})
importUsersResult.value = result
importUsersDialogOpen.value = false
usersMergeModeSelectOpen.value = false
importUsersResultDialogOpen.value = true
success('用户数据导入成功')
} catch (err: any) {

View File

@@ -103,7 +103,7 @@
</div>
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
<Clock class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
<Clock class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
平均响应
@@ -114,7 +114,7 @@
</div>
</Card>
<Card class="relative p-3 sm:p-4 border-kraft/30">
<AlertTriangle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
<AlertTriangle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
错误率
@@ -128,7 +128,7 @@
</div>
</Card>
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
<Shuffle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
<Shuffle class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
转移次数
@@ -142,7 +142,7 @@
v-if="costStats"
class="relative p-3 sm:p-4 border-manilla/40"
>
<DollarSign class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
<DollarSign class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
实际成本
@@ -180,7 +180,7 @@
</div>
<div class="grid grid-cols-2 gap-2 sm:gap-3 xl:grid-cols-4">
<Card class="relative p-3 sm:p-4 border-book-cloth/30">
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
缓存命中率
@@ -191,7 +191,7 @@
</div>
</Card>
<Card class="relative p-3 sm:p-4 border-kraft/30">
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
缓存读取
@@ -202,7 +202,7 @@
</div>
</Card>
<Card class="relative p-3 sm:p-4 border-book-cloth/25">
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-kraft" />
<Database class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
缓存创建
@@ -216,7 +216,7 @@
v-if="tokenBreakdown"
class="relative p-3 sm:p-4 border-manilla/40"
>
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-book-cloth" />
<Hash class="absolute top-3 right-3 h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
<div class="pr-6">
<p class="text-[9px] sm:text-[10px] font-semibold uppercase tracking-[0.2em] sm:tracking-[0.3em] text-muted-foreground">
总Token
@@ -254,16 +254,16 @@
<Card class="overflow-hidden p-4 flex flex-col flex-1 min-h-0 h-full max-h-[280px] sm:max-h-none">
<div
v-if="loadingAnnouncements"
class="py-8 text-center"
class="flex-1 flex items-center justify-center"
>
<Loader2 class="h-5 w-5 animate-spin mx-auto text-muted-foreground" />
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
</div>
<div
v-else-if="announcements.length === 0"
class="py-8 text-center"
class="flex-1 flex flex-col items-center justify-center"
>
<Bell class="h-8 w-8 mx-auto text-muted-foreground/40" />
<Bell class="h-8 w-8 text-muted-foreground/40" />
<p class="mt-2 text-xs text-muted-foreground">
暂无公告
</p>
@@ -793,9 +793,8 @@ const statCardGlows = [
'bg-kraft/30'
]
const getStatIconColor = (index: number): string => {
const colors = ['text-book-cloth', 'text-kraft', 'text-book-cloth', 'text-kraft']
return colors[index % colors.length]
const getStatIconColor = (_index: number): string => {
return 'text-muted-foreground'
}
// 统计数据

View File

@@ -21,7 +21,8 @@ from src.core.logger import logger
from src.database import get_db
from src.models.database import ApiKey, User
from src.services.cache.affinity_manager import get_affinity_manager
from src.services.cache.aware_scheduler import get_cache_aware_scheduler
from src.services.cache.aware_scheduler import CacheAwareScheduler, get_cache_aware_scheduler
from src.services.system.config import SystemConfigService
router = APIRouter(prefix="/api/admin/monitoring/cache", tags=["Admin - Monitoring: Cache"])
pipeline = ApiRequestPipeline()
@@ -250,7 +251,22 @@ class AdminCacheStatsAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Dict[str, Any]: # type: ignore[override]
try:
redis_client = get_redis_client_sync()
scheduler = await get_cache_aware_scheduler(redis_client)
# 读取系统配置,确保监控接口与编排器使用一致的模式
priority_mode = SystemConfigService.get_config(
context.db,
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
scheduling_mode = SystemConfigService.get_config(
context.db,
"scheduling_mode",
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
)
scheduler = await get_cache_aware_scheduler(
redis_client,
priority_mode=priority_mode,
scheduling_mode=scheduling_mode,
)
stats = await scheduler.get_stats()
logger.info("缓存统计信息查询成功")
context.add_audit_metadata(
@@ -270,7 +286,22 @@ class AdminCacheMetricsAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> PlainTextResponse:
try:
redis_client = get_redis_client_sync()
scheduler = await get_cache_aware_scheduler(redis_client)
# 读取系统配置,确保监控接口与编排器使用一致的模式
priority_mode = SystemConfigService.get_config(
context.db,
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
scheduling_mode = SystemConfigService.get_config(
context.db,
"scheduling_mode",
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
)
scheduler = await get_cache_aware_scheduler(
redis_client,
priority_mode=priority_mode,
scheduling_mode=scheduling_mode,
)
stats = await scheduler.get_stats()
payload = self._format_prometheus(stats)
context.add_audit_metadata(

View File

@@ -852,7 +852,7 @@ class AdminImportConfigAdapter(AdminApiAdapter):
from src.services.cache.invalidation import get_cache_invalidation_service
cache_service = get_cache_invalidation_service()
cache_service.invalidate_all()
cache_service.clear_all_caches()
return {
"message": "配置导入成功",

View File

@@ -121,8 +121,17 @@ class CacheAwareScheduler:
PRIORITY_MODE_PROVIDER,
PRIORITY_MODE_GLOBAL_KEY,
}
# 调度模式常量
SCHEDULING_MODE_FIXED_ORDER = "fixed_order" # 固定顺序模式
SCHEDULING_MODE_CACHE_AFFINITY = "cache_affinity" # 缓存亲和模式
ALLOWED_SCHEDULING_MODES = {
SCHEDULING_MODE_FIXED_ORDER,
SCHEDULING_MODE_CACHE_AFFINITY,
}
def __init__(self, redis_client=None, priority_mode: Optional[str] = None):
def __init__(
self, redis_client=None, priority_mode: Optional[str] = None, scheduling_mode: Optional[str] = None
):
"""
初始化调度器
@@ -132,12 +141,16 @@ class CacheAwareScheduler:
Args:
redis_client: Redis客户端可选
priority_mode: 候选排序策略provider | global_key
scheduling_mode: 调度模式fixed_order | cache_affinity
"""
self.redis = redis_client
self.priority_mode = self._normalize_priority_mode(
priority_mode or self.PRIORITY_MODE_PROVIDER
)
logger.debug(f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}")
self.scheduling_mode = self._normalize_scheduling_mode(
scheduling_mode or self.SCHEDULING_MODE_CACHE_AFFINITY
)
logger.debug(f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}, 调度模式: {self.scheduling_mode}")
# 初始化子组件(将在第一次使用时异步初始化)
self._affinity_manager: Optional[CacheAffinityManager] = None
@@ -673,7 +686,8 @@ class CacheAwareScheduler:
f"(api_format={target_format.value}, model={model_name})"
)
# 4. 应用缓存亲和性排序(使用 global_model_id 作为模型标识
# 4. 应用缓存亲和性排序(仅在缓存亲和模式下启用
if self.scheduling_mode == self.SCHEDULING_MODE_CACHE_AFFINITY:
if affinity_key and candidates:
candidates = await self._apply_cache_affinity(
candidates=candidates,
@@ -681,6 +695,10 @@ class CacheAwareScheduler:
api_format=target_format,
global_model_id=global_model_id,
)
else:
# 固定顺序模式:标记所有候选为非缓存
for candidate in candidates:
candidate.is_cached = False
return candidates, global_model_id
@@ -1060,6 +1078,22 @@ class CacheAwareScheduler:
self.priority_mode = normalized
logger.debug(f"[CacheAwareScheduler] 切换优先级模式为: {self.priority_mode}")
def _normalize_scheduling_mode(self, mode: Optional[str]) -> str:
normalized = (mode or "").strip().lower()
if normalized not in self.ALLOWED_SCHEDULING_MODES:
if normalized:
logger.warning(f"[CacheAwareScheduler] 无效的调度模式 '{mode}',回退为 cache_affinity")
return self.SCHEDULING_MODE_CACHE_AFFINITY
return normalized
def set_scheduling_mode(self, mode: Optional[str]) -> None:
"""运行时更新调度模式"""
normalized = self._normalize_scheduling_mode(mode)
if normalized == self.scheduling_mode:
return
self.scheduling_mode = normalized
logger.debug(f"[CacheAwareScheduler] 切换调度模式为: {self.scheduling_mode}")
def _apply_priority_mode_sort(
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None
) -> List[ProviderCandidate]:
@@ -1307,6 +1341,7 @@ _scheduler: Optional[CacheAwareScheduler] = None
async def get_cache_aware_scheduler(
redis_client=None,
priority_mode: Optional[str] = None,
scheduling_mode: Optional[str] = None,
) -> CacheAwareScheduler:
"""
获取全局CacheAwareScheduler实例
@@ -1317,6 +1352,7 @@ async def get_cache_aware_scheduler(
Args:
redis_client: Redis客户端可选
priority_mode: 外部覆盖的优先级模式provider | global_key
scheduling_mode: 外部覆盖的调度模式fixed_order | cache_affinity
Returns:
CacheAwareScheduler实例
@@ -1324,8 +1360,13 @@ async def get_cache_aware_scheduler(
global _scheduler
if _scheduler is None:
_scheduler = CacheAwareScheduler(redis_client, priority_mode=priority_mode)
elif priority_mode:
_scheduler = CacheAwareScheduler(
redis_client, priority_mode=priority_mode, scheduling_mode=scheduling_mode
)
else:
if priority_mode:
_scheduler.set_priority_mode(priority_mode)
if scheduling_mode:
_scheduler.set_scheduling_mode(scheduling_mode)
return _scheduler

View File

@@ -102,9 +102,15 @@ class FallbackOrchestrator:
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
scheduling_mode = SystemConfigService.get_config(
self.db,
"scheduling_mode",
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
)
self.cache_scheduler = await get_cache_aware_scheduler(
self.redis,
priority_mode=priority_mode,
scheduling_mode=scheduling_mode,
)
else:
# 确保运行时配置变更能生效
@@ -113,7 +119,13 @@ class FallbackOrchestrator:
"provider_priority_mode",
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
)
scheduling_mode = SystemConfigService.get_config(
self.db,
"scheduling_mode",
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
)
self.cache_scheduler.set_priority_mode(priority_mode)
self.cache_scheduler.set_scheduling_mode(scheduling_mode)
# 确保 cache_scheduler 内部组件也已初始化
await self.cache_scheduler._ensure_initialized()

View File

@@ -71,6 +71,10 @@ class SystemConfigService:
"value": "provider",
"description": "优先级策略provider(提供商优先模式) 或 global_key(全局Key优先模式)",
},
"scheduling_mode": {
"value": "cache_affinity",
"description": "调度模式fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式优先使用已缓存的Provider)",
},
"auto_delete_expired_keys": {
"value": False,
"description": "是否自动删除过期的API KeyTrue=物理删除False=仅禁用),仅管理员可配置",