From 415a0dc9d0cae3bda18bb1c1573e16de999eb3bf Mon Sep 17 00:00:00 2001
From: milesians <37968554+Milesians@users.noreply.github.com>
Date: Mon, 7 Sep 2026 10:11:07 +0800
Subject: [PATCH 1/3] fix: display multiplier-only interval prices
---
.../handler/available_channel_handler.go | 44 ++++++++-------
.../handler/available_channel_handler_test.go | 18 ++++++-
frontend/src/api/channels.ts | 4 ++
frontend/src/api/modelPlaza.ts | 2 +-
.../channels/SupportedModelChip.vue | 25 ++++++---
.../__tests__/SupportedModelChip.spec.ts | 54 +++++++++++++++++++
.../modelPlaza/PlazaModelPricingTable.vue | 43 +++++++++------
.../__tests__/PlazaModelPricingTable.spec.ts | 38 +++++++++++++
8 files changed, 185 insertions(+), 43 deletions(-)
create mode 100644 frontend/src/components/channels/__tests__/SupportedModelChip.spec.ts
diff --git a/backend/internal/handler/available_channel_handler.go b/backend/internal/handler/available_channel_handler.go
index ffa2408bcd..ae01687bdb 100644
--- a/backend/internal/handler/available_channel_handler.go
+++ b/backend/internal/handler/available_channel_handler.go
@@ -81,15 +81,19 @@ type userSupportedModelPricing struct {
// userPricingIntervalDTO 定价区间白名单(去掉内部 ID、SortOrder 等前端不渲染的字段)。
type userPricingIntervalDTO struct {
- MinTokens int `json:"min_tokens"`
- MaxTokens *int `json:"max_tokens"`
- TierLabel string `json:"tier_label,omitempty"`
- InputPrice *float64 `json:"input_price"`
- OutputPrice *float64 `json:"output_price"`
- CacheWritePrice *float64 `json:"cache_write_price"`
- CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
- CacheReadPrice *float64 `json:"cache_read_price"`
- PerRequestPrice *float64 `json:"per_request_price"`
+ MinTokens int `json:"min_tokens"`
+ MaxTokens *int `json:"max_tokens"`
+ TierLabel string `json:"tier_label,omitempty"`
+ InputPrice *float64 `json:"input_price"`
+ OutputPrice *float64 `json:"output_price"`
+ CacheWritePrice *float64 `json:"cache_write_price"`
+ CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
+ CacheReadPrice *float64 `json:"cache_read_price"`
+ InputMultiplier *float64 `json:"input_multiplier"`
+ OutputMultiplier *float64 `json:"output_multiplier"`
+ CacheWriteMultiplier *float64 `json:"cache_write_multiplier"`
+ CacheReadMultiplier *float64 `json:"cache_read_multiplier"`
+ PerRequestPrice *float64 `json:"per_request_price"`
}
// userSupportedModel 用户可见的支持模型条目。
@@ -295,15 +299,19 @@ func toUserPricingIntervals(src []service.PricingInterval) []userPricingInterval
intervals := make([]userPricingIntervalDTO, 0, len(src))
for _, iv := range src {
intervals = append(intervals, userPricingIntervalDTO{
- MinTokens: iv.MinTokens,
- MaxTokens: iv.MaxTokens,
- TierLabel: iv.TierLabel,
- InputPrice: iv.InputPrice,
- OutputPrice: iv.OutputPrice,
- CacheWritePrice: iv.CacheWritePrice,
- CacheWrite1hPrice: iv.CacheWrite1hPrice,
- CacheReadPrice: iv.CacheReadPrice,
- PerRequestPrice: iv.PerRequestPrice,
+ MinTokens: iv.MinTokens,
+ MaxTokens: iv.MaxTokens,
+ TierLabel: iv.TierLabel,
+ InputPrice: iv.InputPrice,
+ OutputPrice: iv.OutputPrice,
+ CacheWritePrice: iv.CacheWritePrice,
+ CacheWrite1hPrice: iv.CacheWrite1hPrice,
+ CacheReadPrice: iv.CacheReadPrice,
+ InputMultiplier: iv.InputMultiplier,
+ OutputMultiplier: iv.OutputMultiplier,
+ CacheWriteMultiplier: iv.CacheWriteMultiplier,
+ CacheReadMultiplier: iv.CacheReadMultiplier,
+ PerRequestPrice: iv.PerRequestPrice,
})
}
return intervals
diff --git a/backend/internal/handler/available_channel_handler_test.go b/backend/internal/handler/available_channel_handler_test.go
index 24d7d1cb45..325f10133b 100644
--- a/backend/internal/handler/available_channel_handler_test.go
+++ b/backend/internal/handler/available_channel_handler_test.go
@@ -113,10 +113,18 @@ func TestUserAvailableChannel_FieldWhitelist(t *testing.T) {
}
// pricing interval 白名单:不应暴露 id / sort_order。
+ inputMultiplier := 2.0
+ outputMultiplier := 1.5
+ cacheWriteMultiplier := 2.0
+ cacheReadMultiplier := 2.0
pricing := toUserPricing(&service.ChannelModelPricing{
BillingMode: service.BillingModeToken,
Intervals: []service.PricingInterval{
- {ID: 7, MinTokens: 0, MaxTokens: nil, SortOrder: 3},
+ {
+ ID: 7, MinTokens: 0, MaxTokens: nil, SortOrder: 3,
+ InputMultiplier: &inputMultiplier, OutputMultiplier: &outputMultiplier,
+ CacheWriteMultiplier: &cacheWriteMultiplier, CacheReadMultiplier: &cacheReadMultiplier,
+ },
},
})
require.NotNil(t, pricing)
@@ -129,6 +137,14 @@ func TestUserAvailableChannel_FieldWhitelist(t *testing.T) {
_, exists := ivDecoded[key]
require.Falsef(t, exists, "user pricing interval must not expose %q", key)
}
+ for key, want := range map[string]float64{
+ "input_multiplier": inputMultiplier, "output_multiplier": outputMultiplier,
+ "cache_write_multiplier": cacheWriteMultiplier, "cache_read_multiplier": cacheReadMultiplier,
+ } {
+ got, exists := ivDecoded[key]
+ require.Truef(t, exists, "user pricing interval must expose %q", key)
+ require.InDelta(t, want, got.(float64), 1e-12)
+ }
}
func TestBuildPlatformSections_GroupsByPlatform(t *testing.T) {
diff --git a/frontend/src/api/channels.ts b/frontend/src/api/channels.ts
index c998fc3efb..5c3f83542e 100644
--- a/frontend/src/api/channels.ts
+++ b/frontend/src/api/channels.ts
@@ -31,6 +31,10 @@ export interface UserPricingInterval {
cache_write_price: number | null
cache_write_1h_price?: number | null
cache_read_price: number | null
+ input_multiplier?: number | null
+ output_multiplier?: number | null
+ cache_write_multiplier?: number | null
+ cache_read_multiplier?: number | null
per_request_price: number | null
}
diff --git a/frontend/src/api/modelPlaza.ts b/frontend/src/api/modelPlaza.ts
index e96a621934..5096efbe82 100644
--- a/frontend/src/api/modelPlaza.ts
+++ b/frontend/src/api/modelPlaza.ts
@@ -46,7 +46,7 @@ export interface PlazaTimePricing {
export interface PlazaModel {
name: string
platform: string
- /** 实收口径的展示定价:多档时 intervals 为各档绝对单价(已由计费服务折算);均为标准时段价。 */
+ /** 实收口径的展示定价:档位可提供绝对单价或相对基础价倍率;均为标准时段价。 */
pricing: UserSupportedModelPricing | null
official_pricing: PlazaOfficialPricing | null
/** 仅多档模型返回。 */
diff --git a/frontend/src/components/channels/SupportedModelChip.vue b/frontend/src/components/channels/SupportedModelChip.vue
index b94fc9f072..04a2ad9de5 100644
--- a/frontend/src/components/channels/SupportedModelChip.vue
+++ b/frontend/src/components/channels/SupportedModelChip.vue
@@ -153,7 +153,7 @@
{{ iv.tier_label }}
{{ formatRange(iv.min_tokens, iv.max_tokens) }}
- {{ formatInterval(iv, model.pricing.billing_mode) }}
+ {{ formatInterval(iv, model.pricing) }}
@@ -172,12 +172,11 @@ import { formatScaled } from '@/utils/pricing'
import {
BILLING_MODE_TOKEN,
BILLING_MODE_PER_REQUEST,
- BILLING_MODE_IMAGE,
- type BillingMode
+ BILLING_MODE_IMAGE
} from '@/constants/channel'
// 复用 api/channels.ts 的用户侧最小形态 DTO。
// admin 侧 ChannelModelPricing 字段更多,但结构上是用户 DTO 的超集,admin 视图传入可直接通过结构化子类型检查。
-import type { UserPricingInterval, UserSupportedModel } from '@/api/channels'
+import type { UserPricingInterval, UserSupportedModel, UserSupportedModelPricing } from '@/api/channels'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
import type { GroupPlatform } from '@/types'
import { platformBadgeClass, platformBorderClass, platformBadgeLightClass } from '@/utils/platformColors'
@@ -246,12 +245,22 @@ function formatRange(min: number, max: number | null): string {
return `(${min}, ${maxLabel}]`
}
-function formatInterval(iv: UserPricingInterval, mode: BillingMode): string {
- if (mode === BILLING_MODE_PER_REQUEST || mode === BILLING_MODE_IMAGE) {
+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(iv.input_price, perMillionScale)
- const output = formatScaled(iv.output_price, perMillionScale)
+ 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)
return `${input} / ${output}`
}
diff --git a/frontend/src/components/channels/__tests__/SupportedModelChip.spec.ts b/frontend/src/components/channels/__tests__/SupportedModelChip.spec.ts
new file mode 100644
index 0000000000..b3799b697c
--- /dev/null
+++ b/frontend/src/components/channels/__tests__/SupportedModelChip.spec.ts
@@ -0,0 +1,54 @@
+import { nextTick } from 'vue'
+import { describe, expect, it, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import SupportedModelChip from '../SupportedModelChip.vue'
+
+vi.mock('vue-i18n', async () => {
+ const actual = await vi.importActual('vue-i18n')
+ return {
+ ...actual,
+ useI18n: () => ({ t: (key: string) => key })
+ }
+})
+
+describe('SupportedModelChip', () => {
+ it('仅配置区间倍率时按基础价展示 token 档位', async () => {
+ const wrapper = mount(SupportedModelChip, {
+ attachTo: document.body,
+ props: {
+ model: {
+ name: 'gpt-test',
+ platform: '',
+ pricing: {
+ billing_mode: 'token',
+ input_price: 10e-6,
+ output_price: 50e-6,
+ cache_write_price: null,
+ cache_read_price: null,
+ image_input_price: null,
+ image_output_price: null,
+ per_request_price: null,
+ intervals: [{
+ min_tokens: 272000,
+ max_tokens: null,
+ input_price: null,
+ output_price: null,
+ cache_write_price: null,
+ cache_read_price: null,
+ input_multiplier: 2,
+ output_multiplier: 1.5,
+ per_request_price: null
+ }]
+ }
+ },
+ showPlatform: false
+ }
+ })
+
+ await wrapper.find('[tabindex="0"]').trigger('mouseenter')
+ await nextTick()
+
+ expect(document.body.textContent).toContain('$20 / $75')
+ wrapper.unmount()
+ })
+})
diff --git a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue
index 43aee2c140..59910f2a36 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(iv.input_price, period) }}
+ {{ paidPerMillion(effectiveIntervalPrice(iv.input_price, iv.input_multiplier, m.pricing?.input_price), period) }}
{{ paidPerMillion(m.pricing?.input_price, period) }}
@@ -132,7 +132,7 @@
class="whitespace-nowrap text-xs leading-5"
:title="tierHint(m)"
>
- {{ paidPerMillion(iv.output_price, period) }}
+ {{ paidPerMillion(effectiveIntervalPrice(iv.output_price, iv.output_multiplier, m.pricing?.output_price), period) }}
{{ paidPerMillion(m.pricing?.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)"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
- {{ paidPerMillion(iv.cache_write_price, period) }}
- (1h {{ paidPerMillion(iv.cache_write_1h_price, period)
+ {{ paidPerMillion(effectiveIntervalPrice(iv.cache_write_price, iv.cache_write_multiplier, m.pricing?.cache_write_price), period) }}
+ (1h {{ paidPerMillion(effectiveIntervalPrice(iv.cache_write_1h_price, iv.cache_write_multiplier, m.pricing?.cache_write_1h_price), period)
}})
{{ t('modelPlaza.table.cacheReadShort') }}
- {{ paidPerMillion(iv.cache_read_price, period) }}
+ {{ paidPerMillion(effectiveIntervalPrice(iv.cache_read_price, iv.cache_read_multiplier, m.pricing?.cache_read_price), period) }}
-
@@ -217,7 +217,7 @@
class="whitespace-nowrap leading-5"
>
{{ tierLabel(iv) }}
- {{ official(iv.input_price) }}
+ {{ official(effectiveIntervalPrice(iv.input_price, iv.input_multiplier, m.official_pricing?.input_price)) }}
{{ official(m.official_pricing?.input_price) }}
@@ -230,7 +230,7 @@
class="whitespace-nowrap leading-5"
:title="t('modelPlaza.table.tierHint')"
>
- {{ official(iv.output_price) }}
+ {{ official(effectiveIntervalPrice(iv.output_price, iv.output_multiplier, m.official_pricing?.output_price)) }}
{{ official(m.official_pricing?.output_price) }}
@@ -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')"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
- {{ official(iv.cache_write_price) }}
- (1h {{ official(iv.cache_write_1h_price)
+ {{ official(effectiveIntervalPrice(iv.cache_write_price, iv.cache_write_multiplier, m.official_pricing?.cache_write_price)) }}
+ (1h {{ official(effectiveIntervalPrice(iv.cache_write_1h_price, iv.cache_write_multiplier, m.official_pricing?.cache_write_1h_price))
}})
{{ t('modelPlaza.table.cacheReadShort') }}
- {{ official(iv.cache_read_price) }}
+ {{ official(effectiveIntervalPrice(iv.cache_read_price, iv.cache_read_multiplier, m.official_pricing?.cache_read_price)) }}
-
@@ -434,6 +434,16 @@ function official(value: number | null | undefined): string {
return formatScaled(value, PER_MILLION, MIN_DECIMALS)
}
+function effectiveIntervalPrice(
+ price: number | null | undefined,
+ multiplier: number | null | undefined,
+ basePrice: number | null | undefined
+): number | null | undefined {
+ if (price != null) return price
+ if (multiplier != null && basePrice != null) return basePrice * multiplier
+ return basePrice
+}
+
/** 非 token 计费的单位后缀:按图片 → “/ 张”,按次 → “/ 次”。 */
function perUnitSuffix(m: PlazaModel): string {
return billingMode(m) === BILLING_MODE_IMAGE
@@ -495,7 +505,10 @@ function officialIntervals(m: PlazaModel): UserPricingInterval[] {
/** 任一档带缓存价才按档渲染缓存列;否则沿用平价的写入/读取两行。 */
function hasTierCachePricing(intervals: UserPricingInterval[]): boolean {
- return intervals.some((iv) => iv.cache_write_price != null || iv.cache_write_1h_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 ||
+ iv.cache_write_multiplier != null || iv.cache_read_multiplier != null
+ )
}
/** 档位说明:整单按档计价,或(平台旧规则)仅超出部分按档计价。 */
diff --git a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
index 902195f384..e5bee72305 100644
--- a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
+++ b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts
@@ -294,6 +294,44 @@ describe('PlazaModelPricingTable', () => {
expect(text).toContain('$15.00')
})
+ it('仅配置区间倍率时按基础价格展示 input/output/cache 各档价格', () => {
+ const model = tokenModel({
+ pricing: {
+ billing_mode: 'token',
+ input_price: 10e-6,
+ output_price: 50e-6,
+ cache_write_price: 12.5e-6,
+ cache_write_1h_price: 12.5e-6,
+ cache_read_price: 2e-6,
+ image_input_price: null,
+ image_output_price: null,
+ per_request_price: null,
+ intervals: [{
+ min_tokens: 272000,
+ max_tokens: null,
+ tier_label: '>272K',
+ input_price: null,
+ output_price: null,
+ cache_write_price: null,
+ cache_write_1h_price: null,
+ cache_read_price: null,
+ input_multiplier: 2,
+ output_multiplier: 1.5,
+ cache_write_multiplier: 2,
+ cache_read_multiplier: 2,
+ per_request_price: null
+ }]
+ },
+ official_pricing: null
+ })
+
+ const text = mountTable([model], 1).text()
+ expect(text).toContain('$20.00')
+ expect(text).toContain('$75.00')
+ expect(text).toContain('$25.00')
+ expect(text).toContain('$4.00')
+ })
+
it('生图独立倍率开启时,按图价格 × 独立倍率,不乘分组倍率;倍率列展示独立倍率', () => {
const model = tokenModel({
name: 'gpt-image-2',
From ff758f37d2e751aa26183c6b4473974eb5c69971 Mon Sep 17 00:00:00 2001
From: milesians <37968554+Milesians@users.noreply.github.com>
Date: Mon, 7 Sep 2026 11:07:35 +0800
Subject: [PATCH 2/3] fix: preserve plaza 1h pricing and align interval
fallbacks
---
.../service/billing_context_schedule.go | 26 ++++++++----
.../service/billing_context_schedule_test.go | 18 ++++++++
.../internal/service/model_plaza_service.go | 18 ++++----
.../channels/SupportedModelChip.vue | 17 ++------
.../modelPlaza/PlazaModelPricingTable.vue | 42 +++++++------------
.../__tests__/PlazaModelPricingTable.spec.ts | 9 ++++
.../utils/__tests__/intervalPrices.spec.ts | 20 +++++++++
frontend/src/utils/pricing.ts | 18 ++++++++
8 files changed, 112 insertions(+), 56 deletions(-)
create mode 100644 frontend/src/utils/__tests__/intervalPrices.spec.ts
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) }}
{{ paidPerMillion(m.pricing?.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) }}
{{ paidPerMillion(m.pricing?.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)"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
- {{ paidPerMillion(effectiveIntervalPrice(iv.cache_write_price, iv.cache_write_multiplier, m.pricing?.cache_write_price), period) }}
- (1h {{ paidPerMillion(effectiveIntervalPrice(iv.cache_write_1h_price, iv.cache_write_multiplier, m.pricing?.cache_write_1h_price), period)
+ {{ paidPerMillion(iv.cache_write_price, period) }}
+ (1h {{ paidPerMillion(iv.cache_write_1h_price, period)
}})
{{ t('modelPlaza.table.cacheReadShort') }}
- {{ paidPerMillion(effectiveIntervalPrice(iv.cache_read_price, iv.cache_read_multiplier, m.pricing?.cache_read_price), period) }}
+ {{ paidPerMillion(iv.cache_read_price, period) }}
-
@@ -217,7 +217,7 @@
class="whitespace-nowrap leading-5"
>
{{ tierLabel(iv) }}
- {{ official(effectiveIntervalPrice(iv.input_price, iv.input_multiplier, m.official_pricing?.input_price)) }}
+ {{ official(iv.input_price) }}
{{ official(m.official_pricing?.input_price) }}
@@ -230,7 +230,7 @@
class="whitespace-nowrap leading-5"
:title="t('modelPlaza.table.tierHint')"
>
- {{ official(effectiveIntervalPrice(iv.output_price, iv.output_multiplier, m.official_pricing?.output_price)) }}
+ {{ official(iv.output_price) }}
{{ official(m.official_pricing?.output_price) }}
@@ -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')"
>
-
+
{{ t('modelPlaza.table.cacheWriteShort') }}
- {{ official(effectiveIntervalPrice(iv.cache_write_price, iv.cache_write_multiplier, m.official_pricing?.cache_write_price)) }}
- (1h {{ official(effectiveIntervalPrice(iv.cache_write_1h_price, iv.cache_write_multiplier, m.official_pricing?.cache_write_1h_price))
+ {{ official(iv.cache_write_price) }}
+ (1h {{ official(iv.cache_write_1h_price)
}})
{{ t('modelPlaza.table.cacheReadShort') }}
- {{ official(effectiveIntervalPrice(iv.cache_read_price, iv.cache_read_multiplier, m.official_pricing?.cache_read_price)) }}
+ {{ official(iv.cache_read_price) }}
-
@@ -306,7 +306,7 @@