@@ -201,6 +217,8 @@ import {
createReasoningEffortMappingRow,
normalizeReasoningEffortMatchType,
reasoningEffortOptionsForPlatform,
+ reasoningEffortOverLimitDeny,
+ reasoningEffortOverLimitDowngrade,
validateReasoningEffortMappings,
type ReasoningEffortMappingErrorCode,
type ReasoningEffortMappingRow,
@@ -210,11 +228,13 @@ const props = defineProps<{
idPrefix: string;
platform: GroupPlatform;
maxEffort: string;
+ overLimit: string;
mappings: ReasoningEffortMappingRow[];
}>();
const emit = defineEmits<{
(event: "update:maxEffort", value: string): void;
+ (event: "update:overLimit", value: string): void;
(event: "update:mappings", value: ReasoningEffortMappingRow[]): void;
}>();
@@ -237,6 +257,16 @@ const matchTypeOptions = computed(() => [
label: t("admin.groups.form.reasoningEffortMatchSuffix"),
},
]);
+const overLimitOptions = computed(() => [
+ {
+ value: reasoningEffortOverLimitDowngrade,
+ label: t("admin.groups.form.maxReasoningEffortOverLimitDowngrade"),
+ },
+ {
+ value: reasoningEffortOverLimitDeny,
+ label: t("admin.groups.form.maxReasoningEffortOverLimitDeny"),
+ },
+]);
const validationErrors = computed(() =>
validateReasoningEffortMappings(props.mappings, props.platform),
);
@@ -251,6 +281,13 @@ const updateMaxEffort = (value: string | number | boolean | null) => {
emit("update:maxEffort", asString(value));
};
+const updateOverLimit = (value: string | number | boolean | null) => {
+ emit(
+ "update:overLimit",
+ asString(value) || reasoningEffortOverLimitDowngrade,
+ );
+};
+
const updateGroup = (
id: string,
field: "match_type" | "model",
diff --git a/frontend/src/components/admin/group/__tests__/ReasoningEffortPolicyFields.spec.ts b/frontend/src/components/admin/group/__tests__/ReasoningEffortPolicyFields.spec.ts
index 83950fba3c..f407e1995a 100644
--- a/frontend/src/components/admin/group/__tests__/ReasoningEffortPolicyFields.spec.ts
+++ b/frontend/src/components/admin/group/__tests__/ReasoningEffortPolicyFields.spec.ts
@@ -25,6 +25,7 @@ describe("ReasoningEffortPolicyFields", () => {
idPrefix: "create-group-reasoning",
platform: "openai",
maxEffort: "",
+ overLimit: "downgrade",
mappings: [mapping],
},
global: {
@@ -60,6 +61,7 @@ describe("ReasoningEffortPolicyFields", () => {
idPrefix: "edit-group-reasoning",
platform: "openai",
maxEffort: "",
+ overLimit: "downgrade",
mappings: [],
},
global: {
diff --git a/frontend/src/components/channels/SupportedModelChip.vue b/frontend/src/components/channels/SupportedModelChip.vue
index 437e0bc5c9..b94fc9f072 100644
--- a/frontend/src/components/channels/SupportedModelChip.vue
+++ b/frontend/src/components/channels/SupportedModelChip.vue
@@ -79,11 +79,18 @@
:scale="perMillionScale"
/>
+
{
expect(claudeConfig).toBeDefined()
const parsed = JSON.parse(claudeConfig!)
+ const fable51 = parsed.provider['antigravity-claude'].models['claude-fable-5-1']
const fable = parsed.provider['antigravity-claude'].models['claude-fable-5']
+ expect(fable51.name).toBe('Claude Fable 5.1')
+ expect(fable51.limit).toEqual({ context: 1048576, output: 128000 })
+ expect(fable51.options.thinking).toEqual({ type: 'adaptive' })
+ expect(fable51.options.thinking).not.toHaveProperty('budgetTokens')
expect(fable.name).toBe('Claude Fable 5')
expect(fable.limit).toEqual({ context: 1048576, output: 128000 })
expect(fable.options.thinking).toEqual({ type: 'adaptive' })
diff --git a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
index d4aac37a07..05eb586951 100644
--- a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
+++ b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
@@ -138,9 +138,13 @@
class="whitespace-nowrap font-mono text-xs leading-5 text-gray-800 dark:text-gray-200"
:title="tierHint(m)"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
{{ paidPerMillion(iv.cache_write_price, period) }}
+ (1h {{ paidPerMillion(iv.cache_write_1h_price, period)
+ }})
{{ t('modelPlaza.table.cacheReadShort') }}
{{ paidPerMillion(iv.cache_read_price, period) }}
@@ -153,7 +157,11 @@
>
{{ t('modelPlaza.table.cacheWrite') }}
- {{ paidPerMillion(m.pricing?.cache_write_price, period) }}
+ {{ paidPerMillion(m.pricing?.cache_write_price, period)
+ }} (1h {{ paidPerMillion(m.pricing.cache_write_1h_price, period)
+ }})
{{ t('modelPlaza.table.cacheRead') }}
@@ -228,9 +236,13 @@
class="whitespace-nowrap font-mono text-xs leading-5 text-gray-500 dark:text-dark-400"
:title="t('modelPlaza.table.tierHint')"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
{{ official(iv.cache_write_price) }}
+ (1h {{ official(iv.cache_write_1h_price)
+ }})
{{ t('modelPlaza.table.cacheReadShort') }}
{{ official(iv.cache_read_price) }}
@@ -423,7 +435,7 @@ function perUnitSuffix(m: PlazaModel): string {
}
function hasCachePricing(m: PlazaModel): boolean {
- return m.pricing?.cache_write_price != null || m.pricing?.cache_read_price != null
+ return m.pricing?.cache_write_price != null || m.pricing?.cache_write_1h_price != null || m.pricing?.cache_read_price != null
}
function hasOfficialCache(o: NonNullable): boolean {
@@ -476,7 +488,7 @@ function officialIntervals(m: PlazaModel): UserPricingInterval[] {
/** 任一档带缓存价才按档渲染缓存列;否则沿用平价的写入/读取两行。 */
function hasTierCachePricing(intervals: UserPricingInterval[]): boolean {
- return intervals.some((iv) => iv.cache_write_price != null || iv.cache_read_price != null)
+ return intervals.some((iv) => iv.cache_write_price != null || iv.cache_write_1h_price != null || iv.cache_read_price != null)
}
/** 档位说明:整单按档计价,或(平台旧规则)仅超出部分按档计价。 */
diff --git a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
index b4acf6efe5..2971431a93 100644
--- a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
+++ b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
@@ -204,6 +204,16 @@ describe('PlazaModelPricingTable', () => {
expect(cells[6].text().trim()).toBe('-')
})
+ it('实付价分别展示自定义 5m 与 1h 缓存写入价', () => {
+ const model = tokenModel()
+ model.pricing!.cache_write_1h_price = 7e-6
+
+ const wrapper = mountTable([model], 1)
+ expect(wrapper.text()).toContain('$3.75')
+ expect(wrapper.text()).toContain('$7.00')
+ expect(wrapper.text()).toContain('(1h')
+ })
+
it('per_request 模型按单次价 × 倍率展示,官方价列显示 -', () => {
const model = tokenModel({
name: 'search-tool',
diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
index af6cc61820..a4198a7afb 100644
--- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
+++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts
@@ -37,6 +37,8 @@ describe('useModelWhitelist', () => {
})
it('Claude 模型列表包含新发布的 Claude 模型', () => {
+ expect(getModelsByPlatform('claude')).toContain('claude-fable-5-1')
+ expect(getModelsByPlatform('antigravity')).toContain('claude-fable-5-1')
expect(getModelsByPlatform('claude')).toContain('claude-fable-5')
expect(getModelsByPlatform('antigravity')).toContain('claude-fable-5')
expect(getModelsByPlatform('claude')).toContain('claude-opus-4-8')
diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts
index 548044394b..b17427a8a2 100644
--- a/frontend/src/composables/useModelWhitelist.ts
+++ b/frontend/src/composables/useModelWhitelist.ts
@@ -35,6 +35,7 @@ export const claudeModels = [
'claude-opus-5',
'claude-sonnet-4-6',
'claude-sonnet-5',
+ 'claude-fable-5-1',
'claude-fable-5'
]
@@ -56,6 +57,7 @@ const geminiModels = [
// 基于官方 API 返回的模型列表,只支持 Claude 4.5+ 和 Gemini 2.5+
const antigravityModels = [
// Claude 4.5+ 系列
+ 'claude-fable-5-1',
'claude-fable-5',
'claude-opus-4-6',
'claude-opus-4-6-thinking',
@@ -264,6 +266,7 @@ export const allModels = allModelsList.map(m => ({ value: m, label: m }))
// =====================
const anthropicPresetMappings = [
+ { label: 'Fable 5.1', from: 'claude-fable-5-1', to: 'claude-fable-5-1', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Fable 5', from: 'claude-fable-5', to: 'claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Sonnet 5', from: 'claude-sonnet-5', to: 'claude-sonnet-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'Sonnet 4', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-20250514', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
@@ -328,6 +331,7 @@ const grokPresetMappings = [
const antigravityPresetMappings = [
// Claude 通配符映射
{ label: 'Claude→Sonnet', from: 'claude-*', to: 'claude-sonnet-4-5', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
+ { label: 'Fable 5.1', from: 'claude-fable-5-1', to: 'claude-fable-5-1', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Fable 5', from: 'claude-fable-5', to: 'claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
@@ -364,6 +368,7 @@ const antigravityPresetMappings = [
// Bedrock 预设映射(与后端 DefaultBedrockModelMapping 保持一致)
const bedrockPresetMappings = [
+ { label: 'Fable 5.1', from: 'claude-fable-5-1', to: 'anthropic.claude-fable-5-1', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Fable 5', from: 'claude-fable-5', to: 'anthropic.claude-fable-5', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
{ label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'us.anthropic.claude-opus-4-6-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
{ label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'us.anthropic.claude-opus-4-7-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
diff --git a/frontend/src/i18n/locales/en/admin/channels.ts b/frontend/src/i18n/locales/en/admin/channels.ts
index 404bcc17d1..5055dc8fd6 100644
--- a/frontend/src/i18n/locales/en/admin/channels.ts
+++ b/frontend/src/i18n/locales/en/admin/channels.ts
@@ -30,6 +30,8 @@ export default {
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
+ cacheWrite5mPrice: 'Cache Write (5m)',
+ cacheWrite1hPrice: 'Cache Write (1h)',
cacheReadPrice: 'Cache Read',
imageOutputPrice: 'Image Output',
perRequestPrice: 'Per Request',
@@ -77,6 +79,7 @@ export default {
inputPrice: 'input price',
outputPrice: 'output price',
cacheWritePrice: 'cache write price',
+ cacheWrite1hPrice: '1h cache write price',
cacheReadPrice: 'cache read price',
perRequestPrice: 'per-request price'
}
@@ -122,8 +125,12 @@ export default {
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
+ cacheWrite5mPrice: 'Cache Write (5m)',
+ cacheWrite1hPrice: 'Cache Write (1h)',
cacheReadPrice: 'Cache Read',
cacheWritePriceShort: 'Cache W',
+ cacheWrite5mPriceShort: 'Cache W 5m',
+ cacheWrite1hPriceShort: 'Cache W 1h',
cacheReadPriceShort: 'Cache R',
imageInputPrice: 'Image Input',
imageTokenPrice: 'Image Output',
diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts
index 13221be5b0..a31cb65e10 100644
--- a/frontend/src/i18n/locales/en/admin/overview.ts
+++ b/frontend/src/i18n/locales/en/admin/overview.ts
@@ -859,7 +859,11 @@ export default {
rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).',
maxReasoningEffort: 'Max reasoning effort',
maxReasoningEffortUnlimited: 'Unlimited (follow request)',
- maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Higher values are capped; omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
+ maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
+ maxReasoningEffortOverLimit: 'Over-limit access control',
+ maxReasoningEffortOverLimitDowngrade: 'Automatically downgrade when over limit',
+ maxReasoningEffortOverLimitDeny: 'Deny access',
+ maxReasoningEffortOverLimitHint: 'Applies after a ceiling is set. Downgrade rewrites values above the ceiling to the ceiling. Deny rejects the request.',
reasoningEffortMappings: 'Reasoning effort mappings',
reasoningEffortMappingsHint: 'Type and model can both be left empty to match every model. One type and model can hold multiple request mappings, for example prefix gpt mapping both high and xhigh to medium. Exact matches beat affixes, and longer affixes beat shorter ones.',
addReasoningEffortMapping: 'Add mapping',
@@ -1163,6 +1167,13 @@ 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.',
+ 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',
hint: 'Triggered only when upstream explicitly returns prompt too long. Leave empty to disable fallback.',
diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts
index ef75763c7e..576067c6a3 100644
--- a/frontend/src/i18n/locales/en/dashboard.ts
+++ b/frontend/src/i18n/locales/en/dashboard.ts
@@ -585,6 +585,8 @@ export default {
inputPrice: 'Input',
outputPrice: 'Output',
cacheWritePrice: 'Cache Write',
+ cacheWrite5mPrice: 'Cache Write (5m)',
+ cacheWrite1hPrice: 'Cache Write (1h)',
cacheReadPrice: 'Cache Read',
imageInputPrice: 'Image Input',
imageOutputPrice: 'Image Output',
diff --git a/frontend/src/i18n/locales/zh/admin/channels.ts b/frontend/src/i18n/locales/zh/admin/channels.ts
index 12272becd1..b9e40a6a20 100644
--- a/frontend/src/i18n/locales/zh/admin/channels.ts
+++ b/frontend/src/i18n/locales/zh/admin/channels.ts
@@ -30,6 +30,8 @@ export default {
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
+ cacheWrite5mPrice: '缓存写入(5m)',
+ cacheWrite1hPrice: '缓存写入(1h)',
cacheReadPrice: '缓存读取',
imageOutputPrice: '图片输出',
perRequestPrice: '每次请求',
@@ -77,6 +79,7 @@ export default {
inputPrice: '输入价格',
outputPrice: '输出价格',
cacheWritePrice: '缓存写入价格',
+ cacheWrite1hPrice: '1h 缓存写入价格',
cacheReadPrice: '缓存读取价格',
perRequestPrice: '单次价格'
}
@@ -122,8 +125,12 @@ export default {
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
+ cacheWrite5mPrice: '缓存写入(5m)',
+ cacheWrite1hPrice: '缓存写入(1h)',
cacheReadPrice: '缓存读取',
cacheWritePriceShort: '缓存写',
+ cacheWrite5mPriceShort: '缓存写 5m',
+ cacheWrite1hPriceShort: '缓存写 1h',
cacheReadPriceShort: '缓存读',
imageInputPrice: '图片输入',
imageTokenPrice: '图片输出',
diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts
index 38e37e4b04..344bbc41f7 100644
--- a/frontend/src/i18n/locales/zh/admin/overview.ts
+++ b/frontend/src/i18n/locales/zh/admin/overview.ts
@@ -841,7 +841,11 @@ export default {
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
maxReasoningEffort: '推理强度上限',
maxReasoningEffortUnlimited: '不限制(跟随请求)',
- maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。超过上限时自动降档,不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
+ maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
+ maxReasoningEffortOverLimit: '超限访问控制',
+ maxReasoningEffortOverLimitDowngrade: '超过上限时自动降档',
+ maxReasoningEffortOverLimitDeny: '拒绝访问',
+ maxReasoningEffortOverLimitHint: '设置上限后生效。自动降档会将超过上限的请求改写为上限值后转发;拒绝访问则直接返回错误。',
reasoningEffortMappings: '推理强度映射',
reasoningEffortMappingsHint: '类型和模型均可留空,表示匹配全部模型。同一类型和模型下可添加多条请求值映射,例如前缀 gpt 同时将 high、xhigh 转到 medium。精确优先于前后缀,更长前后缀优先。',
addReasoningEffortMapping: '添加映射',
@@ -1161,6 +1165,13 @@ export default {
unsupportedMessage: '当前 Sub2API 服务端无法生成 Live 所需的设备证明,即使开启也不能使用。是否仍然开启?',
enableAnyway: '仍然开启'
},
+ openaiFast: {
+ title: 'OpenAI Fast 模式',
+ force: '强制使用 Fast(priority)',
+ hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。',
+ free: '免费 Fast',
+ freeHint: '该分组的 Fast 请求仍使用 priority 档位,但客户实际费用按同一请求的 Standard 价格计算。'
+ },
invalidRequestFallback: {
title: '无效请求兜底分组',
hint: '仅当上游明确返回 prompt too long 时才会触发,留空表示不兜底',
diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts
index 738a25f0f1..60117abbbc 100644
--- a/frontend/src/i18n/locales/zh/dashboard.ts
+++ b/frontend/src/i18n/locales/zh/dashboard.ts
@@ -590,6 +590,8 @@ export default {
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
+ cacheWrite5mPrice: '缓存写入(5m)',
+ cacheWrite1hPrice: '缓存写入(1h)',
cacheReadPrice: '缓存读取',
imageInputPrice: '图片输入',
imageOutputPrice: '图片输出',
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 93184d42f0..1c2fcc6f18 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -561,6 +561,7 @@ export interface Group {
rate_multiplier: number
rpm_limit?: number // Group-level RPM cap (0 = unlimited); overrides user-level rpm_limit when set
max_reasoning_effort?: string // OpenAI/Codex reasoning ceiling; empty means unlimited
+ max_reasoning_effort_over_limit?: string // downgrade (default) or deny when over the ceiling
reasoning_effort_mappings?: ReasoningEffortMapping[]
is_exclusive: boolean
status: 'active' | 'inactive'
@@ -615,6 +616,8 @@ 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。
@@ -779,6 +782,8 @@ export interface CreateGroupRequest {
weekly_limit_usd?: number | null
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
@@ -822,6 +827,7 @@ export interface CreateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
+ max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
@@ -841,6 +847,8 @@ export interface UpdateGroupRequest {
weekly_limit_usd?: number | null
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
@@ -884,6 +892,7 @@ export interface UpdateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
+ max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
diff --git a/frontend/src/views/admin/ChannelsView.vue b/frontend/src/views/admin/ChannelsView.vue
index fece592ad1..f040d2e64a 100644
--- a/frontend/src/views/admin/ChannelsView.vue
+++ b/frontend/src/views/admin/ChannelsView.vue
@@ -861,6 +861,7 @@ function addPricingEntry(sectionIdx: number) {
input_price: null,
output_price: null,
cache_write_price: null,
+ cache_write_1h_price: null,
cache_read_price: null,
fast_multiplier: null,
flex_multiplier: null,
@@ -897,6 +898,7 @@ async function syncLatestModels(sectionIdx: number) {
input_price: null,
output_price: null,
cache_write_price: null,
+ cache_write_1h_price: null,
cache_read_price: null,
fast_multiplier: null,
flex_multiplier: null,
@@ -965,6 +967,7 @@ function addRulePricingEntry(sectionIdx: number, ruleIndex: number) {
input_price: null,
output_price: null,
cache_write_price: null,
+ cache_write_1h_price: null,
cache_read_price: null,
image_input_price: null,
image_output_price: null,
@@ -1082,6 +1085,7 @@ function accountStatsRulesToAPI(): AccountStatsPricingRule[] {
input_price: mTokToPerToken(p.input_price),
output_price: mTokToPerToken(p.output_price),
cache_write_price: mTokToPerToken(p.cache_write_price),
+ cache_write_1h_price: mTokToPerToken(p.cache_write_1h_price),
cache_read_price: mTokToPerToken(p.cache_read_price),
image_input_price: mTokToPerToken(p.image_input_price),
image_output_price: mTokToPerToken(p.image_output_price),
@@ -1124,6 +1128,7 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[
input_price: mTokToPerToken(entry.input_price),
output_price: mTokToPerToken(entry.output_price),
cache_write_price: mTokToPerToken(entry.cache_write_price),
+ cache_write_1h_price: mTokToPerToken(entry.cache_write_1h_price),
cache_read_price: mTokToPerToken(entry.cache_read_price),
fast_multiplier: entry.fast_multiplier != null && entry.fast_multiplier !== '' ? Number(entry.fast_multiplier) : null,
flex_multiplier: entry.flex_multiplier != null && entry.flex_multiplier !== '' ? Number(entry.flex_multiplier) : null,
@@ -1225,6 +1230,7 @@ function apiToForm(channel: Channel): PlatformSection[] {
input_price: perTokenToMTok(p.input_price),
output_price: perTokenToMTok(p.output_price),
cache_write_price: perTokenToMTok(p.cache_write_price),
+ cache_write_1h_price: perTokenToMTok(p.cache_write_1h_price),
cache_read_price: perTokenToMTok(p.cache_read_price),
fast_multiplier: p.fast_multiplier,
flex_multiplier: p.flex_multiplier,
@@ -1417,6 +1423,7 @@ function distributeRulesToPlatforms(apiRules: AccountStatsPricingRule[]) {
input_price: perTokenToMTok(p.input_price),
output_price: perTokenToMTok(p.output_price),
cache_write_price: perTokenToMTok(p.cache_write_price),
+ cache_write_1h_price: perTokenToMTok(p.cache_write_1h_price),
cache_read_price: perTokenToMTok(p.cache_read_price),
image_input_price: perTokenToMTok(p.image_input_price),
image_output_price: perTokenToMTok(p.image_output_price),
diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue
index 612624904d..e60c8768ce 100644
--- a/frontend/src/views/admin/GroupsView.vue
+++ b/frontend/src/views/admin/GroupsView.vue
@@ -631,6 +631,7 @@
id-prefix="create-group-reasoning"
:platform="createForm.platform"
v-model:max-effort="createForm.max_reasoning_effort"
+ v-model:over-limit="createForm.max_reasoning_effort_over_limit"
v-model:mappings="createForm.reasoning_effort_mappings"
/>
+
+
+
+ {{ t("admin.groups.openaiFast.title") }}
+
+
+
+ {{ t("admin.groups.openaiFast.force") }}
+
+
+
+
+
+
+ {{ t("admin.groups.openaiFast.hint") }}
+
+
+
+ {{ t("admin.groups.openaiFast.free") }}
+
+
+
+
+
+
+ {{ t("admin.groups.openaiFast.freeHint") }}
+
+
+
@@ -3309,6 +3379,74 @@
+
+
+
+ {{ t("admin.groups.openaiFast.title") }}
+
+
+
+ {{ t("admin.groups.openaiFast.force") }}
+
+
+
+
+
+
+ {{ t("admin.groups.openaiFast.hint") }}
+
+
+
+ {{ t("admin.groups.openaiFast.free") }}
+
+
+
+
+
+
+ {{ t("admin.groups.openaiFast.freeHint") }}
+
+
+
({
input_price: null,
output_price: null,
cache_write_price: null,
+ cache_write_1h_price: null,
cache_read_price: null,
image_input_price: null,
image_output_price: null,
@@ -4546,6 +4691,7 @@ const groupPricingFromAPI = (
input_price: perTokenToMTok(entry.input_price),
output_price: perTokenToMTok(entry.output_price),
cache_write_price: perTokenToMTok(entry.cache_write_price),
+ cache_write_1h_price: perTokenToMTok(entry.cache_write_1h_price),
cache_read_price: perTokenToMTok(entry.cache_read_price),
image_input_price: perTokenToMTok(entry.image_input_price),
image_output_price: perTokenToMTok(entry.image_output_price),
@@ -4567,6 +4713,7 @@ const groupPricingToAPI = (
input_price: mTokToPerToken(entry.input_price),
output_price: mTokToPerToken(entry.output_price),
cache_write_price: mTokToPerToken(entry.cache_write_price),
+ cache_write_1h_price: mTokToPerToken(entry.cache_write_1h_price),
cache_read_price: mTokToPerToken(entry.cache_read_price),
image_input_price: mTokToPerToken(entry.image_input_price),
image_output_price: mTokToPerToken(entry.image_output_price),
@@ -5047,6 +5194,8 @@ 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,
+ free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5105,6 +5254,7 @@ const createForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
+ max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5408,6 +5558,8 @@ 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,
+ free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5467,6 +5619,7 @@ const editForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
+ max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5882,6 +6035,8 @@ const closeCreateModal = () => {
createForm.video_price_1080p = null;
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;
@@ -5907,6 +6062,7 @@ const closeCreateModal = () => {
createForm.copy_accounts_from_group_ids = [];
createForm.rpm_limit = 0;
createForm.max_reasoning_effort = "";
+ createForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = [];
createReasoningEffortPolicyRef.value?.resetValidation();
resetModelsListState(createModelsListState);
@@ -5982,6 +6138,14 @@ const handleCreateGroup = async () => {
// 构建请求数据,包含模型路由配置
const requestData = {
...createGroupForm,
+ force_openai_fast: normalizeGroupOpenAIFast(
+ createForm.platform,
+ createForm.force_openai_fast,
+ ),
+ free_openai_fast: normalizeGroupOpenAIFast(
+ createForm.platform,
+ createForm.free_openai_fast,
+ ),
model_pricing: groupPricingToAPI(
createForm.model_pricing,
createForm.platform,
@@ -6110,6 +6274,8 @@ 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.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 =
@@ -6177,6 +6343,9 @@ const handleEdit = async (group: AdminGroup) => {
group.platform,
group.max_reasoning_effort,
);
+ editForm.max_reasoning_effort_over_limit = normalizeReasoningEffortOverLimit(
+ group.max_reasoning_effort_over_limit,
+ );
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
group.reasoning_effort_mappings,
group.platform,
@@ -6198,6 +6367,7 @@ const closeEditModal = () => {
showEditModal.value = false;
editingGroup.value = null;
editForm.max_reasoning_effort = "";
+ editForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = [];
editReasoningEffortPolicyRef.value?.resetValidation();
editModelRoutingRules.value = [];
@@ -6216,6 +6386,8 @@ const closeEditModal = () => {
editForm.video_price_1080p = null;
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;
@@ -6249,6 +6421,14 @@ const handleUpdateGroup = async () => {
// 转换 fallback_group_id: null -> 0 (后端使用 0 表示清除)
const payload = {
...editForm,
+ force_openai_fast: normalizeGroupOpenAIFast(
+ editForm.platform,
+ editForm.force_openai_fast,
+ ),
+ free_openai_fast: normalizeGroupOpenAIFast(
+ editForm.platform,
+ editForm.free_openai_fast,
+ ),
model_pricing: groupPricingToAPI(
editForm.model_pricing,
editForm.platform,
@@ -6673,6 +6853,13 @@ watch(
newVal,
createForm.max_reasoning_effort,
);
+ createForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
+ newVal,
+ )
+ ? normalizeReasoningEffortOverLimit(
+ createForm.max_reasoning_effort_over_limit,
+ )
+ : reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(createForm.reasoning_effort_mappings),
newVal,
@@ -6723,6 +6910,13 @@ watch(
newVal,
editForm.max_reasoning_effort,
);
+ editForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
+ newVal,
+ )
+ ? normalizeReasoningEffortOverLimit(
+ editForm.max_reasoning_effort_over_limit,
+ )
+ : reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(editForm.reasoning_effort_mappings),
newVal,
diff --git a/frontend/src/views/admin/__tests__/groupsOpenAIFast.spec.ts b/frontend/src/views/admin/__tests__/groupsOpenAIFast.spec.ts
new file mode 100644
index 0000000000..a0333e0a5a
--- /dev/null
+++ b/frontend/src/views/admin/__tests__/groupsOpenAIFast.spec.ts
@@ -0,0 +1,40 @@
+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"),
+ 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"),
+ });
+ });
+});
diff --git a/frontend/src/views/admin/__tests__/groupsReasoningEffort.spec.ts b/frontend/src/views/admin/__tests__/groupsReasoningEffort.spec.ts
index 5bc361844d..663d7cefd3 100644
--- a/frontend/src/views/admin/__tests__/groupsReasoningEffort.spec.ts
+++ b/frontend/src/views/admin/__tests__/groupsReasoningEffort.spec.ts
@@ -5,9 +5,12 @@ import {
createReasoningEffortMappingRow,
normalizeReasoningEffortForPlatform,
normalizeReasoningEffortMatchType,
+ normalizeReasoningEffortOverLimit,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOptionsForPlatform,
+ reasoningEffortOverLimitDeny,
+ reasoningEffortOverLimitDowngrade,
supportsReasoningEffortPolicyPlatform,
validateReasoningEffortMappings,
} from "../groupsReasoningEffort";
@@ -248,4 +251,22 @@ describe("groupsReasoningEffort", () => {
[row.pairs[0].id]: { from: "unsupportedFrom" },
});
});
+
+ it("normalizes over-limit access control to downgrade or deny", () => {
+ expect(normalizeReasoningEffortOverLimit(undefined)).toBe(
+ reasoningEffortOverLimitDowngrade,
+ );
+ expect(normalizeReasoningEffortOverLimit("")).toBe(
+ reasoningEffortOverLimitDowngrade,
+ );
+ expect(normalizeReasoningEffortOverLimit(" downgrade ")).toBe(
+ reasoningEffortOverLimitDowngrade,
+ );
+ expect(normalizeReasoningEffortOverLimit("DENY")).toBe(
+ reasoningEffortOverLimitDeny,
+ );
+ expect(normalizeReasoningEffortOverLimit("block")).toBe(
+ reasoningEffortOverLimitDowngrade,
+ );
+ });
});
diff --git a/frontend/src/views/admin/groupsOpenAIFast.ts b/frontend/src/views/admin/groupsOpenAIFast.ts
new file mode 100644
index 0000000000..a4e3f544a2
--- /dev/null
+++ b/frontend/src/views/admin/groupsOpenAIFast.ts
@@ -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;
+}
diff --git a/frontend/src/views/admin/groupsReasoningEffort.ts b/frontend/src/views/admin/groupsReasoningEffort.ts
index 35a4b273ef..de2fbbee4b 100644
--- a/frontend/src/views/admin/groupsReasoningEffort.ts
+++ b/frontend/src/views/admin/groupsReasoningEffort.ts
@@ -19,6 +19,9 @@ const reasoningEffortMatchTypes: readonly ReasoningEffortMatchType[] = [
"suffix",
];
+export const reasoningEffortOverLimitDowngrade = "downgrade";
+export const reasoningEffortOverLimitDeny = "deny";
+
const reasoningEffortValuesForPlatform = (
platform: GroupPlatform,
): readonly string[] =>
@@ -60,6 +63,14 @@ export function normalizeReasoningEffortMatchType(
: "";
}
+export function normalizeReasoningEffortOverLimit(
+ value: string | null | undefined,
+): string {
+ return value?.trim().toLowerCase() === reasoningEffortOverLimitDeny
+ ? reasoningEffortOverLimitDeny
+ : reasoningEffortOverLimitDowngrade;
+}
+
export interface ReasoningEffortMappingPair {
id: string;
from: string;