feat: 按上游计费倍率调度 OpenAI 账号

This commit is contained in:
Tian Lee
2026-07-16 00:40:46 +08:00
parent 0765d10c1d
commit 90ee85f3ef
34 changed files with 2299 additions and 252 deletions
+110 -2
View File
@@ -4020,7 +4020,61 @@
<Toggle v-model="form.allow_ungrouped_key_scheduling" />
</div>
<div class="flex items-center justify-between">
<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.lowRatePriorityTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{
t("admin.settings.openaiExperimentalScheduler.lowRatePriorityDescription")
}}
</p>
</div>
<Toggle
v-model="form.openai_low_upstream_rate_priority_enabled"
data-testid="openai-low-rate-priority-toggle"
/>
</div>
<div
v-if="!form.openai_advanced_scheduler_enabled && form.openai_low_upstream_rate_priority_enabled"
class="flex flex-col items-stretch gap-3 border-t border-gray-100 pt-5 sm:flex-row sm:items-start sm:justify-between sm:gap-6 dark:border-dark-700"
>
<div class="min-w-0">
<label
class="text-sm font-medium text-gray-700 dark:text-gray-300"
for="openai-oauth-scheduling-rate-multiplier"
>
{{ t("admin.settings.openaiExperimentalScheduler.oauthRateTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.openaiExperimentalScheduler.oauthRatePriorityDescription") }}
</p>
</div>
<div class="relative w-full shrink-0 sm:w-32">
<input
id="openai-oauth-scheduling-rate-multiplier"
v-model.number="form.openai_oauth_scheduling_rate_multiplier"
class="input pr-8"
data-testid="openai-oauth-scheduling-rate-multiplier"
min="0"
required
step="0.01"
type="number"
/>
<span
class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-400"
>x</span>
</div>
</div>
<div 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"
@@ -4033,7 +4087,10 @@
}}
</p>
</div>
<Toggle v-model="form.openai_advanced_scheduler_enabled" />
<Toggle
v-model="form.openai_advanced_scheduler_enabled"
data-testid="openai-advanced-scheduler-toggle"
/>
</div>
<div
@@ -4074,6 +4131,38 @@
<Toggle v-model="form.openai_advanced_scheduler_subscription_priority_enabled" />
</div>
<div
v-if="form.openai_advanced_scheduler_enabled"
class="flex flex-col items-stretch gap-3 border-t border-gray-100 pt-5 sm:flex-row sm:items-start sm:justify-between sm:gap-6 dark:border-dark-700"
>
<div class="min-w-0">
<label
class="text-sm font-medium text-gray-700 dark:text-gray-300"
for="openai-oauth-scheduling-rate-multiplier"
>
{{ t("admin.settings.openaiExperimentalScheduler.oauthRateTitle") }}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.settings.openaiExperimentalScheduler.oauthRateWeightedDescription") }}
</p>
</div>
<div class="relative w-full shrink-0 sm:w-32">
<input
id="openai-oauth-scheduling-rate-multiplier"
v-model.number="form.openai_oauth_scheduling_rate_multiplier"
class="input pr-8"
data-testid="openai-oauth-scheduling-rate-multiplier"
min="0"
required
step="0.01"
type="number"
/>
<span
class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-sm text-gray-400"
>x</span>
</div>
</div>
<div
v-if="form.openai_advanced_scheduler_enabled"
class="border-t border-gray-100 pt-5 dark:border-dark-700"
@@ -8039,6 +8128,8 @@ type SettingsForm = Omit<
github_oauth_client_secret: string;
google_oauth_client_secret: string;
force_email_on_third_party_signup: boolean;
openai_low_upstream_rate_priority_enabled: boolean;
openai_oauth_scheduling_rate_multiplier: number;
openai_advanced_scheduler_enabled: boolean;
openai_advanced_scheduler_sticky_weighted_enabled: boolean;
openai_advanced_scheduler_subscription_priority_enabled: boolean;
@@ -8050,6 +8141,7 @@ type SettingsForm = Omit<
openai_advanced_scheduler_weight_ttft: string;
openai_advanced_scheduler_weight_reset: string;
openai_advanced_scheduler_weight_quota_headroom: string;
openai_advanced_scheduler_weight_upstream_cost: string;
openai_advanced_scheduler_weight_previous_response: string;
openai_advanced_scheduler_weight_session_sticky: string;
// 系统全局平台限额 map;form 内始终归一化为全 4 平台对象(模板非空绑定依赖此不变量)
@@ -8244,6 +8336,8 @@ const form = reactive<SettingsForm>({
max_claude_code_version: "",
// 分组隔离
allow_ungrouped_key_scheduling: false,
openai_low_upstream_rate_priority_enabled: false,
openai_oauth_scheduling_rate_multiplier: 1,
openai_advanced_scheduler_enabled: false,
openai_advanced_scheduler_sticky_weighted_enabled: false,
openai_advanced_scheduler_subscription_priority_enabled: false,
@@ -8255,6 +8349,7 @@ const form = reactive<SettingsForm>({
openai_advanced_scheduler_weight_ttft: "",
openai_advanced_scheduler_weight_reset: "",
openai_advanced_scheduler_weight_quota_headroom: "",
openai_advanced_scheduler_weight_upstream_cost: "",
openai_advanced_scheduler_weight_previous_response: "",
openai_advanced_scheduler_weight_session_sticky: "",
// Gateway forwarding behavior
@@ -8303,6 +8398,7 @@ type OpenAIAdvancedSchedulerOverrideKey =
| "openai_advanced_scheduler_weight_ttft"
| "openai_advanced_scheduler_weight_reset"
| "openai_advanced_scheduler_weight_quota_headroom"
| "openai_advanced_scheduler_weight_upstream_cost"
| "openai_advanced_scheduler_weight_previous_response"
| "openai_advanced_scheduler_weight_session_sticky";
@@ -8315,6 +8411,7 @@ type OpenAIAdvancedSchedulerEffectiveKey =
| "openai_advanced_scheduler_effective_weight_ttft"
| "openai_advanced_scheduler_effective_weight_reset"
| "openai_advanced_scheduler_effective_weight_quota_headroom"
| "openai_advanced_scheduler_effective_weight_upstream_cost"
| "openai_advanced_scheduler_effective_weight_previous_response"
| "openai_advanced_scheduler_effective_weight_session_sticky";
@@ -8378,6 +8475,11 @@ const openAIAdvancedSchedulerWeightFields = computed<
label: t("admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_quota_headroom", "0"),
},
{
key: "openai_advanced_scheduler_weight_upstream_cost",
label: t("admin.settings.openaiExperimentalScheduler.upstreamCostWeight"),
placeholder: placeholder("openai_advanced_scheduler_effective_weight_upstream_cost", "0"),
},
{
key: "openai_advanced_scheduler_weight_previous_response",
label: t("admin.settings.openaiExperimentalScheduler.previousResponseWeight"),
@@ -9622,6 +9724,10 @@ async function saveSettings() {
payment_cancel_rate_limit_window_mode:
form.payment_cancel_rate_limit_window_mode,
payment_alipay_force_qrcode: form.payment_alipay_force_qrcode,
openai_low_upstream_rate_priority_enabled:
form.openai_low_upstream_rate_priority_enabled,
openai_oauth_scheduling_rate_multiplier:
form.openai_oauth_scheduling_rate_multiplier,
openai_advanced_scheduler_enabled: form.openai_advanced_scheduler_enabled,
openai_advanced_scheduler_sticky_weighted_enabled:
form.openai_advanced_scheduler_sticky_weighted_enabled,
@@ -9643,6 +9749,8 @@ async function saveSettings() {
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_upstream_cost:
form.openai_advanced_scheduler_weight_upstream_cost.trim(),
openai_advanced_scheduler_weight_previous_response:
form.openai_advanced_scheduler_weight_previous_response.trim(),
openai_advanced_scheduler_weight_session_sticky:
@@ -161,6 +161,11 @@ vi.mock("vue-i18n", async () => {
"admin.settings.payment.findProvider": "查看支持的支付方式",
"admin.settings.openaiExperimentalScheduler.title": "OpenAI 实验调度策略",
"admin.settings.openaiExperimentalScheduler.description": "默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。",
"admin.settings.openaiExperimentalScheduler.lowRatePriorityTitle": "低倍率优先",
"admin.settings.openaiExperimentalScheduler.lowRatePriorityDescription": "开启后优先选择计费倍率较低的账号;倍率相同时,再比较账号优先级和当前负载等。启用实验调度策略后,此开关不生效。",
"admin.settings.openaiExperimentalScheduler.oauthRateTitle": "OAuth 调度参考倍率",
"admin.settings.openaiExperimentalScheduler.oauthRatePriorityDescription": "同一分组同时包含 API Key 和 OAuth 账号时,OAuth 账号按此倍率与已探测的 API Key 计费倍率一起排序。",
"admin.settings.openaiExperimentalScheduler.oauthRateWeightedDescription": "同一分组同时包含 API Key 和 OAuth 账号时,计算“计费倍率”得分时,OAuth 账号按此倍率参与计算。",
"admin.settings.openaiExperimentalScheduler.stickyWeightedTitle": "粘性加权",
"admin.settings.openaiExperimentalScheduler.stickyWeightedDescription": "开启后 previous_response_id 和 session_hash 粘性进入高级调度打分;关闭时仍按旧逻辑硬命中粘性账号。",
"admin.settings.openaiExperimentalScheduler.subscriptionPriorityTitle": "订阅优先",
@@ -176,6 +181,7 @@ vi.mock("vue-i18n", async () => {
"admin.settings.openaiExperimentalScheduler.ttftWeight": "首包延迟",
"admin.settings.openaiExperimentalScheduler.resetWeight": "重置窗口",
"admin.settings.openaiExperimentalScheduler.quotaHeadroomWeight": "额度余量",
"admin.settings.openaiExperimentalScheduler.upstreamCostWeight": "计费倍率",
"admin.settings.openaiExperimentalScheduler.previousResponseWeight": "previous_response 粘性",
"admin.settings.openaiExperimentalScheduler.sessionStickyWeight": "session_hash 粘性",
"admin.settings.site.uploadImage": "上传图片",
@@ -428,6 +434,8 @@ const baseSettingsResponse = {
payment_visible_method_wxpay_source: "invalid-source",
payment_visible_method_alipay_enabled: true,
payment_visible_method_wxpay_enabled: true,
openai_low_upstream_rate_priority_enabled: false,
openai_oauth_scheduling_rate_multiplier: 1,
openai_advanced_scheduler_enabled: false,
openai_advanced_scheduler_sticky_weighted_enabled: false,
openai_advanced_scheduler_subscription_priority_enabled: false,
@@ -439,6 +447,7 @@ const baseSettingsResponse = {
openai_advanced_scheduler_weight_ttft: "",
openai_advanced_scheduler_weight_reset: "",
openai_advanced_scheduler_weight_quota_headroom: "",
openai_advanced_scheduler_weight_upstream_cost: "",
openai_advanced_scheduler_weight_previous_response: "",
openai_advanced_scheduler_weight_session_sticky: "",
openai_advanced_scheduler_effective_lb_top_k: "7",
@@ -449,6 +458,7 @@ const baseSettingsResponse = {
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_upstream_cost: "0",
openai_advanced_scheduler_effective_weight_previous_response: "5",
openai_advanced_scheduler_effective_weight_session_sticky: "3",
balance_low_notify_enabled: false,
@@ -838,6 +848,66 @@ describe("admin SettingsView payment visible method controls", () => {
expect(wrapper.text()).not.toContain("OpenAI 高级调度器");
});
it("places and explains rate controls for both scheduling modes", async () => {
const wrapper = mountView();
await flushPromises();
expect(
wrapper.find('[data-testid="openai-oauth-scheduling-rate-multiplier"]').exists(),
).toBe(false);
const lowRateToggle = wrapper.get('[data-testid="openai-low-rate-priority-toggle"]');
await lowRateToggle.setValue(true);
const priorityModeText = wrapper.text();
expect(priorityModeText).toContain(
"同一分组同时包含 API Key 和 OAuth 账号时,OAuth 账号按此倍率与已探测的 API Key 计费倍率一起排序。",
);
expect(priorityModeText.indexOf("低倍率优先")).toBeLessThan(
priorityModeText.indexOf("OAuth 调度参考倍率"),
);
expect(priorityModeText.indexOf("OAuth 调度参考倍率")).toBeLessThan(
priorityModeText.indexOf("OpenAI 实验调度策略"),
);
const oauthRateInput = wrapper.get(
'[data-testid="openai-oauth-scheduling-rate-multiplier"]',
);
await oauthRateInput.setValue("0.05");
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
openai_low_upstream_rate_priority_enabled: true,
openai_oauth_scheduling_rate_multiplier: 0.05,
}),
);
await wrapper
.get('[data-testid="openai-advanced-scheduler-toggle"]')
.setValue(true);
expect(
wrapper.find('[data-testid="openai-low-rate-priority-toggle"]').exists(),
).toBe(false);
expect(
wrapper.find('[data-testid="openai-oauth-scheduling-rate-multiplier"]').exists(),
).toBe(true);
const weightedModeText = wrapper.text();
expect(weightedModeText).toContain(
"同一分组同时包含 API Key 和 OAuth 账号时,计算“计费倍率”得分时,OAuth 账号按此倍率参与计算。",
);
expect(weightedModeText).not.toContain(
"OAuth 账号按此倍率与已探测的 API Key 计费倍率一起排序。",
);
expect(weightedModeText.indexOf("订阅优先")).toBeLessThan(
weightedModeText.indexOf("OAuth 调度参考倍率"),
);
expect(weightedModeText.indexOf("OAuth 调度参考倍率")).toBeLessThan(
weightedModeText.indexOf("调度权值覆盖"),
);
expect(weightedModeText).toContain("计费倍率");
});
it("passes translated upload and remove labels to the payment help image uploader", async () => {
const wrapper = mountView();