mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #7524 from Achordchan/fix/plaza-video-rate
fix(model-plaza): apply independent video billing multipliers
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user