diff --git a/frontend/src/features/models/components/GlobalModelFormDialog.vue b/frontend/src/features/models/components/GlobalModelFormDialog.vue index f7b83d5..e4faa5f 100644 --- a/frontend/src/features/models/components/GlobalModelFormDialog.vue +++ b/frontend/src/features/models/components/GlobalModelFormDialog.vue @@ -100,7 +100,7 @@

价格配置

- +
@@ -161,6 +161,7 @@ const emit = defineEmits<{ const { success, error: showError } = useToast() const submitting = ref(false) +const tieredPricingEditorRef = ref | null>(null) // 阶梯计费配置(统一使用,固定价格就是单阶梯) const tieredPricing = ref(null) @@ -275,6 +276,10 @@ async function handleSubmit() { return } + // 获取包含自动计算缓存价格的最终数据 + const finalTiers = tieredPricingEditorRef.value?.getFinalTiers() + const finalTieredPricing = finalTiers ? { tiers: finalTiers } : tieredPricing.value + submitting.value = true try { if (isEditMode.value && props.model) { @@ -283,7 +288,7 @@ async function handleSubmit() { description: form.value.description, // 使用 null 而不是 undefined 来显式清空字段 default_price_per_request: form.value.default_price_per_request ?? null, - default_tiered_pricing: tieredPricing.value, + default_tiered_pricing: finalTieredPricing, default_supports_streaming: form.value.default_supports_streaming, default_supports_image_generation: form.value.default_supports_image_generation, default_supports_vision: form.value.default_supports_vision, @@ -300,7 +305,7 @@ async function handleSubmit() { display_name: form.value.display_name!, description: form.value.description, default_price_per_request: form.value.default_price_per_request || undefined, - default_tiered_pricing: tieredPricing.value, + default_tiered_pricing: finalTieredPricing, default_supports_streaming: form.value.default_supports_streaming, default_supports_image_generation: form.value.default_supports_image_generation, default_supports_vision: form.value.default_supports_vision, diff --git a/frontend/src/features/models/components/TieredPricingEditor.vue b/frontend/src/features/models/components/TieredPricingEditor.vue index c04905b..62e8d61 100644 --- a/frontend/src/features/models/components/TieredPricingEditor.vue +++ b/frontend/src/features/models/components/TieredPricingEditor.vue @@ -287,32 +287,32 @@ function formatTokens(tokens: number): string { // 缓存价格自动计算 function getAutoCacheCreation(index: number): number { const inputPrice = localTiers.value[index]?.input_price_per_1m || 0 - return parseFloat((inputPrice * 1.25).toFixed(2)) + return parseFloat((inputPrice * 1.25).toFixed(4)) } function getAutoCacheRead(index: number): number { const inputPrice = localTiers.value[index]?.input_price_per_1m || 0 - return parseFloat((inputPrice * 0.1).toFixed(2)) + return parseFloat((inputPrice * 0.1).toFixed(4)) } function getAutoCache1h(index: number): number { const inputPrice = localTiers.value[index]?.input_price_per_1m || 0 - return parseFloat((inputPrice * 2).toFixed(2)) + return parseFloat((inputPrice * 2).toFixed(4)) } function getCacheCreationPlaceholder(index: number): string { const auto = getAutoCacheCreation(index) - return auto > 0 ? auto.toFixed(2) : '自动' + return auto > 0 ? String(auto) : '自动' } function getCacheReadPlaceholder(index: number): string { const auto = getAutoCacheRead(index) - return auto > 0 ? auto.toFixed(2) : '自动' + return auto > 0 ? String(auto) : '自动' } function getCache1hPlaceholder(index: number): string { const auto = getAutoCache1h(index) - return auto > 0 ? auto.toFixed(2) : '自动' + return auto > 0 ? String(auto) : '自动' } function getCacheCreationDisplay(index: number): string | number { @@ -346,7 +346,7 @@ function getCache1hDisplay(index: number): string | number { return '' } -// 同步到父组件(包含自动计算的缓存价格) +// 同步到父组件(只同步用户实际输入的值,不自动填充) function syncToParent() { if (validationError.value) return @@ -357,6 +357,36 @@ function syncToParent() { output_price_per_1m: t.output_price_per_1m, } + // 缓存创建价格:只有手动设置才同步 + if (cacheManuallySet[i]?.creation && t.cache_creation_price_per_1m != null) { + tier.cache_creation_price_per_1m = t.cache_creation_price_per_1m + } + + // 缓存读取价格:只有手动设置才同步 + if (cacheManuallySet[i]?.read && t.cache_read_price_per_1m != null) { + tier.cache_read_price_per_1m = t.cache_read_price_per_1m + } + + // 缓存 TTL 价格(1h 缓存)- 只有启用 1h 缓存能力且手动设置时才处理 + if (props.showCache1h && cacheManuallySet[i]?.cache1h && t.cache_ttl_pricing?.length) { + tier.cache_ttl_pricing = t.cache_ttl_pricing + } + + return tier + }) + + emit('update:modelValue', { tiers }) +} + +// 获取最终提交的数据(包含自动计算的缓存价格) +function getFinalTiers(): PricingTier[] { + return localTiers.value.map((t, i) => { + const tier: PricingTier = { + up_to: t.up_to, + input_price_per_1m: t.input_price_per_1m, + output_price_per_1m: t.output_price_per_1m, + } + // 缓存创建价格:手动设置则用设置值,否则自动计算 if (cacheManuallySet[i]?.creation && t.cache_creation_price_per_1m != null) { tier.cache_creation_price_per_1m = t.cache_creation_price_per_1m @@ -374,20 +404,21 @@ function syncToParent() { // 缓存 TTL 价格(1h 缓存)- 只有启用 1h 缓存能力时才处理 if (props.showCache1h) { if (cacheManuallySet[i]?.cache1h && t.cache_ttl_pricing?.length) { - // 手动设置则用设置值 tier.cache_ttl_pricing = t.cache_ttl_pricing } else if (t.input_price_per_1m > 0) { - // 否则自动计算 tier.cache_ttl_pricing = [{ ttl_minutes: 60, cache_creation_price_per_1m: getAutoCache1h(i) }] } } return tier }) - - emit('update:modelValue', { tiers }) } +// 暴露给父组件调用 +defineExpose({ + getFinalTiers, +}) + function parseFloatInput(value: string | number): number { const num = typeof value === 'string' ? parseFloat(value) : value return isNaN(num) ? 0 : num @@ -515,9 +546,13 @@ function removeTier(index: number) { delete cacheManuallySet[index] // 重新整理 cacheManuallySet 的索引 - const newManuallySet: Record = {} + const newManuallySet: Record< + number, + { creation: boolean; read: boolean; cache1h: boolean } + > = {} localTiers.value.forEach((_, i) => { - newManuallySet[i] = cacheManuallySet[i] || { creation: false, read: false } + newManuallySet[i] = + cacheManuallySet[i] || { creation: false, read: false, cache1h: false } }) Object.keys(cacheManuallySet).forEach(k => delete cacheManuallySet[Number(k)]) Object.assign(cacheManuallySet, newManuallySet) diff --git a/frontend/src/features/providers/components/ProviderModelFormDialog.vue b/frontend/src/features/providers/components/ProviderModelFormDialog.vue index 222d906..60b576a 100644 --- a/frontend/src/features/providers/components/ProviderModelFormDialog.vue +++ b/frontend/src/features/providers/components/ProviderModelFormDialog.vue @@ -48,7 +48,7 @@

价格配置

- +
@@ -171,6 +171,8 @@ const emit = defineEmits<{ const { error: showError, success: showSuccess } = useToast() +const tieredPricingEditorRef = ref | null>(null) + const isEditing = computed(() => !!props.editingModel) // 计算是否显示 1h 缓存输入框 @@ -325,11 +327,15 @@ async function handleSubmit() { submitting.value = true try { + // 获取包含自动计算缓存价格的最终数据 + const finalTiers = tieredPricingEditorRef.value?.getFinalTiers() + const finalTieredPricing = finalTiers ? { tiers: finalTiers } : tieredPricing.value + if (isEditing.value && props.editingModel) { // 编辑模式 // 注意:使用 null 而不是 undefined 来显式清空字段(undefined 会被 JSON 序列化忽略) await updateModel(props.providerId, props.editingModel.id, { - tiered_pricing: tieredPricing.value, + tiered_pricing: finalTieredPricing, price_per_request: form.value.price_per_request ?? null, supports_vision: form.value.supports_vision, supports_function_calling: form.value.supports_function_calling, @@ -346,7 +352,7 @@ async function handleSubmit() { global_model_id: form.value.global_model_id, provider_model_name: selectedModel?.name || '', // 只有修改了才提交,否则传 undefined 让后端继承 GlobalModel 配置 - tiered_pricing: tieredPricingModified.value ? tieredPricing.value : undefined, + tiered_pricing: tieredPricingModified.value ? finalTieredPricing : undefined, price_per_request: form.value.price_per_request, supports_vision: form.value.supports_vision, supports_function_calling: form.value.supports_function_calling,