mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
feat: 按上游计费倍率调度 OpenAI 账号
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user