feat: add OpenAI advanced scheduler controls

Related: #1089, #408, #123
This commit is contained in:
linshuboy
2026-07-05 17:24:38 +08:00
parent b650bdd68d
commit f26ca5661e
30 changed files with 3158 additions and 580 deletions
+48 -1
View File
@@ -322,6 +322,29 @@
<template #cell-priority="{ value }">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}</span>
</template>
<template #header-scheduler_score="{ column }">
<div class="flex items-center">
<span>{{ column.label }}</span>
<HelpTooltip :content="t('admin.accounts.schedulerScore.hint')" width-class="w-80" />
</div>
</template>
<template #cell-scheduler_score="{ row }">
<div v-if="getSchedulerScoreRows(row).length" class="flex min-w-[7rem] flex-col gap-0.5 font-mono text-[11px] leading-4">
<div
v-for="score in getSchedulerScoreRows(row)"
:key="String(score.group_id)"
class="flex items-center gap-1 whitespace-nowrap text-gray-700 dark:text-gray-300"
:title="`${formatSchedulerScoreGroup(score)} / ${formatSchedulerScore(score.base_score)} / ${formatStickySchedulerScore(score)}`"
>
<span class="max-w-[4.75rem] truncate text-gray-500 dark:text-dark-400">{{ formatSchedulerScoreGroup(score) }}</span>
<span class="text-gray-300 dark:text-gray-600">/</span>
<span>{{ formatSchedulerScore(score.base_score) }}</span>
<span class="text-gray-300 dark:text-gray-600">/</span>
<span class="text-primary-700 dark:text-primary-300">{{ formatStickySchedulerScore(score) }}</span>
</div>
</div>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
</template>
<template #cell-last_used_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
</template>
@@ -441,7 +464,7 @@ import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfil
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { formatDateTime, formatRelativeTime } from '@/utils/format'
import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry'
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
import type { Account, AccountPlatform, AccountSchedulerGroupScore, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
const { t } = useI18n()
const appStore = useAppStore()
@@ -643,6 +666,29 @@ const autoRefreshIntervalLabel = (sec: number) => {
return `${sec}s`
}
const formatSchedulerScore = (value: unknown): string => {
const num = Number(value)
if (!Number.isFinite(num)) return '-'
return num.toFixed(6).replace(/\.?0+$/, '')
}
const formatStickySchedulerScore = (score: AccountSchedulerGroupScore): string => {
if (!score) return '-'
if (score.sticky_score_infinity) return '+∞'
return formatSchedulerScore(score.sticky_score)
}
const getSchedulerScoreRows = (account: Account): AccountSchedulerGroupScore[] => {
if (!Array.isArray(account.scheduler_scores)) return []
return account.scheduler_scores.filter(score => score.group_id != null)
}
const formatSchedulerScoreGroup = (score: AccountSchedulerGroupScore): string => {
if ('group_name' in score && score.group_name) return score.group_name
if ('group_id' in score && score.group_id != null) return `#${score.group_id}`
return '-'
}
const loadSavedColumns = () => {
try {
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
@@ -1166,6 +1212,7 @@ const allColumns = computed(() => {
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
{ key: 'proxy', label: t('admin.accounts.columns.proxy'), sortable: false },
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
{ key: 'scheduler_score', label: t('admin.accounts.columns.schedulerScore'), sortable: false },
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'created_at', label: t('admin.accounts.columns.createdAt'), sortable: true },
+220
View File
@@ -4014,6 +4014,81 @@
</div>
<Toggle v-model="form.openai_advanced_scheduler_enabled" />
</div>
<div
v-if="form.openai_advanced_scheduler_enabled"
class="flex items-center justify-between border-t border-gray-100 pt-5 dark:border-dark-700"
>
<div>
<label
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.openaiExperimentalScheduler.stickyWeightedTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{
t("admin.settings.openaiExperimentalScheduler.stickyWeightedDescription")
}}
</p>
</div>
<Toggle v-model="form.openai_advanced_scheduler_sticky_weighted_enabled" />
</div>
<div
v-if="form.openai_advanced_scheduler_enabled"
class="flex items-center justify-between border-t border-gray-100 pt-5 dark:border-dark-700"
>
<div>
<label
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.openaiExperimentalScheduler.subscriptionPriorityTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{
t("admin.settings.openaiExperimentalScheduler.subscriptionPriorityDescription")
}}
</p>
</div>
<Toggle v-model="form.openai_advanced_scheduler_subscription_priority_enabled" />
</div>
<div
v-if="form.openai_advanced_scheduler_enabled"
class="border-t border-gray-100 pt-5 dark:border-dark-700"
>
<div>
<label
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.settings.openaiExperimentalScheduler.weightsTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{
t("admin.settings.openaiExperimentalScheduler.weightsDescription")
}}
</p>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5">
<label
v-for="field in openAIAdvancedSchedulerWeightFields"
:key="field.key"
class="block"
>
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">
{{ field.label }}
</span>
<input
v-model="form[field.key]"
class="input mt-1"
inputmode="decimal"
:placeholder="field.placeholder"
type="text"
/>
</label>
</div>
</div>
</div>
</div>
@@ -7903,6 +7978,18 @@ type SettingsForm = Omit<
google_oauth_client_secret: string;
force_email_on_third_party_signup: boolean;
openai_advanced_scheduler_enabled: boolean;
openai_advanced_scheduler_sticky_weighted_enabled: boolean;
openai_advanced_scheduler_subscription_priority_enabled: boolean;
openai_advanced_scheduler_lb_top_k: string;
openai_advanced_scheduler_weight_priority: string;
openai_advanced_scheduler_weight_load: string;
openai_advanced_scheduler_weight_queue: string;
openai_advanced_scheduler_weight_error_rate: string;
openai_advanced_scheduler_weight_ttft: string;
openai_advanced_scheduler_weight_reset: string;
openai_advanced_scheduler_weight_quota_headroom: string;
openai_advanced_scheduler_weight_previous_response: string;
openai_advanced_scheduler_weight_session_sticky: string;
// 系统全局平台限额 map;form 内始终归一化为全 4 平台对象(模板非空绑定依赖此不变量)
default_platform_quotas: DefaultPlatformQuotasMap;
};
@@ -8094,6 +8181,18 @@ const form = reactive<SettingsForm>({
// 分组隔离
allow_ungrouped_key_scheduling: false,
openai_advanced_scheduler_enabled: false,
openai_advanced_scheduler_sticky_weighted_enabled: false,
openai_advanced_scheduler_subscription_priority_enabled: false,
openai_advanced_scheduler_lb_top_k: "",
openai_advanced_scheduler_weight_priority: "",
openai_advanced_scheduler_weight_load: "",
openai_advanced_scheduler_weight_queue: "",
openai_advanced_scheduler_weight_error_rate: "",
openai_advanced_scheduler_weight_ttft: "",
openai_advanced_scheduler_weight_reset: "",
openai_advanced_scheduler_weight_quota_headroom: "",
openai_advanced_scheduler_weight_previous_response: "",
openai_advanced_scheduler_weight_session_sticky: "",
// Gateway forwarding behavior
enable_fingerprint_unification: true,
enable_metadata_passthrough: false,
@@ -8131,6 +8230,103 @@ const form = reactive<SettingsForm>({
allow_user_view_error_requests: false,
});
type OpenAIAdvancedSchedulerOverrideKey =
| "openai_advanced_scheduler_lb_top_k"
| "openai_advanced_scheduler_weight_priority"
| "openai_advanced_scheduler_weight_load"
| "openai_advanced_scheduler_weight_queue"
| "openai_advanced_scheduler_weight_error_rate"
| "openai_advanced_scheduler_weight_ttft"
| "openai_advanced_scheduler_weight_reset"
| "openai_advanced_scheduler_weight_quota_headroom"
| "openai_advanced_scheduler_weight_previous_response"
| "openai_advanced_scheduler_weight_session_sticky";
type OpenAIAdvancedSchedulerEffectiveKey =
| "openai_advanced_scheduler_effective_lb_top_k"
| "openai_advanced_scheduler_effective_weight_priority"
| "openai_advanced_scheduler_effective_weight_load"
| "openai_advanced_scheduler_effective_weight_queue"
| "openai_advanced_scheduler_effective_weight_error_rate"
| "openai_advanced_scheduler_effective_weight_ttft"
| "openai_advanced_scheduler_effective_weight_reset"
| "openai_advanced_scheduler_effective_weight_quota_headroom"
| "openai_advanced_scheduler_effective_weight_previous_response"
| "openai_advanced_scheduler_effective_weight_session_sticky";
const openAIAdvancedSchedulerWeightFields = computed<
Array<{
key: OpenAIAdvancedSchedulerOverrideKey;
label: string;
placeholder: string;
}>
>(() => {
const placeholder = (
effectiveKey: OpenAIAdvancedSchedulerEffectiveKey,
fallbackValue: string,
) => {
const effectiveValue = String(
(form as Record<string, unknown>)[effectiveKey] ?? "",
).trim();
return t("admin.settings.openaiExperimentalScheduler.defaultPlaceholder", {
value: effectiveValue || fallbackValue,
});
};
return [
{
key: "openai_advanced_scheduler_lb_top_k",
label: t("admin.settings.openaiExperimentalScheduler.topKLabel"),
placeholder: placeholder("openai_advanced_scheduler_effective_lb_top_k", "7"),
},
{
key: "openai_advanced_scheduler_weight_priority",
label: t("admin.settings.openaiExperimentalScheduler.priorityWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_priority", "1"),
},
{
key: "openai_advanced_scheduler_weight_load",
label: t("admin.settings.openaiExperimentalScheduler.loadWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_load", "1"),
},
{
key: "openai_advanced_scheduler_weight_queue",
label: t("admin.settings.openaiExperimentalScheduler.queueWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_queue", "0.7"),
},
{
key: "openai_advanced_scheduler_weight_error_rate",
label: t("admin.settings.openaiExperimentalScheduler.errorRateWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_error_rate", "0.8"),
},
{
key: "openai_advanced_scheduler_weight_ttft",
label: t("admin.settings.openaiExperimentalScheduler.ttftWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_ttft", "0.5"),
},
{
key: "openai_advanced_scheduler_weight_reset",
label: t("admin.settings.openaiExperimentalScheduler.resetWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_reset", "0"),
},
{
key: "openai_advanced_scheduler_weight_quota_headroom",
label: t("admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_quota_headroom", "0"),
},
{
key: "openai_advanced_scheduler_weight_previous_response",
label: t("admin.settings.openaiExperimentalScheduler.previousResponseWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_previous_response", "5"),
},
{
key: "openai_advanced_scheduler_weight_session_sticky",
label: t("admin.settings.openaiExperimentalScheduler.sessionStickyWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_session_sticky", "3"),
},
];
});
const authSourceDefaults = reactive<AuthSourceDefaultsState>(
buildAuthSourceDefaultsState({}),
);
@@ -9359,6 +9555,30 @@ async function saveSettings() {
form.payment_cancel_rate_limit_window_mode,
payment_alipay_force_qrcode: form.payment_alipay_force_qrcode,
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
openai_advanced_scheduler_sticky_weighted_enabled:
form.openai_advanced_scheduler_sticky_weighted_enabled,
openai_advanced_scheduler_subscription_priority_enabled:
form.openai_advanced_scheduler_subscription_priority_enabled,
openai_advanced_scheduler_lb_top_k:
form.openai_advanced_scheduler_lb_top_k.trim(),
openai_advanced_scheduler_weight_priority:
form.openai_advanced_scheduler_weight_priority.trim(),
openai_advanced_scheduler_weight_load:
form.openai_advanced_scheduler_weight_load.trim(),
openai_advanced_scheduler_weight_queue:
form.openai_advanced_scheduler_weight_queue.trim(),
openai_advanced_scheduler_weight_error_rate:
form.openai_advanced_scheduler_weight_error_rate.trim(),
openai_advanced_scheduler_weight_ttft:
form.openai_advanced_scheduler_weight_ttft.trim(),
openai_advanced_scheduler_weight_reset:
form.openai_advanced_scheduler_weight_reset.trim(),
openai_advanced_scheduler_weight_quota_headroom:
form.openai_advanced_scheduler_weight_quota_headroom.trim(),
openai_advanced_scheduler_weight_previous_response:
form.openai_advanced_scheduler_weight_previous_response.trim(),
openai_advanced_scheduler_weight_session_sticky:
form.openai_advanced_scheduler_weight_session_sticky.trim(),
// 余额、订阅到期与账号限额通知
balance_low_notify_enabled: form.balance_low_notify_enabled,
balance_low_notify_threshold:
@@ -161,6 +161,23 @@ vi.mock("vue-i18n", async () => {
"admin.settings.payment.findProvider": "查看支持的支付方式",
"admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略",
"admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。",
"admin.settings.openaiExperimentalScheduler.stickyWeightedTitle": "粘性加权",
"admin.settings.openaiExperimentalScheduler.stickyWeightedDescription": "开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。",
"admin.settings.openaiExperimentalScheduler.subscriptionPriorityTitle": "订阅优先",
"admin.settings.openaiExperimentalScheduler.subscriptionPriorityDescription": "开启后先在 ChatGPT 订阅账号池中按权值选取;订阅池拿不到席位时再回退到非订阅账号池。",
"admin.settings.openaiExperimentalScheduler.weightsTitle": "调度权值覆盖",
"admin.settings.openaiExperimentalScheduler.weightsDescription": "留空时使用配置/环境变量值;配置未设置时使用内置默认值。页面非空设置优先。",
"admin.settings.openaiExperimentalScheduler.defaultPlaceholder": "配置/默认:{value}",
"admin.settings.openaiExperimentalScheduler.topKLabel": "TopK",
"admin.settings.openaiExperimentalScheduler.priorityWeight": "优先级",
"admin.settings.openaiExperimentalScheduler.loadWeight": "负载",
"admin.settings.openaiExperimentalScheduler.queueWeight": "排队",
"admin.settings.openaiExperimentalScheduler.errorRateWeight": "错误率",
"admin.settings.openaiExperimentalScheduler.ttftWeight": "首包延迟",
"admin.settings.openaiExperimentalScheduler.resetWeight": "重置窗口",
"admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight": "额度余量",
"admin.settings.openaiExperimentalScheduler.previousResponseWeight": "previous_response 粘性",
"admin.settings.openaiExperimentalScheduler.sessionStickyWeight": "session_hash 粘性",
"admin.settings.site.uploadImage": "上传图片",
"admin.settings.site.remove": "移除",
"admin.settings.platformQuota.platform": "平台",
@@ -411,6 +428,28 @@ const baseSettingsResponse = {
payment_visible_method_alipay_enabled: true,
payment_visible_method_wxpay_enabled: true,
openai_advanced_scheduler_enabled: false,
openai_advanced_scheduler_sticky_weighted_enabled: false,
openai_advanced_scheduler_subscription_priority_enabled: false,
openai_advanced_scheduler_lb_top_k: "",
openai_advanced_scheduler_weight_priority: "",
openai_advanced_scheduler_weight_load: "",
openai_advanced_scheduler_weight_queue: "",
openai_advanced_scheduler_weight_error_rate: "",
openai_advanced_scheduler_weight_ttft: "",
openai_advanced_scheduler_weight_reset: "",
openai_advanced_scheduler_weight_quota_headroom: "",
openai_advanced_scheduler_weight_previous_response: "",
openai_advanced_scheduler_weight_session_sticky: "",
openai_advanced_scheduler_effective_lb_top_k: "7",
openai_advanced_scheduler_effective_weight_priority: "1",
openai_advanced_scheduler_effective_weight_load: "1",
openai_advanced_scheduler_effective_weight_queue: "0.7",
openai_advanced_scheduler_effective_weight_error_rate: "0.8",
openai_advanced_scheduler_effective_weight_ttft: "0.5",
openai_advanced_scheduler_effective_weight_reset: "0",
openai_advanced_scheduler_effective_weight_quota_headroom: "0",
openai_advanced_scheduler_effective_weight_previous_response: "5",
openai_advanced_scheduler_effective_weight_session_sticky: "3",
balance_low_notify_enabled: false,
balance_low_notify_threshold: 0,
balance_low_notify_recharge_url: "",