merge: resolve conflicts with upstream/main

Keep model-scoped reasoning effort mappings together with the
deny-or-downgrade ceiling control that landed on main.
This commit is contained in:
刘佳林
2026-09-02 10:01:42 +08:00
179 changed files with 6415 additions and 1152 deletions
@@ -224,6 +224,7 @@ const activeModelStatuses = computed<AccountModelStatusItem[]>(() => {
const formatScopeName = (scope: string): string => {
const aliases: Record<string, string> = {
// Claude 系列
'claude-fable-5-1': 'CFable51',
'claude-fable-5': 'CFable5',
'claude-opus-4-6': 'COpus46',
'claude-opus-4-6-thinking': 'COpus46T',
@@ -859,6 +859,7 @@ const antigravity3ImageUsageFromAPI = computed(() =>
// Claude from API (all Claude model variants)
const antigravityClaudeUsageFromAPI = computed(() =>
getAntigravityUsageFromAPI([
'claude-fable-5-1',
'claude-fable-5',
'claude-sonnet-4-5', 'claude-opus-4-5-thinking',
'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-opus-4-6-thinking',
@@ -1319,7 +1319,7 @@
/>
</div>
</div>
<p v-if="form.platform !== 'deepseek'" class="input-hint">
<p v-if="!cnSupportsNativeResponses(form.platform)" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
@@ -3814,6 +3814,7 @@ import {
applyAntigravityProjectID,
applyHeaderOverride,
applyInterceptWarmup,
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
isHeaderOverrideCapable,
@@ -4006,7 +4007,7 @@ const upstreamBillingAutoProbeEnabled = ref(true)
// ── 国产供应商(Kimi / Zhipu / DeepSeek)账号类型、API 协议与端点 ──
const accountMode = ref<CnAccountMode>('payg')
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
// responses=deepseek / kimi 原生 Responses 端点(Codex)。与账号类型正交。
const apiProtocol = ref<CnApiProtocol>('adaptive')
// 智谱团队版 Coding Plan:组织/项目 ID,写入 credentials 供额度探测切换团队端点
const zhipuOrganization = ref('')
@@ -4027,14 +4028,14 @@ const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
}
return 'kimi'
})
// 当前平台可选的协议档(responses 仅 deepseek)。
// 当前平台可选的协议档(responses 仅 deepseek / kimi)。
const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: string }>>(() => {
const opts: Array<{ value: CnApiProtocol; labelKey: string }> = [
{ value: 'adaptive', labelKey: 'adaptive' },
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (form.platform === 'deepseek') {
if (cnSupportsNativeResponses(form.platform)) {
opts.push({ value: 'responses', labelKey: 'responses' })
}
return opts
@@ -4044,7 +4045,7 @@ const cnAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol; l
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (form.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
if (cnSupportsNativeResponses(form.platform)) opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
@@ -72,7 +72,7 @@
<input v-model="editAdaptiveBaseUrls[item.value]" type="text" class="input" />
</div>
</div>
<p v-if="account.platform !== 'deepseek'" class="input-hint">
<p v-if="!cnSupportsNativeResponses(account.platform)" class="input-hint">
{{ t('admin.accounts.cnProviders.apiProtocol.responsesFallbackDesc') }}
</p>
</div>
@@ -2909,6 +2909,7 @@ import {
isHeaderOverrideCapable,
splitHeaderOverridesObject,
validateHeaderOverrideRows,
cnSupportsNativeResponses,
defaultCNAdaptiveBaseUrls,
defaultCNBaseUrl,
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
@@ -3057,7 +3058,7 @@ const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: strin
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (props.account?.platform === 'deepseek') {
if (cnSupportsNativeResponses(props.account?.platform ?? '')) {
opts.push({ value: 'responses', labelKey: 'responses' })
}
return opts
@@ -3067,7 +3068,7 @@ const editAdaptiveProtocolOptions = computed<Array<{ value: CnNativeApiProtocol;
{ value: 'chat_completions', labelKey: 'chatCompletions' },
{ value: 'anthropic', labelKey: 'anthropic' }
]
if (props.account?.platform === 'deepseek') opts.push({ value: 'responses', labelKey: 'responses' })
if (cnSupportsNativeResponses(props.account?.platform ?? '')) opts.push({ value: 'responses', labelKey: 'responses' })
return opts
})
watch(editApiProtocol, (protocol, previousProtocol) => {
@@ -3947,7 +3948,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
storedProtocol === 'responses'
? storedProtocol
: 'chat_completions'
if (newAccount.platform !== 'deepseek' && editApiProtocol.value === 'responses') {
if (!cnSupportsNativeResponses(newAccount.platform) && editApiProtocol.value === 'responses') {
editApiProtocol.value = 'chat_completions'
}
const adaptiveDefaults = defaultCNAdaptiveBaseUrls(newAccount.platform, editAccountMode.value)
@@ -375,7 +375,31 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
base_url: 'https://api.moonshot.cn/v1',
api_base_urls: {
chat_completions: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic'
anthropic: 'https://api.moonshot.cn/anthropic',
responses: 'https://api.moonshot.cn/v1'
}
})
})
it('submits adaptive Kimi Coding Plan Responses endpoint', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Kimi')
await selectButtonByText(wrapper, 'admin.accounts.cnProviders.accountMode.coding')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Kimi coding')
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-kimi-coding')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
await flushPromises()
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.credentials).toMatchObject({
account_mode: 'coding',
api_protocol: 'adaptive',
base_url: 'https://api.kimi.com/coding/v1',
api_base_urls: {
chat_completions: 'https://api.kimi.com/coding/v1',
anthropic: 'https://api.kimi.com/coding',
responses: 'https://api.kimi.com/coding/v1'
}
})
})
@@ -355,6 +355,39 @@ describe('EditAccountModal', () => {
})
})
it('preserves adaptive Kimi Responses endpoint on submit', async () => {
const account = buildAccount()
account.platform = 'kimi'
account.credentials = {
api_key: 'sk-kimi',
account_mode: 'payg',
api_protocol: 'adaptive',
base_url: 'https://api.moonshot.cn/v1',
api_base_urls: {
chat_completions: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic',
responses: 'https://api.moonshot.cn/v1'
}
}
updateAccountMock.mockReset().mockResolvedValue(account)
checkMixedChannelRiskMock.mockReset().mockResolvedValue({ has_risk: false })
const wrapper = mountModal(account)
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials).toMatchObject({
account_mode: 'payg',
api_protocol: 'adaptive',
base_url: 'https://api.moonshot.cn/v1',
api_base_urls: {
chat_completions: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic',
responses: 'https://api.moonshot.cn/v1'
}
})
})
it('preserves adaptive GLM endpoints on submit', async () => {
const account = buildAccount()
account.platform = 'zhipu'
@@ -1,18 +1,27 @@
import { describe, expect, it } from 'vitest'
import { defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
import { cnSupportsNativeResponses, defaultCNAdaptiveBaseUrls } from '../credentialsBuilder'
describe('cnSupportsNativeResponses', () => {
it('is true for DeepSeek and Kimi only', () => {
expect(cnSupportsNativeResponses('deepseek')).toBe(true)
expect(cnSupportsNativeResponses('kimi')).toBe(true)
expect(cnSupportsNativeResponses('zhipu')).toBe(false)
expect(cnSupportsNativeResponses('openai')).toBe(false)
})
})
describe('defaultCNAdaptiveBaseUrls', () => {
it('resolves Kimi endpoints by account mode', () => {
expect(defaultCNAdaptiveBaseUrls('kimi', 'payg')).toEqual({
chat_completions: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic',
responses: ''
responses: 'https://api.moonshot.cn/v1'
})
expect(defaultCNAdaptiveBaseUrls('kimi', 'coding')).toEqual({
chat_completions: 'https://api.kimi.com/coding/v1',
anthropic: 'https://api.kimi.com/coding',
responses: ''
responses: 'https://api.kimi.com/coding/v1'
})
})
@@ -256,10 +256,15 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
export type CnAccountMode = 'payg' | 'coding'
/** 仅 deepseek 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
/** deepseek / kimi 支持原生 responses;adaptive 会按入站协议选择原生端点。 */
export type CnApiProtocol = 'adaptive' | 'chat_completions' | 'anthropic' | 'responses'
export type CnNativeApiProtocol = Exclude<CnApiProtocol, 'adaptive'>
/** DeepSeek 与 Kimi(按量付费 / Coding Plan)提供原生 Responses 端点。 */
export function cnSupportsNativeResponses(platform: string): boolean {
return platform === 'deepseek' || platform === 'kimi'
}
export interface CnBaseUrlPreset {
mode: CnAccountMode
protocol: CnApiProtocol
@@ -273,8 +278,10 @@ export const CN_BASE_URL_PRESETS: Record<'kimi' | 'zhipu' | 'deepseek', CnBaseUr
kimi: [
{ mode: 'payg', protocol: 'chat_completions', label: 'Moonshot', url: 'https://api.moonshot.cn/v1' },
{ mode: 'payg', protocol: 'anthropic', label: 'Moonshot Anthropic', url: 'https://api.moonshot.cn/anthropic' },
{ mode: 'payg', protocol: 'responses', label: 'Moonshot Responses', url: 'https://api.moonshot.cn/v1' },
{ mode: 'coding', protocol: 'chat_completions', label: 'Kimi For Coding', url: 'https://api.kimi.com/coding/v1' },
{ mode: 'coding', protocol: 'anthropic', label: 'Kimi Coding Anthropic', url: 'https://api.kimi.com/coding' }
{ mode: 'coding', protocol: 'anthropic', label: 'Kimi Coding Anthropic', url: 'https://api.kimi.com/coding' },
{ mode: 'coding', protocol: 'responses', label: 'Kimi Coding Responses', url: 'https://api.kimi.com/coding/v1' }
],
zhipu: [
{ mode: 'payg', protocol: 'chat_completions', label: 'GLM PaaS', url: 'https://open.bigmodel.cn/api/paas/v4' },
@@ -307,7 +314,7 @@ export function defaultCNBaseUrl(
return ''
}
}
// responses 仅 deepseek:base 与 chat_completions 相同(端点路径差异由后端处理)。
// responses:Kimi / DeepSeek 的 base 与 chat_completions 相同(端点路径差异由后端处理)。
switch (platform) {
case 'kimi':
return mode === 'coding' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1'
@@ -330,7 +337,7 @@ export function defaultCNAdaptiveBaseUrls(
return {
chat_completions: defaultCNBaseUrl(platform, mode, 'chat_completions'),
anthropic: defaultCNBaseUrl(platform, mode, 'anthropic'),
responses: platform === 'deepseek' ? defaultCNBaseUrl(platform, mode, 'responses') : ''
responses: cnSupportsNativeResponses(platform) ? defaultCNBaseUrl(platform, mode, 'responses') : ''
}
}
@@ -3,7 +3,7 @@
:class="isEmpty ? 'border-red-400 bg-red-50 dark:border-red-500 dark:bg-red-950/20' : 'border-gray-200 bg-white dark:border-dark-500 dark:bg-dark-700'">
<!-- Token mode: context range + prices ($/MTok) -->
<template v-if="mode === 'token'">
<div class="grid min-w-0 flex-1 grid-cols-2 gap-2 sm:grid-cols-4 xl:grid-cols-6">
<div class="grid min-w-0 flex-1 grid-cols-2 gap-2 sm:grid-cols-4 xl:grid-cols-7">
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.minTokens') }}</label>
<input :value="interval.min_tokens" @input="emitField('min_tokens', toInt(($event.target as HTMLInputElement).value))"
@@ -25,10 +25,15 @@
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWritePriceShort') }} <span class="text-gray-300">$/M</span></label>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWrite5mPriceShort') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.cache_write_price" @input="emitField('cache_write_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWrite1hPriceShort') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.cache_write_1h_price" @input="emitField('cache_write_1h_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheReadPriceShort') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.cache_read_price" @input="emitField('cache_read_price', ($event.target as HTMLInputElement).value)"
@@ -117,6 +122,7 @@ const isEmpty = computed(() => {
return (iv.input_price == null || iv.input_price === '') &&
(iv.output_price == null || iv.output_price === '') &&
(iv.cache_write_price == null || iv.cache_write_price === '') &&
(iv.cache_write_1h_price == null || iv.cache_write_1h_price === '') &&
(iv.cache_read_price == null || iv.cache_read_price === '') &&
(iv.input_multiplier == null || iv.input_multiplier === '') &&
(iv.output_multiplier == null || iv.output_multiplier === '') &&
@@ -106,7 +106,7 @@
{{ t('admin.channels.form.defaultPrices') }}
<span class="ml-1 font-normal text-gray-400">$/MTok</span>
</label>
<div class="mt-1 grid grid-cols-2 gap-2 sm:grid-cols-6">
<div class="mt-1 grid grid-cols-2 gap-2 sm:grid-cols-4 xl:grid-cols-7">
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.inputPrice') }}</label>
<input :value="entry.input_price" @input="emitField('input_price', ($event.target as HTMLInputElement).value)"
@@ -118,10 +118,15 @@
type="number" step="any" min="0" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWritePrice') }}</label>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWrite5mPrice') }}</label>
<input :value="entry.cache_write_price" @input="emitField('cache_write_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWrite1hPrice') }}</label>
<input :value="entry.cache_write_1h_price" @input="emitField('cache_write_1h_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.pricePlaceholder')" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheReadPrice') }}</label>
<input :value="entry.cache_read_price" @input="emitField('cache_read_price', ($event.target as HTMLInputElement).value)"
@@ -312,6 +317,7 @@ function addInterval() {
intervals.push({
min_tokens: 0, max_tokens: null, tier_label: '',
input_price: null, output_price: null, cache_write_price: null,
cache_write_1h_price: null,
cache_read_price: null, per_request_price: null,
input_multiplier: null, output_multiplier: null,
cache_write_multiplier: null, cache_read_multiplier: null,
@@ -328,6 +334,7 @@ function addMediaTier() {
intervals.push({
min_tokens: 0, max_tokens: null, tier_label: labels[intervals.length] || '',
input_price: null, output_price: null, cache_write_price: null,
cache_write_1h_price: null,
cache_read_price: null, per_request_price: null,
input_multiplier: null, output_multiplier: null,
cache_write_multiplier: null, cache_read_multiplier: null,
@@ -359,7 +366,7 @@ async function onModelsUpdate(newModels: string[]) {
// 检查是否所有价格字段都为空
const e = props.entry
const hasPrice = e.input_price != null || e.output_price != null ||
e.cache_write_price != null || e.cache_read_price != null
e.cache_write_price != null || e.cache_write_1h_price != null || e.cache_read_price != null
if (hasPrice) return
// 查询第一个新增模型的默认价格
@@ -372,6 +379,7 @@ async function onModelsUpdate(newModels: string[]) {
input_price: perTokenToMTok(result.input_price ?? null),
output_price: perTokenToMTok(result.output_price ?? null),
cache_write_price: perTokenToMTok(result.cache_write_price ?? null),
cache_write_1h_price: perTokenToMTok(result.cache_write_1h_price ?? null),
cache_read_price: perTokenToMTok(result.cache_read_price ?? null),
image_input_price: perTokenToMTok(result.image_input_price ?? null),
image_output_price: perTokenToMTok(result.image_output_price ?? null),
@@ -9,6 +9,7 @@ export interface IntervalFormEntry {
input_price: number | string | null
output_price: number | string | null
cache_write_price: number | string | null
cache_write_1h_price?: number | string | null
cache_read_price: number | string | null
input_multiplier: number | string | null
output_multiplier: number | string | null
@@ -24,6 +25,7 @@ export interface PricingFormEntry {
input_price: number | string | null
output_price: number | string | null
cache_write_price: number | string | null
cache_write_1h_price?: number | string | null
cache_read_price: number | string | null
fast_multiplier?: number | string | null
flex_multiplier?: number | string | null
@@ -200,6 +202,7 @@ export function apiIntervalsToForm(intervals: PricingInterval[]): IntervalFormEn
input_price: perTokenToMTok(iv.input_price),
output_price: perTokenToMTok(iv.output_price),
cache_write_price: perTokenToMTok(iv.cache_write_price),
cache_write_1h_price: perTokenToMTok(iv.cache_write_1h_price),
cache_read_price: perTokenToMTok(iv.cache_read_price),
input_multiplier: iv.input_multiplier,
output_multiplier: iv.output_multiplier,
@@ -218,6 +221,7 @@ export function formIntervalsToAPI(intervals: IntervalFormEntry[]): PricingInter
input_price: mTokToPerToken(iv.input_price),
output_price: mTokToPerToken(iv.output_price),
cache_write_price: mTokToPerToken(iv.cache_write_price),
cache_write_1h_price: mTokToPerToken(iv.cache_write_1h_price),
cache_read_price: mTokToPerToken(iv.cache_read_price),
input_multiplier: toNullableNumber(iv.input_multiplier),
output_multiplier: toNullableNumber(iv.output_multiplier),
@@ -342,6 +346,7 @@ function validateIntervalPrices(iv: IntervalFormEntry, idx: number, t: Translate
['inputPrice', iv.input_price],
['outputPrice', iv.output_price],
['cacheWritePrice', iv.cache_write_price],
['cacheWrite1hPrice', iv.cache_write_1h_price ?? null],
['cacheReadPrice', iv.cache_read_price],
['perRequestPrice', iv.per_request_price],
]
@@ -17,6 +17,22 @@
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortHint") }}</p>
</div>
<div>
<label :for="`${idPrefix}-over-limit`" class="input-label">
{{ t("admin.groups.form.maxReasoningEffortOverLimit") }}
</label>
<Select
:id="`${idPrefix}-over-limit`"
:model-value="overLimit"
:options="overLimitOptions"
:aria-label="t('admin.groups.form.maxReasoningEffortOverLimit')"
:searchable="false"
:disabled="!maxEffort"
@update:model-value="updateOverLimit"
/>
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortOverLimitHint") }}</p>
</div>
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between gap-3">
<div>
@@ -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",
@@ -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: {
@@ -79,11 +79,18 @@
:scale="perMillionScale"
/>
<PricingRow
:label="t(prefixKey('cacheWritePrice'))"
:label="t(prefixKey('cacheWrite5mPrice'))"
:value="model.pricing.cache_write_price"
:unit="t(prefixKey('unitPerMillion'))"
:scale="perMillionScale"
/>
<PricingRow
v-if="model.pricing.cache_write_1h_price != null"
:label="t(prefixKey('cacheWrite1hPrice'))"
:value="model.pricing.cache_write_1h_price"
:unit="t(prefixKey('unitPerMillion'))"
:scale="perMillionScale"
/>
<PricingRow
:label="t(prefixKey('cacheReadPrice'))"
:value="model.pricing.cache_read_price"
@@ -1708,6 +1708,22 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
}
}
const claudeModels = {
'claude-fable-5-1': {
name: 'Claude Fable 5.1',
limit: {
context: 1048576,
output: 128000
},
modalities: {
input: ['text', 'image', 'pdf'],
output: ['text']
},
options: {
thinking: {
type: 'adaptive'
}
}
},
'claude-fable-5': {
name: 'Claude Fable 5',
limit: {
@@ -671,8 +671,13 @@ describe('UseKeyModal', () => {
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' })
@@ -138,9 +138,13 @@
class="whitespace-nowrap font-mono text-xs leading-5 text-gray-800 dark:text-gray-200"
:title="tierHint(m)"
>
<template v-if="iv.cache_write_price != null || iv.cache_read_price != null">
<template v-if="iv.cache_write_price != null || iv.cache_write_1h_price != null || iv.cache_read_price != null">
<span class="font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheWriteShort') }}</span>
{{ paidPerMillion(iv.cache_write_price, period) }}
<template v-if="iv.cache_write_1h_price != null"
><span class="font-sans font-normal text-gray-400 dark:text-dark-500"> (1h </span>{{ paidPerMillion(iv.cache_write_1h_price, period)
}}<span class="font-sans font-normal text-gray-400 dark:text-dark-500">)</span></template
>
<span class="ml-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheReadShort') }}</span>
{{ paidPerMillion(iv.cache_read_price, period) }}
</template>
@@ -153,7 +157,11 @@
>
<div>
<span class="mr-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheWrite') }}</span>
{{ paidPerMillion(m.pricing?.cache_write_price, period) }}
{{ paidPerMillion(m.pricing?.cache_write_price, period)
}}<template v-if="m.pricing?.cache_write_1h_price != null"
><span class="font-sans font-normal text-gray-400 dark:text-dark-500"> (1h </span>{{ paidPerMillion(m.pricing.cache_write_1h_price, period)
}}<span class="font-sans font-normal text-gray-400 dark:text-dark-500">)</span></template
>
</div>
<div>
<span class="mr-1 font-sans font-normal text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheRead') }}</span>
@@ -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')"
>
<template v-if="iv.cache_write_price != null || iv.cache_read_price != null">
<template v-if="iv.cache_write_price != null || iv.cache_write_1h_price != null || iv.cache_read_price != null">
<span class="font-sans text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheWriteShort') }}</span>
{{ official(iv.cache_write_price) }}
<template v-if="iv.cache_write_1h_price != null"
><span class="font-sans text-gray-400 dark:text-dark-500"> (1h </span>{{ official(iv.cache_write_1h_price)
}}<span class="font-sans text-gray-400 dark:text-dark-500">)</span></template
>
<span class="ml-1 font-sans text-gray-400 dark:text-dark-500">{{ t('modelPlaza.table.cacheReadShort') }}</span>
{{ official(iv.cache_read_price) }}
</template>
@@ -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<PlazaModel['official_pricing']>): 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)
}
/** 档位说明:整单按档计价,或(平台旧规则)仅超出部分按档计价。 */
@@ -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',