feat(channel): 模型标签输入 + $/MTok 价格单位 + 左开右闭区间 + i18n

- 模型输入改为标签列表(输入回车添加,支持粘贴批量导入)
- 价格显示单位改为 $/MTok(每百万 token),提交时自动转换
- Token 模式增加图片输出价格字段(适配 Gemini 图片模型按 token 计费)
- 区间边界改为左开右闭 (min, max],右边界包含
- 默认价格作为未命中区间时的回退价格
- 添加完整中英文 i18n 翻译
This commit is contained in:
erio
2026-03-30 02:24:54 +08:00
parent 983fe58959
commit dca0054e93
9 changed files with 375 additions and 224 deletions

View File

@@ -110,11 +110,12 @@ func (c *Channel) GetModelPricing(model string) *ChannelModelPricing {
}
// FindMatchingInterval 在区间列表中查找匹配 totalTokens 的区间。
// 通用辅助函数,供 GetIntervalForContext、ModelPricingResolver 等复用
// 区间为左开右闭 (min, max]min 不含max 包含
// 第一个区间 min=0 时0 token 不匹配任何区间(回退到默认价格)。
func FindMatchingInterval(intervals []PricingInterval, totalTokens int) *PricingInterval {
for i := range intervals {
iv := &intervals[i]
if totalTokens >= iv.MinTokens && (iv.MaxTokens == nil || totalTokens < *iv.MaxTokens) {
if totalTokens > iv.MinTokens && (iv.MaxTokens == nil || totalTokens <= *iv.MaxTokens) {
return iv
}
}

View File

@@ -87,10 +87,13 @@ func TestGetIntervalForContext(t *testing.T) {
wantNil bool
}{
{"first interval", 50000, channelTestPtrFloat64(1e-6), false},
{"boundary: at min of second", 128000, channelTestPtrFloat64(2e-6), false},
{"boundary: at max of first (exclusive)", 128000, channelTestPtrFloat64(2e-6), false},
// (min, max] — 128000 在第一个区间的 max包含所以匹配第一个
{"boundary: max of first (inclusive)", 128000, channelTestPtrFloat64(1e-6), false},
// 128001 > 128000匹配第二个区间
{"boundary: just above first max", 128001, channelTestPtrFloat64(2e-6), false},
{"unbounded interval", 500000, channelTestPtrFloat64(2e-6), false},
{"zero tokens", 0, channelTestPtrFloat64(1e-6), false},
// (0, max] — 0 不匹配任何区间(左开)
{"zero tokens: no match", 0, nil, true},
}
for _, tt := range tests {
@@ -112,8 +115,10 @@ func TestGetIntervalForContext_NoMatch(t *testing.T) {
{MinTokens: 10000, MaxTokens: channelTestPtrInt(50000)},
},
}
require.Nil(t, p.GetIntervalForContext(5000))
require.Nil(t, p.GetIntervalForContext(50000))
require.Nil(t, p.GetIntervalForContext(5000)) // 5000 <= 10000, not > min
require.Nil(t, p.GetIntervalForContext(10000)) // 10000 not > 10000 (left-open)
require.NotNil(t, p.GetIntervalForContext(50000)) // 50000 <= 50000 (right-closed)
require.Nil(t, p.GetIntervalForContext(50001)) // 50001 > 50000
}
func TestGetIntervalForContext_Empty(t *testing.T) {