diff --git a/backend/internal/service/channel_service.go b/backend/internal/service/channel_service.go index a3a14893a..934f06d97 100644 --- a/backend/internal/service/channel_service.go +++ b/backend/internal/service/channel_service.go @@ -60,6 +60,16 @@ type channelModelKey struct { model string // lowercase } +// normalizeChannelPricingModelName makes Anthropic's dot and hyphen spelling +// differences equivalent in channel pricing cache keys. +func normalizeChannelPricingModelName(model string) string { + model = strings.ToLower(strings.TrimSpace(model)) + if strings.HasPrefix(model, "claude-") { + model = strings.ReplaceAll(model, ".", "-") + } + return model +} + // channelGroupPlatformKey 通配符定价缓存键 type channelGroupPlatformKey struct { groupID int64 @@ -217,13 +227,13 @@ func expandPricingToCache(cache *channelCache, ch *Channel, gid int64, platform gpKey := channelGroupPlatformKey{groupID: gid, platform: pricingPlatform} for _, model := range pricing.Models { if strings.HasSuffix(model, "*") { - prefix := strings.ToLower(strings.TrimSuffix(model, "*")) + prefix := normalizeChannelPricingModelName(strings.TrimSuffix(model, "*")) cache.wildcardByGroupPlatform[gpKey] = append(cache.wildcardByGroupPlatform[gpKey], &wildcardPricingEntry{ prefix: prefix, pricing: pricing, }) } else { - key := channelModelKey{groupID: gid, platform: pricingPlatform, model: strings.ToLower(model)} + key := channelModelKey{groupID: gid, platform: pricingPlatform, model: normalizeChannelPricingModelName(model)} cache.pricingByGroupModel[key] = pricing } } @@ -402,6 +412,7 @@ func (c *channelCache) matchWildcardMapping(groupID int64, platform, modelLower // lookupPricingAcrossPlatforms 在分组平台内查找模型定价。 // 各平台严格独立,只在本平台内查找(先精确匹配,再通配符)。 func lookupPricingAcrossPlatforms(cache *channelCache, groupID int64, groupPlatform, modelLower string) *ChannelModelPricing { + modelLower = normalizeChannelPricingModelName(modelLower) for _, p := range matchingPlatforms(groupPlatform) { key := channelModelKey{groupID: groupID, platform: p, model: modelLower} if pricing, ok := cache.pricingByGroupModel[key]; ok { diff --git a/backend/internal/service/channel_service_test.go b/backend/internal/service/channel_service_test.go index 035b941db..a89e5063a 100644 --- a/backend/internal/service/channel_service_test.go +++ b/backend/internal/service/channel_service_test.go @@ -675,6 +675,25 @@ func TestGetChannelModelPricing_CaseInsensitive(t *testing.T) { require.Equal(t, int64(100), result.ID) } +func TestGetChannelModelPricing_NormalizesDotsAndHyphens(t *testing.T) { + ch := Channel{ + ID: 1, + Status: StatusActive, + GroupIDs: []int64{10}, + ModelPricing: []ChannelModelPricing{ + {ID: 100, Platform: "anthropic", Models: []string{"claude-opus-4.8"}, BillingMode: BillingModePerRequest, PerRequestPrice: testPtrFloat64(0.007)}, + }, + } + repo := makeStandardRepo(ch, map[int64]string{10: "anthropic"}) + svc := newTestChannelService(repo) + + result := svc.GetChannelModelPricing(context.Background(), 10, "claude-opus-4-8") + require.NotNil(t, result) + require.Equal(t, int64(100), result.ID) + require.Equal(t, BillingModePerRequest, result.BillingMode) + require.InDelta(t, 0.007, *result.PerRequestPrice, 1e-12) +} + func TestGetChannelModelPricing_WildcardMatch(t *testing.T) { ch := Channel{ ID: 1,