mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-28 16:09:34 +00:00
Merge pull request #6443 from Ritel-T/pr/group-openai-fast
Add group-level OpenAI Fast enforcement
This commit is contained in:
@@ -1151,6 +1151,11 @@ export default {
|
||||
unsupportedMessage: 'This Sub2API server cannot generate the required Live attestation. Live will not work even if enabled. Continue anyway?',
|
||||
enableAnyway: 'Enable anyway'
|
||||
},
|
||||
openaiFast: {
|
||||
title: 'OpenAI Fast mode',
|
||||
force: 'Force Fast (priority)',
|
||||
hint: 'Forces service_tier=priority on OpenAI requests in this group. The global Fast/Flex policy can still filter or block it. New requests update immediately after saving; existing WebSocket sessions must reconnect.'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: 'Invalid Request Fallback Group',
|
||||
hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.',
|
||||
|
||||
@@ -1149,6 +1149,11 @@ export default {
|
||||
unsupportedMessage: '当前 Sub2API 服务端无法生成 Live 所需的设备证明,即使开启也不能使用。是否仍然开启?',
|
||||
enableAnyway: '仍然开启'
|
||||
},
|
||||
openaiFast: {
|
||||
title: 'OpenAI Fast 模式',
|
||||
force: '强制使用 Fast(priority)',
|
||||
hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: '无效请求兜底分组',
|
||||
hint: '仅当上游明确返回 prompt too long 时才会触发,留空表示不兜底',
|
||||
|
||||
@@ -611,6 +611,7 @@ export interface Group {
|
||||
}
|
||||
|
||||
export interface AdminGroup extends Group {
|
||||
force_openai_fast: boolean
|
||||
model_pricing: import('@/api/admin/channels').ChannelModelPricing[]
|
||||
// 分组利润控制(openai/anthropic/gemini/grok/antigravity 分组可启用;margin/buffer 为小数存储)。
|
||||
// 仅管理员可见:与 rate_multiplier 相乘即可反推上游成本上限,不得下放到 Group。
|
||||
@@ -775,6 +776,7 @@ export interface CreateGroupRequest {
|
||||
weekly_limit_usd?: number | null
|
||||
monthly_limit_usd?: number | null
|
||||
long_context_pricing_enabled?: boolean
|
||||
force_openai_fast?: boolean
|
||||
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
|
||||
allow_image_generation?: boolean
|
||||
allow_batch_image_generation?: boolean
|
||||
@@ -837,6 +839,7 @@ export interface UpdateGroupRequest {
|
||||
weekly_limit_usd?: number | null
|
||||
monthly_limit_usd?: number | null
|
||||
long_context_pricing_enabled?: boolean
|
||||
force_openai_fast?: boolean
|
||||
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
|
||||
allow_image_generation?: boolean
|
||||
allow_batch_image_generation?: boolean
|
||||
|
||||
@@ -1581,6 +1581,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OpenAI Fast 开关(OpenAI 与 Composite 平台) -->
|
||||
<div
|
||||
v-if="supportsGroupOpenAIFast(createForm.platform)"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ t("admin.groups.openaiFast.title") }}
|
||||
</h4>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t("admin.groups.openaiFast.force") }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="createForm.force_openai_fast"
|
||||
:aria-label="t('admin.groups.openaiFast.force')"
|
||||
data-testid="create-force-openai-fast"
|
||||
@click="createForm.force_openai_fast = !createForm.force_openai_fast"
|
||||
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="
|
||||
createForm.force_openai_fast
|
||||
? 'bg-primary-500'
|
||||
: 'bg-gray-300 dark:bg-dark-600'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="
|
||||
createForm.force_openai_fast ? 'translate-x-6' : 'translate-x-1'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ t("admin.groups.openaiFast.hint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
|
||||
<div
|
||||
v-if="supportsLivePlatform(createForm.platform)"
|
||||
@@ -3309,6 +3348,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OpenAI Fast 开关(OpenAI 与 Composite 平台) -->
|
||||
<div
|
||||
v-if="supportsGroupOpenAIFast(editForm.platform)"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{{ t("admin.groups.openaiFast.title") }}
|
||||
</h4>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t("admin.groups.openaiFast.force") }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="editForm.force_openai_fast"
|
||||
:aria-label="t('admin.groups.openaiFast.force')"
|
||||
data-testid="edit-force-openai-fast"
|
||||
@click="editForm.force_openai_fast = !editForm.force_openai_fast"
|
||||
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||||
:class="
|
||||
editForm.force_openai_fast
|
||||
? 'bg-primary-500'
|
||||
: 'bg-gray-300 dark:bg-dark-600'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="
|
||||
editForm.force_openai_fast ? 'translate-x-6' : 'translate-x-1'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ t("admin.groups.openaiFast.hint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
|
||||
<div
|
||||
v-if="supportsLivePlatform(editForm.platform)"
|
||||
@@ -4476,6 +4554,10 @@ import {
|
||||
supportsMessagesDispatchPlatform,
|
||||
type MessagesDispatchMappingRow,
|
||||
} from "./groupsMessagesDispatch";
|
||||
import {
|
||||
normalizeGroupOpenAIFast,
|
||||
supportsGroupOpenAIFast,
|
||||
} from "./groupsOpenAIFast";
|
||||
import {
|
||||
buildModelsListConfig,
|
||||
createModelsListState as createInitialModelsListState,
|
||||
@@ -5050,6 +5132,7 @@ const createForm = reactive({
|
||||
weekly_limit_usd: null as number | null,
|
||||
monthly_limit_usd: null as number | null,
|
||||
long_context_pricing_enabled: true,
|
||||
force_openai_fast: false,
|
||||
model_pricing: [] as PricingFormEntry[],
|
||||
// 图片生成计费配置
|
||||
allow_image_generation: false,
|
||||
@@ -5411,6 +5494,7 @@ const editForm = reactive({
|
||||
weekly_limit_usd: null as number | null,
|
||||
monthly_limit_usd: null as number | null,
|
||||
long_context_pricing_enabled: true,
|
||||
force_openai_fast: false,
|
||||
model_pricing: [] as PricingFormEntry[],
|
||||
// 图片生成计费配置
|
||||
allow_image_generation: false,
|
||||
@@ -5885,6 +5969,7 @@ const closeCreateModal = () => {
|
||||
createForm.video_price_1080p = null;
|
||||
createForm.video_model_prices = createVideoModelPricesForm();
|
||||
createForm.long_context_pricing_enabled = true;
|
||||
createForm.force_openai_fast = false;
|
||||
createForm.model_pricing = [];
|
||||
createForm.web_search_price_per_call = null;
|
||||
createForm.search_price_per_1k = null;
|
||||
@@ -5985,6 +6070,10 @@ const handleCreateGroup = async () => {
|
||||
// 构建请求数据,包含模型路由配置
|
||||
const requestData = {
|
||||
...createGroupForm,
|
||||
force_openai_fast: normalizeGroupOpenAIFast(
|
||||
createForm.platform,
|
||||
createForm.force_openai_fast,
|
||||
),
|
||||
model_pricing: groupPricingToAPI(
|
||||
createForm.model_pricing,
|
||||
createForm.platform,
|
||||
@@ -6113,6 +6202,7 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
editForm.monthly_limit_usd = group.monthly_limit_usd;
|
||||
editForm.long_context_pricing_enabled =
|
||||
group.long_context_pricing_enabled ?? true;
|
||||
editForm.force_openai_fast = group.force_openai_fast ?? false;
|
||||
editForm.model_pricing = groupPricingFromAPI(group.model_pricing);
|
||||
editForm.allow_image_generation = group.allow_image_generation ?? false;
|
||||
editForm.allow_batch_image_generation =
|
||||
@@ -6219,6 +6309,7 @@ const closeEditModal = () => {
|
||||
editForm.video_price_1080p = null;
|
||||
editForm.video_model_prices = createVideoModelPricesForm();
|
||||
editForm.long_context_pricing_enabled = true;
|
||||
editForm.force_openai_fast = false;
|
||||
editForm.model_pricing = [];
|
||||
editForm.web_search_price_per_call = null;
|
||||
editForm.search_price_per_1k = null;
|
||||
@@ -6252,6 +6343,10 @@ const handleUpdateGroup = async () => {
|
||||
// 转换 fallback_group_id: null -> 0 (后端使用 0 表示清除)
|
||||
const payload = {
|
||||
...editForm,
|
||||
force_openai_fast: normalizeGroupOpenAIFast(
|
||||
editForm.platform,
|
||||
editForm.force_openai_fast,
|
||||
),
|
||||
model_pricing: groupPricingToAPI(
|
||||
editForm.model_pricing,
|
||||
editForm.platform,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
normalizeGroupOpenAIFast,
|
||||
supportsGroupOpenAIFast,
|
||||
} from "../groupsOpenAIFast";
|
||||
import en from "@/i18n/locales/en/admin/overview";
|
||||
import zh from "@/i18n/locales/zh/admin/overview";
|
||||
|
||||
describe("groupsOpenAIFast", () => {
|
||||
it("supports OpenAI and composite groups", () => {
|
||||
expect(supportsGroupOpenAIFast("openai")).toBe(true);
|
||||
expect(supportsGroupOpenAIFast("composite")).toBe(true);
|
||||
expect(supportsGroupOpenAIFast("anthropic")).toBe(false);
|
||||
});
|
||||
|
||||
it("clears stale enabled state on unsupported platforms", () => {
|
||||
expect(normalizeGroupOpenAIFast("openai", true)).toBe(true);
|
||||
expect(normalizeGroupOpenAIFast("composite", true)).toBe(true);
|
||||
expect(normalizeGroupOpenAIFast("anthropic", true)).toBe(false);
|
||||
expect(normalizeGroupOpenAIFast("openai", false)).toBe(false);
|
||||
});
|
||||
|
||||
it("provides localized switch labels", () => {
|
||||
expect(zh.groups.openaiFast).toMatchObject({
|
||||
title: expect.any(String),
|
||||
force: expect.any(String),
|
||||
hint: expect.stringContaining("service_tier=priority"),
|
||||
});
|
||||
expect(en.groups.openaiFast).toMatchObject({
|
||||
title: expect.any(String),
|
||||
force: expect.any(String),
|
||||
hint: expect.stringContaining("service_tier=priority"),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function supportsGroupOpenAIFast(platform: string): boolean {
|
||||
return platform === "openai" || platform === "composite";
|
||||
}
|
||||
|
||||
export function normalizeGroupOpenAIFast(
|
||||
platform: string,
|
||||
enabled: boolean,
|
||||
): boolean {
|
||||
return supportsGroupOpenAIFast(platform) && enabled;
|
||||
}
|
||||
Reference in New Issue
Block a user