diff --git a/backend/internal/service/billing_context_schedule.go b/backend/internal/service/billing_context_schedule.go index 7773022504..d1f994707c 100644 --- a/backend/internal/service/billing_context_schedule.go +++ b/backend/internal/service/billing_context_schedule.go @@ -19,13 +19,14 @@ const ContextPricingBasisWholeRequest ContextPricingBasis = "whole_request" // ContextPricingTier (MinTokens, MaxTokens] 区间内的有效 per-token 单价(USD)。 // nil 表示该项无价/不计费;MaxTokens 为 nil 表示无上限。 type ContextPricingTier struct { - MinTokens int - MaxTokens *int - Label string - Input *float64 - Output *float64 - CacheWrite *float64 - CacheRead *float64 + MinTokens int + MaxTokens *int + Label string + Input *float64 + Output *float64 + CacheWrite *float64 + CacheWrite1h *float64 + CacheRead *float64 } // TimePricingPeriod 分时倍率时段:配置时区当天 [StartTime, EndTime) 内整单费用乘 Multiplier。 @@ -289,6 +290,10 @@ func probeContextTier(seg contextSegment, resolved *ResolvedPricing, probe func( return tier, err } // 输出价只随上下文所在档变化:固定上下文 c,对输出 token 数做差商(固定部分相减抵消)。 + tier.CacheWrite1h, err = probeComponentPrice(func(n int) UsageTokens { return UsageTokens{CacheCreationTokens: n, CacheCreation1hTokens: n} }, c, delta, probe) + if err != nil { + return tier, err + } tier.Output, err = probeComponentPrice(func(n int) UsageTokens { return UsageTokens{InputTokens: c, OutputTokens: n} }, 0, contextProbeDelta, probe) if err != nil { return tier, err @@ -298,6 +303,7 @@ func probeContextTier(seg contextSegment, resolved *ResolvedPricing, probe func( tier.Input = contextPricePtr(tier.Input, explicit.input) tier.Output = contextPricePtr(tier.Output, explicit.output) tier.CacheWrite = contextPricePtr(tier.CacheWrite, explicit.cacheWrite) + tier.CacheWrite1h = contextPricePtr(tier.CacheWrite1h, explicit.cacheWrite1h) tier.CacheRead = contextPricePtr(tier.CacheRead, explicit.cacheRead) return tier, nil } @@ -341,7 +347,7 @@ func roundContextPrice(v float64) float64 { } type explicitContextFields struct { - input, output, cacheWrite, cacheRead bool + input, output, cacheWrite, cacheWrite1h, cacheRead bool } // explicitContextPricingFields 判断各项是否被分组卡/渠道定价(含命中区间)显式配置。 @@ -355,11 +361,13 @@ func explicitContextPricingFields(resolved *ResolvedPricing, contextTokens int) out.input = cp.InputPrice != nil out.output = cp.OutputPrice != nil out.cacheWrite = cp.CacheWritePrice != nil + out.cacheWrite1h = cp.CacheWrite1hPrice != nil || cp.CacheWritePrice != nil out.cacheRead = cp.CacheReadPrice != nil if iv := FindMatchingInterval(resolved.Intervals, contextTokens); iv != nil { out.input = out.input || iv.InputPrice != nil out.output = out.output || iv.OutputPrice != nil out.cacheWrite = out.cacheWrite || iv.CacheWritePrice != nil + out.cacheWrite1h = out.cacheWrite1h || iv.CacheWrite1hPrice != nil || iv.CacheWritePrice != nil out.cacheRead = out.cacheRead || iv.CacheReadPrice != nil } return out @@ -393,7 +401,7 @@ func mergeEqualContextTiers(tiers []ContextPricingTier) []ContextPricingTier { func sameContextPrices(a, b ContextPricingTier) bool { return samePricePtr(a.Input, b.Input) && samePricePtr(a.Output, b.Output) && - samePricePtr(a.CacheWrite, b.CacheWrite) && samePricePtr(a.CacheRead, b.CacheRead) + samePricePtr(a.CacheWrite, b.CacheWrite) && samePricePtr(a.CacheWrite1h, b.CacheWrite1h) && samePricePtr(a.CacheRead, b.CacheRead) } func samePricePtr(a, b *float64) bool { diff --git a/backend/internal/service/billing_context_schedule_test.go b/backend/internal/service/billing_context_schedule_test.go index dfe43f1265..fa2c4bd8ff 100644 --- a/backend/internal/service/billing_context_schedule_test.go +++ b/backend/internal/service/billing_context_schedule_test.go @@ -385,6 +385,24 @@ func TestResolveContextPricingSchedule_NilResolver(t *testing.T) { require.Nil(t, sched) } +func TestPlazaSchedulePreservesCacheWrite1h(t *testing.T) { + cp := sonnetChannel(PricingInterval{MinTokens: 272000, CacheWriteMultiplier: testPtrFloat64(2)}) + cp[0].CacheWritePrice = testPtrFloat64(12.5e-6) + cp[0].CacheWrite1hPrice = testPtrFloat64(20e-6) + group := enabledGroup(PlatformAnthropic) + bs, resolver := newScheduleTestEnv(t, scheduleScenario{channel: cp, groupPlatform: PlatformAnthropic}) + sched, err := bs.ResolveContextPricingSchedule(context.Background(), resolver, ContextPricingScheduleInput{Model: "claude-sonnet-4", Group: group, Platform: PlatformAnthropic}) + require.NoError(t, err) + display := plazaPricingFromSchedule(&cp[0], sched) + requirePrice(t, testPtrFloat64(20e-6), display.CacheWrite1hPrice, "base 1h") + require.Len(t, display.Intervals, 2) + requirePrice(t, testPtrFloat64(25e-6), display.Intervals[1].CacheWritePrice, "tier 5m") + requirePrice(t, testPtrFloat64(40e-6), display.Intervals[1].CacheWrite1hPrice, "tier 1h") + a := ContextPricingTier{CacheWrite1h: testPtrFloat64(1)} + b := ContextPricingTier{CacheWrite1h: testPtrFloat64(2)} + require.Len(t, mergeEqualContextTiers([]ContextPricingTier{a, b}), 2) +} + // --- 对账:阶梯表推算的费用必须等于真实计费函数 --- type tokenKind int diff --git a/backend/internal/service/model_plaza_service.go b/backend/internal/service/model_plaza_service.go index 6b2a87b263..b1618acf78 100644 --- a/backend/internal/service/model_plaza_service.go +++ b/backend/internal/service/model_plaza_service.go @@ -269,6 +269,7 @@ func plazaPricingFromSchedule(raw *ChannelModelPricing, sched *ContextPricingSch out.InputPrice = first.Input out.OutputPrice = first.Output out.CacheWritePrice = first.CacheWrite + out.CacheWrite1hPrice = first.CacheWrite1h out.CacheReadPrice = first.CacheRead if len(sched.Tiers) > 1 { out.Intervals = plazaIntervalsFromTiers(sched.Tiers) @@ -280,14 +281,15 @@ func plazaIntervalsFromTiers(tiers []ContextPricingTier) []PricingInterval { intervals := make([]PricingInterval, 0, len(tiers)) for i, t := range tiers { intervals = append(intervals, PricingInterval{ - MinTokens: t.MinTokens, - MaxTokens: t.MaxTokens, - TierLabel: t.Label, - InputPrice: t.Input, - OutputPrice: t.Output, - CacheWritePrice: t.CacheWrite, - CacheReadPrice: t.CacheRead, - SortOrder: i, + MinTokens: t.MinTokens, + MaxTokens: t.MaxTokens, + TierLabel: t.Label, + InputPrice: t.Input, + OutputPrice: t.Output, + CacheWritePrice: t.CacheWrite, + CacheWrite1hPrice: t.CacheWrite1h, + CacheReadPrice: t.CacheRead, + SortOrder: i, }) } return intervals diff --git a/frontend/src/components/channels/SupportedModelChip.vue b/frontend/src/components/channels/SupportedModelChip.vue index 04a2ad9de5..e64c28ebc1 100644 --- a/frontend/src/components/channels/SupportedModelChip.vue +++ b/frontend/src/components/channels/SupportedModelChip.vue @@ -168,7 +168,7 @@ import { computed, nextTick, onBeforeUnmount, ref } from 'vue' import { useI18n } from 'vue-i18n' import PricingRow from './PricingRow.vue' -import { formatScaled } from '@/utils/pricing' +import { formatScaled, resolveIntervalPrices } from '@/utils/pricing' import { BILLING_MODE_TOKEN, BILLING_MODE_PER_REQUEST, @@ -245,22 +245,13 @@ function formatRange(min: number, max: number | null): string { return `(${min}, ${maxLabel}]` } -function effectiveIntervalPrice( - price: number | null | undefined, - multiplier: number | null | undefined, - basePrice: number | null -): number | null { - if (price != null) return price - if (multiplier != null && basePrice != null) return basePrice * multiplier - return basePrice -} - function formatInterval(iv: UserPricingInterval, pricing: UserSupportedModelPricing): string { if (pricing.billing_mode === BILLING_MODE_PER_REQUEST || pricing.billing_mode === BILLING_MODE_IMAGE) { return formatScaled(iv.per_request_price, 1) } - const input = formatScaled(effectiveIntervalPrice(iv.input_price, iv.input_multiplier, pricing.input_price), perMillionScale) - const output = formatScaled(effectiveIntervalPrice(iv.output_price, iv.output_multiplier, pricing.output_price), perMillionScale) + const resolved = resolveIntervalPrices(iv, pricing) + const input = formatScaled(resolved.input_price, perMillionScale) + const output = formatScaled(resolved.output_price, perMillionScale) return `${input} / ${output}` } diff --git a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue index 59910f2a36..a3c7d32bc0 100644 --- a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue +++ b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue @@ -119,7 +119,7 @@ class="whitespace-nowrap text-xs leading-5" > {{ tierLabel(iv) }} - {{ paidPerMillion(effectiveIntervalPrice(iv.input_price, iv.input_multiplier, m.pricing?.input_price), period) }} + {{ paidPerMillion(iv.input_price, period) }} @@ -132,7 +132,7 @@ class="whitespace-nowrap text-xs leading-5" :title="tierHint(m)" > - {{ paidPerMillion(effectiveIntervalPrice(iv.output_price, iv.output_multiplier, m.pricing?.output_price), period) }} + {{ paidPerMillion(iv.output_price, period) }} @@ -145,15 +145,15 @@ class="whitespace-nowrap font-mono text-xs leading-5 text-gray-800 dark:text-gray-200" :title="tierHint(m)" > - @@ -243,15 +243,15 @@ class="whitespace-nowrap font-mono text-xs leading-5 text-gray-500 dark:text-dark-400" :title="t('modelPlaza.table.tierHint')" > -