| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- package service
- import (
- "context"
- "database/sql"
- "testing"
- "github.com/stretchr/testify/require"
- )
- type stubGlobalPricingRepo struct {
- rows []GlobalModelPrice
- }
- func (s *stubGlobalPricingRepo) ListEnabled(context.Context) ([]GlobalModelPrice, error) {
- return append([]GlobalModelPrice(nil), s.rows...), nil
- }
- func (s *stubGlobalPricingRepo) ListAll(context.Context) ([]GlobalModelPrice, error) {
- return append([]GlobalModelPrice(nil), s.rows...), nil
- }
- func (s *stubGlobalPricingRepo) Create(_ context.Context, in GlobalModelPriceInput) (*GlobalModelPrice, error) {
- out := GlobalModelPrice{ID: 1, ModelPattern: in.ModelPattern, BillingMode: in.BillingMode,
- InputPrice: in.InputPrice, OutputPrice: in.OutputPrice, Enabled: in.Enabled}
- s.rows = append(s.rows, out)
- return &out, nil
- }
- func (s *stubGlobalPricingRepo) GetByID(_ context.Context, id int64) (*GlobalModelPrice, error) {
- for i := range s.rows {
- if s.rows[i].ID == id {
- cp := s.rows[i]
- return &cp, nil
- }
- }
- return nil, sql.ErrNoRows
- }
- func (s *stubGlobalPricingRepo) Update(_ context.Context, id int64, in GlobalModelPriceInput) (*GlobalModelPrice, error) {
- return &GlobalModelPrice{ID: id, ModelPattern: in.ModelPattern}, nil
- }
- func (s *stubGlobalPricingRepo) Delete(context.Context, int64) error { return nil }
- func (s *stubGlobalPricingRepo) SetEnabled(_ context.Context, id int64, enabled bool) error {
- for i := range s.rows {
- if s.rows[i].ID == id {
- s.rows[i].Enabled = enabled
- return nil
- }
- }
- return sql.ErrNoRows
- }
- func fptr(v float64) *float64 { return &v }
- func TestValidateGlobalModelPriceInput(t *testing.T) {
- valid := GlobalModelPriceInput{ModelPattern: "gpt-5", InputPrice: fptr(1), OutputPrice: fptr(2)}
- require.NoError(t, ValidateGlobalModelPriceInput(valid))
- require.NoError(t, ValidateGlobalModelPriceInput(GlobalModelPriceInput{ModelPattern: "gpt-5*", OutputPrice: fptr(2)}))
- require.NoError(t, ValidateGlobalModelPriceInput(GlobalModelPriceInput{
- ModelPattern: "x", BillingMode: BillingModePerRequest, PerRequestPrice: fptr(0.01)}))
- bad := []GlobalModelPriceInput{
- {ModelPattern: ""},
- {ModelPattern: "*"},
- {ModelPattern: "*foo"},
- {ModelPattern: "a*b"},
- {ModelPattern: "has space"},
- {ModelPattern: "x", BillingMode: "hourly"},
- {ModelPattern: "x", InputPrice: fptr(-1)},
- {ModelPattern: "x"},
- {ModelPattern: "x", BillingMode: BillingModePerRequest},
- {ModelPattern: "x", BillingMode: BillingModeVideo, InputPrice: fptr(1)},
- }
- for i, in := range bad {
- require.Error(t, ValidateGlobalModelPriceInput(in), "case %d", i)
- }
- }
- func TestGlobalModelPricingMatch(t *testing.T) {
- svc := NewGlobalModelPricingService(&stubGlobalPricingRepo{rows: []GlobalModelPrice{
- {ID: 1, ModelPattern: "gpt-5", BillingMode: BillingModeToken, InputPrice: fptr(1), Enabled: true},
- {ID: 2, ModelPattern: "gpt-*", BillingMode: BillingModeToken, InputPrice: fptr(5), Enabled: true},
- {ID: 3, ModelPattern: "gpt-5-*", BillingMode: BillingModeToken, InputPrice: fptr(7), Enabled: true},
- }})
- // 精确优先于通配
- hit := svc.Match(context.Background(), "GPT-5")
- require.NotNil(t, hit)
- require.Equal(t, 1.0, *hit.InputPrice)
- // 最长前缀通配
- hit = svc.Match(context.Background(), "gpt-5-turbo")
- require.NotNil(t, hit)
- require.Equal(t, 7.0, *hit.InputPrice)
- hit = svc.Match(context.Background(), "gpt-4o")
- require.NotNil(t, hit)
- require.Equal(t, 5.0, *hit.InputPrice)
- // 未命中
- require.Nil(t, svc.Match(context.Background(), "claude-sonnet-4"))
- // nil 服务恒未命中
- var nilSvc *GlobalModelPricingService
- require.Nil(t, nilSvc.Match(context.Background(), "gpt-5"))
- }
- func TestModelPricingResolverGlobalFirst(t *testing.T) {
- repo := &stubGlobalPricingRepo{rows: []GlobalModelPrice{
- {ID: 1, ModelPattern: "gpt-5", BillingMode: BillingModeToken,
- InputPrice: fptr(10), OutputPrice: fptr(20), Enabled: true},
- }}
- global := NewGlobalModelPricingService(repo)
- r := NewModelPricingResolverWithGlobal(nil, &BillingService{}, global)
- resolved := r.Resolve(context.Background(), PricingInput{
- Model: "gpt-5",
- Group: &Group{ID: 9, ModelPricing: []ChannelModelPricing{{
- Models: []string{"gpt-5"}, InputPrice: fptr(1), OutputPrice: fptr(1),
- }}},
- })
- require.Equal(t, PricingSourceGlobal, resolved.Source)
- require.NotNil(t, resolved.BasePricing)
- require.Equal(t, 10.0, resolved.BasePricing.InputPricePerToken)
- require.Equal(t, 20.0, resolved.BasePricing.OutputPricePerToken)
- }
- func TestModelPricingResolverWithoutGlobalKeepsLegacy(t *testing.T) {
- r := NewModelPricingResolver(nil, &BillingService{})
- resolved := r.Resolve(context.Background(), PricingInput{Model: "gpt-5"})
- require.NotEqual(t, PricingSourceGlobal, resolved.Source)
- }
|