Merge pull request #7524 from Achordchan/fix/plaza-video-rate

fix(model-plaza): apply independent video billing multipliers
This commit is contained in:
Wesley Liddick
2026-09-28 09:55:34 +08:00
committed by GitHub
9 changed files with 95 additions and 8 deletions
@@ -92,6 +92,8 @@ type modelPlazaGroup struct {
// 不取分组/用户专属倍率。
ImageRateIndependent bool `json:"image_rate_independent"`
ImageRateMultiplier float64 `json:"image_rate_multiplier"`
VideoRateIndependent bool `json:"video_rate_independent"`
VideoRateMultiplier float64 `json:"video_rate_multiplier"`
// 分组是否启用长上下文阶梯计费;关闭时模型实付列只展示最低档/基础价。
LongContextPricingEnabled bool `json:"long_context_pricing_enabled"`
Models []modelPlazaModel `json:"models"`
@@ -211,6 +213,8 @@ func toModelPlazaGroupDTO(g *service.PlazaGroup, userRates map[int64]float64) mo
IsExclusive: g.IsExclusive,
ImageRateIndependent: g.ImageRateIndependent,
ImageRateMultiplier: g.ImageRateMultiplier,
VideoRateIndependent: g.VideoRateIndependent,
VideoRateMultiplier: g.VideoRateMultiplier,
LongContextPricingEnabled: g.LongContextPricingEnabled,
Models: models,
}
@@ -89,6 +89,7 @@ func TestToModelPlazaGroupDTO_UserRateAndFieldWhitelist(t *testing.T) {
g := service.PlazaGroup{
ID: 2, Name: "vip", Description: "d", Platform: "anthropic",
SubscriptionType: "standard", RateMultiplier: 1, IsExclusive: true,
VideoRateIndependent: true, VideoRateMultiplier: 0.7,
Models: []service.PlazaModel{{
Name: "claude-sonnet",
Platform: "anthropic",
@@ -115,11 +116,14 @@ func TestToModelPlazaGroupDTO_UserRateAndFieldWhitelist(t *testing.T) {
"rate_multiplier", "user_rate_multiplier", "is_exclusive", "models",
"peak_rate_enabled", "peak_start", "peak_end", "peak_rate_multiplier",
"image_rate_independent", "image_rate_multiplier", "long_context_pricing_enabled",
"video_rate_independent", "video_rate_multiplier",
} {
_, exists := decoded[key]
require.Truef(t, exists, "plaza group DTO must expose %q", key)
}
require.InDelta(t, 0.5, decoded["user_rate_multiplier"].(float64), 1e-9)
require.Equal(t, true, decoded["video_rate_independent"])
require.Equal(t, 0.7, decoded["video_rate_multiplier"])
// 模型条目:pricing + official_pricing 并存;official 缺失字段输出 null 而非省略
models := decoded["models"].([]any)
@@ -52,6 +52,9 @@ type PlazaGroup struct {
// = 档位价 × ImageRateMultiplier,不乘分组/用户专属倍率(与计费口径一致)。
ImageRateIndependent bool
ImageRateMultiplier float64
// 视频独立倍率与图片独立倍率分别配置,开启时覆盖分组/用户专属倍率。
VideoRateIndependent bool
VideoRateMultiplier float64
// LongContextPricingEnabled 分组是否按上下文长度应用阶梯价;关闭时模型展示的是最低档。
LongContextPricingEnabled bool
Models []PlazaModel
@@ -132,6 +135,8 @@ func (s *ModelPlazaService) ListGroups(ctx context.Context) ([]PlazaGroup, error
IsExclusive: g.IsExclusive,
ImageRateIndependent: g.ImageRateIndependent,
ImageRateMultiplier: g.ImageRateMultiplier,
VideoRateIndependent: g.VideoRateIndependent,
VideoRateMultiplier: g.VideoRateMultiplier,
LongContextPricingEnabled: g.LongContextPricingEnabled,
}
groupEnt[g.ID] = g
@@ -265,7 +265,8 @@ func TestListPlazaGroups_GroupImagePriceOverridesChannelPricing(t *testing.T) {
}}
groups := []Group{
{ID: 10, Name: "g-media", Platform: "openai", RateMultiplier: 1,
ImagePrice1K: &imgPrice, ImageRateIndependent: true, ImageRateMultiplier: 1},
ImagePrice1K: &imgPrice, ImageRateIndependent: true, ImageRateMultiplier: 1,
VideoRateIndependent: true, VideoRateMultiplier: 0.7},
{ID: 20, Name: "g-plain", Platform: "openai", RateMultiplier: 0.1},
}
svc := newPlazaService(channels, groups, nil)
@@ -279,6 +280,8 @@ func TestListPlazaGroups_GroupImagePriceOverridesChannelPricing(t *testing.T) {
media := byName["g-media"]
require.True(t, media.ImageRateIndependent)
require.True(t, media.VideoRateIndependent)
require.InDelta(t, 0.7, media.VideoRateMultiplier, 1e-9)
require.InDelta(t, 1.0, media.ImageRateMultiplier, 1e-9)
require.Len(t, media.Models, 1)
p := media.Models[0].Pricing
@@ -295,6 +298,7 @@ func TestListPlazaGroups_GroupImagePriceOverridesChannelPricing(t *testing.T) {
plain := byName["g-plain"]
require.False(t, plain.ImageRateIndependent)
require.False(t, plain.VideoRateIndependent)
require.Len(t, plain.Models, 1)
pp := plain.Models[0].Pricing
require.NotNil(t, pp)
+3
View File
@@ -73,6 +73,9 @@ export interface ModelPlazaGroup {
/** 生图独立倍率:true 时图片计费模型的实付倍率取 image_rate_multiplier,不取分组/专属倍率。 */
image_rate_independent: boolean
image_rate_multiplier: number
/** 视频独立倍率开启时,覆盖视频模型的分组/用户专属倍率。 */
video_rate_independent: boolean
video_rate_multiplier: number
/** 分组是否启用长上下文阶梯计费;false 时实付列只展示最低档,官方阶梯仅供参考。 */
long_context_pricing_enabled: boolean
models: PlazaModel[]
@@ -61,6 +61,8 @@
:user-rate-multiplier="group.user_rate_multiplier ?? null"
:image-rate-independent="group.image_rate_independent"
:image-rate-multiplier="group.image_rate_multiplier"
:video-rate-independent="group.video_rate_independent"
:video-rate-multiplier="group.video_rate_multiplier"
:peak-window="peakWindow"
:peak-rate-multiplier="group.peak_rate_multiplier"
/>
@@ -289,7 +289,7 @@
>{{ periodRate(period) }}x</span
>
<span
v-else-if="usesIndependentImageRate(m)"
v-else-if="usesIndependentMediaRate(m)"
class="font-bold text-gray-700 dark:text-gray-300"
>{{ requestRate(m) }}x</span
>
@@ -313,6 +313,7 @@ import { platformAccentColor, platformBadgeLightClass, platformLabel } from '@/u
import {
BILLING_MODE_TOKEN,
BILLING_MODE_IMAGE,
BILLING_MODE_VIDEO,
REASONING_EFFORT_LEVELS,
type BillingMode
} from '@/constants/channel'
@@ -340,6 +341,9 @@ const props = defineProps<{
/** 生图独立倍率:true 时图片计费模型的实付倍率取 imageRateMultiplier,不取分组/专属倍率。 */
imageRateIndependent?: boolean
imageRateMultiplier?: number | null
/** 视频独立倍率开启时,覆盖分组/用户专属倍率,与后端视频计费保持一致。 */
videoRateIndependent?: boolean
videoRateMultiplier?: number | null
/**
* 高峰窗口描述(含倍率与服务器时区标注),空串/缺省 = 分组未启用高峰。
* 表格所有价格均为不含高峰因子的口径,该窗口仅用于分时时段行的 tooltip 披露:
@@ -425,17 +429,23 @@ function paidPerMillion(value: number | null | undefined, period: PlazaTimePrici
return formatScaled(value * rate, PER_MILLION, MIN_DECIMALS)
}
/** 图片计费模型且分组开启生图独立倍率:实付倍率取独立倍率,与计费口径一致。 */
function usesIndependentImageRate(m: PlazaModel): boolean {
return billingMode(m) === BILLING_MODE_IMAGE && props.imageRateIndependent === true
/** 图片和视频分别使用对应的独立倍率开关。 */
function usesIndependentMediaRate(m: PlazaModel): boolean {
return (billingMode(m) === BILLING_MODE_IMAGE && props.imageRateIndependent === true) ||
(billingMode(m) === BILLING_MODE_VIDEO && props.videoRateIndependent === true)
}
/** 按次/按图片行的生效倍率。 */
/** 非 token 行的生效倍率,图片和视频可分别覆盖通用倍率。 */
function requestRate(m: PlazaModel): number {
return usesIndependentImageRate(m) ? (props.imageRateMultiplier ?? 1) : effectiveRate.value
if (billingMode(m) === BILLING_MODE_VIDEO && props.videoRateIndependent === true) {
return Math.max(0, props.videoRateMultiplier ?? 1)
}
return billingMode(m) === BILLING_MODE_IMAGE && props.imageRateIndependent === true
? (props.imageRateMultiplier ?? 1)
: effectiveRate.value
}
/** 按次 / 按图片单价(乘该行生效倍率,不换算 1M)。 */
/** 非 token 单价乘该行生效倍率,不换算为每百万 token。 */
function paidRequestPrice(m: PlazaModel, value: number | null | undefined): string {
if (value == null) return '-'
return formatScaled(value * requestRate(m), 1, MIN_DECIMALS)
@@ -68,6 +68,8 @@ function group(overrides: Partial<ModelPlazaGroup> = {}): ModelPlazaGroup {
is_exclusive: false,
image_rate_independent: false,
image_rate_multiplier: 1,
video_rate_independent: false,
video_rate_multiplier: 1,
long_context_pricing_enabled: true,
models: [ladderModel(2)],
...overrides
@@ -90,6 +92,14 @@ function mountSection(g: ModelPlazaGroup) {
const NOTE = 'modelPlaza.detail.longContextDisabledNote'
describe('PlazaGroupSection 长上下文说明', () => {
it('passes video-specific rates to the pricing table', () => {
const wrapper = mountSection(group({ video_rate_independent: true, video_rate_multiplier: 0 }))
expect(wrapper.findComponent(PlazaModelPricingTable).props()).toMatchObject({
videoRateIndependent: true,
videoRateMultiplier: 0
})
wrapper.unmount()
})
it('分组关闭阶梯且组内有官方阶梯模型时显示说明', () => {
const wrapper = mountSection(group({ long_context_pricing_enabled: false }))
expect(wrapper.text()).toContain(NOTE)
@@ -46,6 +46,8 @@ function mountTable(
extraProps?: {
imageRateIndependent?: boolean
imageRateMultiplier?: number | null
videoRateIndependent?: boolean
videoRateMultiplier?: number | null
peakWindow?: string
peakRateMultiplier?: number | null
}
@@ -56,6 +58,49 @@ function mountTable(
}
describe('PlazaModelPricingTable', () => {
it.each([
{ enabled: true, multiplier: 1, userRate: 0.05, expected: 1 },
{ enabled: true, multiplier: 0.5, userRate: null, expected: 0.5 },
{ enabled: true, multiplier: 0, userRate: 0.05, expected: 0 },
{ enabled: true, multiplier: -1, userRate: null, expected: 0 },
{ enabled: false, multiplier: 1, userRate: 0.05, expected: 0.05 },
{ enabled: false, multiplier: 1, userRate: null, expected: 0.15 }
])('uses the video billing multiplier $expected when independent=$enabled', (tc) => {
const model = tokenModel({ name: 'video-test', official_pricing: null })
model.pricing!.billing_mode = 'video'
model.pricing!.per_request_price = 2
const wrapper = mountTable([model], 0.15, tc.userRate, {
imageRateIndependent: true,
imageRateMultiplier: 9,
videoRateIndependent: tc.enabled,
videoRateMultiplier: tc.multiplier
})
try {
expect(wrapper.text()).toContain(`$${(2 * tc.expected).toFixed(2)}`)
const rateCell = wrapper.findAll('tbody tr td').at(-1)!
if (tc.enabled) expect(rateCell.text()).toBe(`${tc.expected}x`)
else expect(rateCell.text()).toContain(`${tc.expected}x`)
} finally {
wrapper.unmount()
}
})
it('applies video independent rates to resolution tiers', () => {
const model = tokenModel({ name: 'video-tier-test', official_pricing: null })
model.pricing!.billing_mode = 'video'
model.pricing!.intervals = [{
min_tokens: 0, max_tokens: null, tier_label: '720p',
input_price: null, output_price: null, cache_write_price: null,
cache_read_price: null, per_request_price: 2
}]
const wrapper = mountTable([model], 0.15, 0.05, {
videoRateIndependent: true, videoRateMultiplier: 0.5
})
expect(wrapper.text()).toContain('720p')
expect(wrapper.text()).toContain('$1.00')
wrapper.unmount()
})
it('倍率为 1 时展示渠道单价原值($/1M),价格保底 2 位小数', () => {
const wrapper = mountTable([tokenModel()], 1)
const text = wrapper.text()