模型广场:分时计价同步渠道仅工作日规则

- 阶梯表分时倍率透传渠道 weekdays_only;探针锚点显式固定在工作日
  (原 2026-01-01 恰为周四是巧合,锚点落周末会把仅工作日时段整组剔除)
- 前端时段徽章加「工作日」前缀,tooltip 说明周末全天按标准价计费
This commit is contained in:
feeeei
2026-08-24 11:16:00 +08:00
parent 83d4eb6a43
commit b07d85c497
9 changed files with 109 additions and 9 deletions
@@ -54,9 +54,11 @@ type modelPlazaTimePricingPeriod struct {
}
// modelPlazaTimePricing 计费会生效的分时倍率(仅倍率 ≠ 1 的时段)。
// WeekdaysOnly 为 true 时时段仅周一至周五生效,周末整天按标准价计费。
type modelPlazaTimePricing struct {
Timezone string `json:"timezone"`
Periods []modelPlazaTimePricingPeriod `json:"periods"`
Timezone string `json:"timezone"`
WeekdaysOnly bool `json:"weekdays_only,omitempty"`
Periods []modelPlazaTimePricingPeriod `json:"periods"`
}
// modelPlazaModel 广场模型条目:实收口径展示定价(白名单形态)+ 官方参考价。
@@ -226,7 +228,7 @@ func toModelPlazaTimePricing(p *service.TimePricingSchedule) *modelPlazaTimePric
Multiplier: period.Multiplier,
})
}
return &modelPlazaTimePricing{Timezone: p.Timezone, Periods: periods}
return &modelPlazaTimePricing{Timezone: p.Timezone, WeekdaysOnly: p.WeekdaysOnly, Periods: periods}
}
// toModelPlazaOfficialPricing 转换官方参考价;nil 透传(前端显示 "-")。
@@ -192,6 +192,13 @@ func TestToModelPlazaGroupDTO_TimePricing(t *testing.T) {
TimePricing: &service.TimePricingSchedule{Timezone: "Asia/Shanghai", Periods: []service.TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
}},
}, {
Name: "deepseek-reasoner",
Platform: "deepseek",
Pricing: &service.ChannelModelPricing{BillingMode: service.BillingModeToken, InputPrice: testPtr(0.56e-6)},
TimePricing: &service.TimePricingSchedule{Timezone: "Asia/Shanghai", WeekdaysOnly: true, Periods: []service.TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
}},
}},
}
raw, err := json.Marshal(toModelPlazaGroupDTO(&g, nil))
@@ -201,10 +208,16 @@ func TestToModelPlazaGroupDTO_TimePricing(t *testing.T) {
model := decoded["models"].([]any)[0].(map[string]any)
tp := model["time_pricing"].(map[string]any)
require.Equal(t, "Asia/Shanghai", tp["timezone"])
_, hasWeekdaysOnly := tp["weekdays_only"]
require.False(t, hasWeekdaysOnly, "未开启仅工作日时字段省略")
periods := tp["periods"].([]any)
require.Len(t, periods, 1)
first := periods[0].(map[string]any)
require.Equal(t, "00:30", first["start_time"])
require.Equal(t, "08:30", first["end_time"])
require.InDelta(t, 0.5, first["multiplier"].(float64), 1e-12)
weekdaysModel := decoded["models"].([]any)[1].(map[string]any)
weekdaysTP := weekdaysModel["time_pricing"].(map[string]any)
require.Equal(t, true, weekdaysTP["weekdays_only"])
}
@@ -39,9 +39,11 @@ type TimePricingPeriod struct {
}
// TimePricingSchedule 分组+模型生效的分时倍率(仅列出倍率 ≠ 1 的时段,按开始时间升序)。
// WeekdaysOnly 为 true 时时段仅周一至周五生效,周末整天按标准价计费。
type TimePricingSchedule struct {
Timezone string
Periods []TimePricingPeriod
Timezone string
WeekdaysOnly bool
Periods []TimePricingPeriod
}
// ContextPricingSchedule 分组+模型按上下文长度分档的有效单价表。
@@ -168,8 +170,10 @@ func resolvedTimePricingSchedule(resolved *ResolvedPricing) *TimePricingSchedule
if err != nil {
continue
}
// 时段按每日循环,取任意一天该时段开始后 1 秒作为探针时刻。
at := time.Date(2026, time.January, 1, 0, 0, start+1, 0, location)
// 时段按每日循环,取该时段开始后 1 秒作为探针时刻。
// 锚点日必须是工作日(2026-01-05 为周一):weekdays_only 配置在周末恒为 1,
// 锚点落在周末会把时段整组剔除。
at := time.Date(2026, time.January, 5, 0, 0, start+1, 0, location)
multiplier := resolvedChannelTimeMultiplier(resolved, at)
if multiplier == 1 {
continue
@@ -184,7 +188,11 @@ func resolvedTimePricingSchedule(resolved *ResolvedPricing) *TimePricingSchedule
return nil
}
sort.SliceStable(probed, func(i, j int) bool { return probed[i].start < probed[j].start })
out := &TimePricingSchedule{Timezone: cfg.Timezone, Periods: make([]TimePricingPeriod, 0, len(probed))}
out := &TimePricingSchedule{
Timezone: cfg.Timezone,
WeekdaysOnly: cfg.WeekdaysOnly,
Periods: make([]TimePricingPeriod, 0, len(probed)),
}
for _, p := range probed {
out.Periods = append(out.Periods, p.period)
}
@@ -515,6 +515,7 @@ func TestResolveContextPricingSchedule_TimePricing(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, sched.TimePricing)
require.Equal(t, "Asia/Shanghai", sched.TimePricing.Timezone)
require.False(t, sched.TimePricing.WeekdaysOnly)
require.Equal(t, []TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
{StartTime: "18:00", EndTime: "22:00:00", Multiplier: 1.2},
@@ -546,6 +547,43 @@ func TestResolveContextPricingSchedule_TimePricing(t *testing.T) {
}
})
t.Run("仅工作日配置透传标注且时段仍列出", func(t *testing.T) {
weekdays := &ChannelTimePricing{Timezone: "Asia/Shanghai", WeekdaysOnly: true, Periods: []ChannelTimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
}}
bs, resolver := newTokenCostTestEnv(t, PlatformAnthropic, sonnetChannelWithTimePricing(weekdays), nil)
sched, err := bs.ResolveContextPricingSchedule(context.Background(), resolver, ContextPricingScheduleInput{
Model: "claude-sonnet-4", Group: enabledGroup(PlatformAnthropic), Platform: PlatformAnthropic,
})
require.NoError(t, err)
require.NotNil(t, sched.TimePricing, "探针锚点必须落在工作日,否则仅工作日配置的时段会被整组剔除")
require.True(t, sched.TimePricing.WeekdaysOnly)
require.Equal(t, []TimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
}, sched.TimePricing.Periods)
// 对账:工作日时段内乘倍率,周末同一时段按标准价
group := enabledGroup(PlatformAnthropic)
gid := group.ID
resolved := resolver.Resolve(context.Background(), PricingInput{Model: "claude-sonnet-4", GroupID: &gid, Group: group})
loc, err := time.LoadLocation("Asia/Shanghai")
require.NoError(t, err)
for _, tc := range []struct {
at time.Time
want float64
}{
{time.Date(2026, 8, 24, 3, 0, 0, 0, loc), 0.5}, // 周一
{time.Date(2026, 8, 23, 3, 0, 0, 0, loc), 1}, // 周日
} {
cost, err := bs.CalculateTokenCostForRequest(TokenCostRequest{
Ctx: context.Background(), Model: "claude-sonnet-4", Group: group, Tokens: UsageTokens{InputTokens: 1000},
RateMultiplier: 1, PricingAt: tc.at, Resolver: resolver, Resolved: resolved,
})
require.NoError(t, err)
assertCostClose(t, 1000*2e-6*tc.want, cost.ActualCost, "at %s", tc.at)
}
})
t.Run("配置非法时计费按 1 计,阶梯表不列分时", func(t *testing.T) {
invalid := &ChannelTimePricing{Timezone: "Asia/Shanghai", Periods: []ChannelTimePricingPeriod{
{StartTime: "00:30", EndTime: "08:30", Multiplier: 0.5},
+2
View File
@@ -38,6 +38,8 @@ export interface PlazaTimePricingPeriod {
export interface PlazaTimePricing {
/** IANA 时区名,如 Asia/Shanghai。 */
timezone: string
/** true 时时段仅周一至周五生效,周末整天按标准价计费。 */
weekdays_only?: boolean
periods: PlazaTimePricingPeriod[]
}
@@ -70,8 +70,11 @@
<span
v-if="period"
class="inline-flex items-center whitespace-nowrap rounded-md bg-gray-100 px-1 py-0.5 font-mono text-[10px] font-medium text-gray-500 dark:bg-dark-700/70 dark:text-dark-300"
:title="t('modelPlaza.table.timePricingRowHint', { timezone: m.time_pricing?.timezone })"
:title="timePricingRowHint(m)"
>
<span v-if="m.time_pricing?.weekdays_only" class="mr-1 font-sans">{{
t('modelPlaza.table.timePricingWeekdays')
}}</span>
{{ formatTimeWindow(period) }}
</span>
<span
@@ -425,6 +428,14 @@ function timePeriods(m: PlazaModel): PlazaTimePricingPeriod[] {
return m.time_pricing?.periods ?? []
}
/** 时段行 tooltip:仅工作日生效的配置换用带周末回落说明的文案。 */
function timePricingRowHint(m: PlazaModel): string {
const key = m.time_pricing?.weekdays_only
? 'modelPlaza.table.timePricingRowHintWeekdays'
: 'modelPlaza.table.timePricingRowHint'
return t(key, { timezone: m.time_pricing?.timezone })
}
/** “00:30–08:30”;整分钟的 HH:mm:ss 省略秒。 */
function formatTimeWindow(p: PlazaTimePricingPeriod): string {
const clock = (v: string) => v.replace(/^(\d{2}:\d{2}):00$/, '$1')
@@ -582,6 +582,26 @@ describe('PlazaModelPricingTable 分时计价', () => {
expect(nightCells[4].text()).toContain('$3.00')
})
it('仅工作日生效时时段行带工作日前缀,tooltip 换用周末回落文案', () => {
const model = timePricedModel()
model.time_pricing!.weekdays_only = true
const wrapper = mountTable([model], 1)
const trs = wrapper.findAll('tbody tr')
expect(trs).toHaveLength(3)
const nightCells = trs[1].findAll('td')
expect(nightCells[0].text()).toContain('modelPlaza.table.timePricingWeekdays')
expect(nightCells[0].text()).toContain('00:30–08:30')
expect(nightCells[0].find('[title="modelPlaza.table.timePricingRowHintWeekdays"]').exists()).toBe(true)
expect(nightCells[0].find('[title="modelPlaza.table.timePricingRowHint"]').exists()).toBe(false)
})
it('每日生效(无 weekdays_only)不渲染工作日前缀', () => {
const wrapper = mountTable([timePricedModel()], 1)
expect(wrapper.find('tbody').text()).not.toContain('modelPlaza.table.timePricingWeekdays')
expect(wrapper.find('[title="modelPlaza.table.timePricingRowHint"]').exists()).toBe(true)
})
it('无分时倍率时只有一行,不渲染时段标注', () => {
const wrapper = mountTable([tokenModel()], 1)
expect(wrapper.findAll('tbody tr')).toHaveLength(1)
@@ -605,6 +605,9 @@ export default {
tierHintMarginal: 'Only the portion above the threshold is billed at this tier; output is unaffected',
marginalBadge: 'excess-only tiers',
timePricingRowHint: 'Requests made within this period ({timezone} time) are billed at the prices in this row',
timePricingRowHintWeekdays:
'On weekdays (Mon–Fri) only, requests made within this period ({timezone} time) are billed at the prices in this row; weekends use the standard prices',
timePricingWeekdays: 'Weekdays',
timePricingRateHint: 'Effective rate {rate} × period multiplier {multiplier}',
paidPrice: 'Your Price (Discounted)',
officialPrice: 'Official Price',
@@ -610,6 +610,9 @@ export default {
tierHintMarginal: '仅超过阈值的部分按该档计价,输出不加价',
marginalBadge: '超出部分计价',
timePricingRowHint: '按 {timezone} 时间,在该时段内发起的请求按本行价格计费',
timePricingRowHintWeekdays:
'按 {timezone} 时间,仅工作日(周一至周五)在该时段内发起的请求按本行价格计费,周末全天按标准价',
timePricingWeekdays: '工作日',
timePricingRateHint: '生效倍率 {rate} × 时段倍率 {multiplier}',
paidPrice: '实付价格(折后)',
officialPrice: '官方价格',