mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 22:31:42 +08:00
Merge pull request #3401 from StarryKira/fix/issue-3394-fallback-pricing-log-spam
fix: stop per-request fallback-pricing log spam for unknown models (#3394)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
@@ -168,6 +169,11 @@ type BillingService struct {
|
||||
cfg *config.Config
|
||||
pricingService *PricingService
|
||||
fallbackPrices map[string]*ModelPricing // 硬编码回退价格
|
||||
|
||||
// fallbackWarnSeen 记录已打过 fallback 警告日志的(已小写化)模型名,
|
||||
// 让 "[Billing] Using fallback pricing" 每个模型每进程最多打一条,
|
||||
// 避免热路径上每请求刷屏(issue #3394)。零值即可用,无需在构造函数初始化。
|
||||
fallbackWarnSeen sync.Map
|
||||
}
|
||||
|
||||
// NewBillingService 创建计费服务实例
|
||||
@@ -722,7 +728,11 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) {
|
||||
// 2. 使用硬编码回退价格
|
||||
fallback := s.getFallbackPricing(model)
|
||||
if fallback != nil {
|
||||
log.Printf("[Billing] Using fallback pricing for model: %s", model)
|
||||
// 按模型名去重:每个模型每进程最多打一条 warn,避免热路径每请求刷屏(issue #3394)。
|
||||
// model 在函数入口已 ToLower,故 GLM-5.2 / glm-5.2 视为同一条目。
|
||||
if _, seen := s.fallbackWarnSeen.LoadOrStore(model, struct{}{}); !seen {
|
||||
log.Printf("[Billing] Using fallback pricing for model: %s", model)
|
||||
}
|
||||
return s.applyModelSpecificPricingPolicy(model, fallback), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// captureStdLog 重定向 stdlib log 输出到 buffer,返回该 buffer;通过 t.Cleanup 还原。
|
||||
// 用于断言 GetModelPricing 的 fallback warn(log.Printf)打了几次。
|
||||
func captureStdLog(t *testing.T) *bytes.Buffer {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
prevOut := log.Writer()
|
||||
prevFlags := log.Flags()
|
||||
log.SetOutput(&buf)
|
||||
log.SetFlags(0)
|
||||
t.Cleanup(func() {
|
||||
log.SetOutput(prevOut)
|
||||
log.SetFlags(prevFlags)
|
||||
})
|
||||
return &buf
|
||||
}
|
||||
|
||||
func newTestBillingService() *BillingService {
|
||||
return NewBillingService(&config.Config{}, nil)
|
||||
}
|
||||
@@ -105,6 +124,53 @@ func TestGetModelPricing_CaseInsensitive(t *testing.T) {
|
||||
require.Equal(t, p1.InputPricePerToken, p2.InputPricePerToken)
|
||||
}
|
||||
|
||||
// issue #3394: fallback warn 应按模型名去重,每个模型每进程最多打一条,
|
||||
// 避免热路径每请求刷屏 ops_system_logs。
|
||||
func TestGetModelPricing_FallbackWarnLoggedOncePerModel(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
buf := captureStdLog(t)
|
||||
|
||||
// glm-5.2 不在 LiteLLM,经 strings.Contains 命中 glm-5 兜底价 → 触发 fallback warn。
|
||||
for i := 0; i < 5; i++ {
|
||||
pricing, err := svc.GetModelPricing("glm-5.2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pricing)
|
||||
}
|
||||
|
||||
got := strings.Count(buf.String(), "Using fallback pricing for model: glm-5.2")
|
||||
require.Equal(t, 1, got, "同一模型的 fallback warn 应只打一条,实际日志:\n%s", buf.String())
|
||||
}
|
||||
|
||||
// 去重按"每模型"而非全局:不同模型各打一条;大小写变体经入口 ToLower 归一,视为同一条目。
|
||||
func TestGetModelPricing_FallbackWarnPerModelNotGlobal(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
buf := captureStdLog(t)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
_, _ = svc.GetModelPricing("glm-5.2")
|
||||
_, _ = svc.GetModelPricing("GLM-5.2") // 与上一行同模型(ToLower 后),去重后不再打
|
||||
_, _ = svc.GetModelPricing("glm-4.6")
|
||||
}
|
||||
|
||||
out := buf.String()
|
||||
require.Equal(t, 1, strings.Count(out, "model: glm-5.2"), out)
|
||||
require.Equal(t, 1, strings.Count(out, "model: glm-4.6"), out)
|
||||
require.Equal(t, 0, strings.Count(out, "model: GLM-5.2"), out) // 大写经 ToLower 归一,不应单独成行
|
||||
}
|
||||
|
||||
// 回归:glm-5.2 仍解析到 glm-5 兜底价(计费金额不变,防止日志改动掩盖未来计费回归)。
|
||||
func TestGetModelPricing_GLM52FallsBackToGLM5Price(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
got, err := svc.GetModelPricing("glm-5.2")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
|
||||
// glm-5 base:Input 1e-6 / Output 3.2e-6(见 TestGetFallbackPricing_FamilyMatching)。
|
||||
require.InDelta(t, 1e-6, got.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 3.2e-6, got.OutputPricePerToken, 1e-12)
|
||||
}
|
||||
|
||||
func TestGetModelPricing_UnknownClaudeModelFallsBackToSonnet(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
|
||||
@@ -983,7 +983,8 @@ func detectConflicts(entries []modelEntry, platform, errCode, label string) erro
|
||||
for j := i + 1; j < len(entries); j++ {
|
||||
if conflictsBetween(entries[i], entries[j]) {
|
||||
return infraerrors.BadRequest(errCode,
|
||||
fmt.Sprintf("%s '%s' and '%s' conflict in platform '%s': overlapping match range",
|
||||
fmt.Sprintf("%s '%s' and '%s' conflict in platform '%s': overlapping match range "+
|
||||
"(model names are matched case-insensitively, so an existing entry already covers all case variants)",
|
||||
label, entries[i].pattern, entries[j].pattern, platform))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2408,8 +2408,8 @@ export default {
|
||||
deleteError: 'Failed to delete channel',
|
||||
nameRequired: 'Please enter a channel name',
|
||||
duplicateModels: 'Model "{0}" appears in multiple pricing entries',
|
||||
modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range",
|
||||
mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range",
|
||||
modelConflict: "Model patterns '{model1}' and '{model2}' conflict: overlapping match range. Model names are matched case-insensitively, so an existing entry already covers all case variants — no need to add the variant separately.",
|
||||
mappingConflict: "Mapping source patterns '{model1}' and '{model2}' conflict: overlapping match range. Source patterns are matched case-insensitively, so an existing entry already covers all case variants.",
|
||||
deleteConfirm: 'Are you sure you want to delete channel "{name}"? This cannot be undone.',
|
||||
columns: {
|
||||
name: 'Name',
|
||||
|
||||
@@ -2484,8 +2484,8 @@ export default {
|
||||
deleteError: '删除渠道失败',
|
||||
nameRequired: '请输入渠道名称',
|
||||
duplicateModels: '模型「{0}」在多个定价条目中重复',
|
||||
modelConflict: "模型模式 '{model1}' 和 '{model2}' 冲突:匹配范围重叠",
|
||||
mappingConflict: "模型映射源 '{model1}' 和 '{model2}' 冲突:匹配范围重叠",
|
||||
modelConflict: "模型模式 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。模型名称按大小写不敏感匹配,已有条目已覆盖其所有大小写变体,无需重复添加。",
|
||||
mappingConflict: "模型映射源 '{model1}' 和 '{model2}' 冲突:匹配范围重叠。源模式按大小写不敏感匹配,已有条目已覆盖其所有大小写变体。",
|
||||
deleteConfirm: '确定要删除渠道「{name}」吗?此操作不可撤销。',
|
||||
columns: {
|
||||
name: '名称',
|
||||
|
||||
Reference in New Issue
Block a user