mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 16:23:31 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user