Merge pull request #6444 from Ritel-T/pr/group-free-openai-fast

Add group-level Standard billing for OpenAI Fast requests
This commit is contained in:
Wesley Liddick
2026-09-02 09:23:49 +08:00
committed by GitHub
38 changed files with 481 additions and 25 deletions
@@ -1158,7 +1158,9 @@ export default {
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.'
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.',
free: 'Free Fast',
freeHint: 'Fast requests in this group still use the priority tier, but customers are charged the equivalent Standard price.'
},
invalidRequestFallback: {
title: 'Invalid Request Fallback Group',
@@ -1156,7 +1156,9 @@ export default {
openaiFast: {
title: 'OpenAI Fast 模式',
force: '强制使用 Fast(priority)',
hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。'
hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。',
free: '免费 Fast',
freeHint: '该分组的 Fast 请求仍使用 priority 档位,但客户实际费用按同一请求的 Standard 价格计算。'
},
invalidRequestFallback: {
title: '无效请求兜底分组',
+3
View File
@@ -613,6 +613,7 @@ export interface Group {
export interface AdminGroup extends Group {
force_openai_fast: boolean
free_openai_fast: boolean
model_pricing: import('@/api/admin/channels').ChannelModelPricing[]
// 分组利润控制(openai/anthropic/gemini/grok/antigravity 分组可启用;margin/buffer 为小数存储)。
// 仅管理员可见:与 rate_multiplier 相乘即可反推上游成本上限,不得下放到 Group。
@@ -778,6 +779,7 @@ export interface CreateGroupRequest {
monthly_limit_usd?: number | null
long_context_pricing_enabled?: boolean
force_openai_fast?: boolean
free_openai_fast?: boolean
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
allow_image_generation?: boolean
allow_batch_image_generation?: boolean
@@ -842,6 +844,7 @@ export interface UpdateGroupRequest {
monthly_limit_usd?: number | null
long_context_pricing_enabled?: boolean
force_openai_fast?: boolean
free_openai_fast?: boolean
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
allow_image_generation?: boolean
allow_batch_image_generation?: boolean
+71
View File
@@ -1619,6 +1619,35 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.hint") }}
</p>
<div class="flex items-center justify-between gap-4 mt-4">
<label class="text-sm text-gray-600 dark:text-gray-400">
{{ t("admin.groups.openaiFast.free") }}
</label>
<button
type="button"
role="switch"
:aria-checked="createForm.free_openai_fast"
:aria-label="t('admin.groups.openaiFast.free')"
data-testid="create-free-openai-fast"
@click="createForm.free_openai_fast = !createForm.free_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.free_openai_fast
? 'bg-emerald-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.free_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.freeHint") }}
</p>
</div>
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
@@ -3387,6 +3416,35 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.hint") }}
</p>
<div class="flex items-center justify-between gap-4 mt-4">
<label class="text-sm text-gray-600 dark:text-gray-400">
{{ t("admin.groups.openaiFast.free") }}
</label>
<button
type="button"
role="switch"
:aria-checked="editForm.free_openai_fast"
:aria-label="t('admin.groups.openaiFast.free')"
data-testid="edit-free-openai-fast"
@click="editForm.free_openai_fast = !editForm.free_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.free_openai_fast
? 'bg-emerald-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.free_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.freeHint") }}
</p>
</div>
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
@@ -5137,6 +5195,7 @@ const createForm = reactive({
monthly_limit_usd: null as number | null,
long_context_pricing_enabled: true,
force_openai_fast: false,
free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5500,6 +5559,7 @@ const editForm = reactive({
monthly_limit_usd: null as number | null,
long_context_pricing_enabled: true,
force_openai_fast: false,
free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5976,6 +6036,7 @@ const closeCreateModal = () => {
createForm.video_model_prices = createVideoModelPricesForm();
createForm.long_context_pricing_enabled = true;
createForm.force_openai_fast = false;
createForm.free_openai_fast = false;
createForm.model_pricing = [];
createForm.web_search_price_per_call = null;
createForm.search_price_per_1k = null;
@@ -6081,6 +6142,10 @@ const handleCreateGroup = async () => {
createForm.platform,
createForm.force_openai_fast,
),
free_openai_fast: normalizeGroupOpenAIFast(
createForm.platform,
createForm.free_openai_fast,
),
model_pricing: groupPricingToAPI(
createForm.model_pricing,
createForm.platform,
@@ -6210,6 +6275,7 @@ const handleEdit = async (group: AdminGroup) => {
editForm.long_context_pricing_enabled =
group.long_context_pricing_enabled ?? true;
editForm.force_openai_fast = group.force_openai_fast ?? false;
editForm.free_openai_fast = group.free_openai_fast ?? false;
editForm.model_pricing = groupPricingFromAPI(group.model_pricing);
editForm.allow_image_generation = group.allow_image_generation ?? false;
editForm.allow_batch_image_generation =
@@ -6321,6 +6387,7 @@ const closeEditModal = () => {
editForm.video_model_prices = createVideoModelPricesForm();
editForm.long_context_pricing_enabled = true;
editForm.force_openai_fast = false;
editForm.free_openai_fast = false;
editForm.model_pricing = [];
editForm.web_search_price_per_call = null;
editForm.search_price_per_1k = null;
@@ -6358,6 +6425,10 @@ const handleUpdateGroup = async () => {
editForm.platform,
editForm.force_openai_fast,
),
free_openai_fast: normalizeGroupOpenAIFast(
editForm.platform,
editForm.free_openai_fast,
),
model_pricing: groupPricingToAPI(
editForm.model_pricing,
editForm.platform,
@@ -26,11 +26,15 @@ describe("groupsOpenAIFast", () => {
title: expect.any(String),
force: expect.any(String),
hint: expect.stringContaining("service_tier=priority"),
free: expect.any(String),
freeHint: expect.stringContaining("Standard"),
});
expect(en.groups.openaiFast).toMatchObject({
title: expect.any(String),
force: expect.any(String),
hint: expect.stringContaining("service_tier=priority"),
free: expect.any(String),
freeHint: expect.stringContaining("Standard"),
});
});
});