diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 388cab606c..3c84890c5b 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -98,6 +98,7 @@ const AntigravityGemini31ProAgentModel = "gemini-pro-agent" // 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致 var DefaultAntigravityModelMapping = map[string]string{ // Claude 白名单 + "claude-fable-5-1": "claude-fable-5-1", // 官方模型 "claude-fable-5": "claude-fable-5", // 官方模型 "claude-opus-4-8": "claude-opus-4-8", // 官方模型 "claude-opus-4-7": "claude-opus-4-7", // 官方模型 @@ -158,7 +159,8 @@ var DefaultAntigravityModelMapping = map[string]string{ // aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等) var DefaultBedrockModelMapping = map[string]string{ // Claude Fable - "claude-fable-5": "anthropic.claude-fable-5", + "claude-fable-5-1": "anthropic.claude-fable-5-1", + "claude-fable-5": "anthropic.claude-fable-5", // Claude Opus "claude-opus-5": "us.anthropic.claude-opus-5-v1", "claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1", diff --git a/backend/internal/domain/constants_test.go b/backend/internal/domain/constants_test.go index acac038ef2..628489a620 100644 --- a/backend/internal/domain/constants_test.go +++ b/backend/internal/domain/constants_test.go @@ -29,8 +29,9 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) { t.Parallel() cases := map[string]string{ - "claude-fable-5": "claude-fable-5", - "claude-opus-4-8": "claude-opus-4-8", + "claude-fable-5-1": "claude-fable-5-1", + "claude-fable-5": "claude-fable-5", + "claude-opus-4-8": "claude-opus-4-8", } for from, want := range cases { got, ok := DefaultAntigravityModelMapping[from] @@ -92,8 +93,9 @@ func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) { t.Parallel() cases := map[string]string{ - "claude-fable-5": "anthropic.claude-fable-5", - "claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1", + "claude-fable-5-1": "anthropic.claude-fable-5-1", + "claude-fable-5": "anthropic.claude-fable-5", + "claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1", } for from, want := range cases { got, ok := DefaultBedrockModelMapping[from] diff --git a/backend/internal/handler/admin/channel_handler.go b/backend/internal/handler/admin/channel_handler.go index c1b3507aef..9723e3595b 100644 --- a/backend/internal/handler/admin/channel_handler.go +++ b/backend/internal/handler/admin/channel_handler.go @@ -57,20 +57,21 @@ type updateChannelRequest struct { } type channelModelPricingRequest struct { - Platform string `json:"platform" binding:"omitempty,max=50"` - Models []string `json:"models" binding:"required,min=1,max=100"` - BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"` - InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"` - OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"` - CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"` - CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"` - FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"` - FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"` - ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"` - ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"` - PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"` - Intervals []pricingIntervalRequest `json:"intervals"` - TimePricing *channelTimePricingRequest `json:"time_pricing"` + Platform string `json:"platform" binding:"omitempty,max=50"` + Models []string `json:"models" binding:"required,min=1,max=100"` + BillingMode string `json:"billing_mode" binding:"omitempty,oneof=token per_request image"` + InputPrice *float64 `json:"input_price" binding:"omitempty,min=0"` + OutputPrice *float64 `json:"output_price" binding:"omitempty,min=0"` + CacheWritePrice *float64 `json:"cache_write_price" binding:"omitempty,min=0"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price" binding:"omitempty,min=0"` + CacheReadPrice *float64 `json:"cache_read_price" binding:"omitempty,min=0"` + FastMultiplier *float64 `json:"fast_multiplier" binding:"omitempty,gt=0"` + FlexMultiplier *float64 `json:"flex_multiplier" binding:"omitempty,gt=0"` + ImageInputPrice *float64 `json:"image_input_price" binding:"omitempty,min=0"` + ImageOutputPrice *float64 `json:"image_output_price" binding:"omitempty,min=0"` + PerRequestPrice *float64 `json:"per_request_price" binding:"omitempty,min=0"` + Intervals []pricingIntervalRequest `json:"intervals"` + TimePricing *channelTimePricingRequest `json:"time_pricing"` } type channelTimePricingRequest struct { @@ -92,6 +93,7 @@ type pricingIntervalRequest struct { InputPrice *float64 `json:"input_price"` OutputPrice *float64 `json:"output_price"` CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` CacheReadPrice *float64 `json:"cache_read_price"` InputMultiplier *float64 `json:"input_multiplier" binding:"omitempty,gt=0"` OutputMultiplier *float64 `json:"output_multiplier" binding:"omitempty,gt=0"` @@ -127,21 +129,22 @@ type channelResponse struct { } type channelModelPricingResponse struct { - ID int64 `json:"id"` - Platform string `json:"platform"` - Models []string `json:"models"` - BillingMode string `json:"billing_mode"` - InputPrice *float64 `json:"input_price"` - OutputPrice *float64 `json:"output_price"` - CacheWritePrice *float64 `json:"cache_write_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - FastMultiplier *float64 `json:"fast_multiplier"` - FlexMultiplier *float64 `json:"flex_multiplier"` - ImageInputPrice *float64 `json:"image_input_price"` - ImageOutputPrice *float64 `json:"image_output_price"` - PerRequestPrice *float64 `json:"per_request_price"` - Intervals []pricingIntervalResponse `json:"intervals"` - TimePricing *channelTimePricingResponse `json:"time_pricing"` + ID int64 `json:"id"` + Platform string `json:"platform"` + Models []string `json:"models"` + BillingMode string `json:"billing_mode"` + InputPrice *float64 `json:"input_price"` + OutputPrice *float64 `json:"output_price"` + CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + CacheReadPrice *float64 `json:"cache_read_price"` + FastMultiplier *float64 `json:"fast_multiplier"` + FlexMultiplier *float64 `json:"flex_multiplier"` + ImageInputPrice *float64 `json:"image_input_price"` + ImageOutputPrice *float64 `json:"image_output_price"` + PerRequestPrice *float64 `json:"per_request_price"` + Intervals []pricingIntervalResponse `json:"intervals"` + TimePricing *channelTimePricingResponse `json:"time_pricing"` } type channelTimePricingResponse struct { @@ -164,6 +167,7 @@ type pricingIntervalResponse struct { InputPrice *float64 `json:"input_price"` OutputPrice *float64 `json:"output_price"` CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` CacheReadPrice *float64 `json:"cache_read_price"` InputMultiplier *float64 `json:"input_multiplier"` OutputMultiplier *float64 `json:"output_multiplier"` @@ -254,21 +258,22 @@ func pricingToResponse(p *service.ChannelModelPricing) channelModelPricingRespon intervals = append(intervals, intervalToResponse(iv)) } return channelModelPricingResponse{ - ID: p.ID, - Platform: platform, - Models: models, - BillingMode: billingMode, - InputPrice: p.InputPrice, - OutputPrice: p.OutputPrice, - CacheWritePrice: p.CacheWritePrice, - CacheReadPrice: p.CacheReadPrice, - FastMultiplier: p.FastMultiplier, - FlexMultiplier: p.FlexMultiplier, - ImageInputPrice: p.ImageInputPrice, - ImageOutputPrice: p.ImageOutputPrice, - PerRequestPrice: p.PerRequestPrice, - Intervals: intervals, - TimePricing: timePricingToResponse(p.TimePricing), + ID: p.ID, + Platform: platform, + Models: models, + BillingMode: billingMode, + InputPrice: p.InputPrice, + OutputPrice: p.OutputPrice, + CacheWritePrice: p.CacheWritePrice, + CacheWrite1hPrice: p.CacheWrite1hPrice, + CacheReadPrice: p.CacheReadPrice, + FastMultiplier: p.FastMultiplier, + FlexMultiplier: p.FlexMultiplier, + ImageInputPrice: p.ImageInputPrice, + ImageOutputPrice: p.ImageOutputPrice, + PerRequestPrice: p.PerRequestPrice, + Intervals: intervals, + TimePricing: timePricingToResponse(p.TimePricing), } } @@ -300,6 +305,7 @@ func intervalToResponse(iv service.PricingInterval) pricingIntervalResponse { InputPrice: iv.InputPrice, OutputPrice: iv.OutputPrice, CacheWritePrice: iv.CacheWritePrice, + CacheWrite1hPrice: iv.CacheWrite1hPrice, CacheReadPrice: iv.CacheReadPrice, InputMultiplier: iv.InputMultiplier, OutputMultiplier: iv.OutputMultiplier, @@ -334,6 +340,7 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult InputPrice: iv.InputPrice, OutputPrice: iv.OutputPrice, CacheWritePrice: iv.CacheWritePrice, + CacheWrite1hPrice: iv.CacheWrite1hPrice, CacheReadPrice: iv.CacheReadPrice, InputMultiplier: inputMultiplier, OutputMultiplier: outputMultiplier, @@ -349,20 +356,21 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult flexMultiplier = r.FlexMultiplier } result = append(result, service.ChannelModelPricing{ - Platform: platform, - Models: r.Models, - BillingMode: billingMode, - InputPrice: r.InputPrice, - OutputPrice: r.OutputPrice, - CacheWritePrice: r.CacheWritePrice, - CacheReadPrice: r.CacheReadPrice, - FastMultiplier: fastMultiplier, - FlexMultiplier: flexMultiplier, - ImageInputPrice: r.ImageInputPrice, - ImageOutputPrice: r.ImageOutputPrice, - PerRequestPrice: r.PerRequestPrice, - Intervals: intervals, - TimePricing: timePricingRequestToService(r.TimePricing), + Platform: platform, + Models: r.Models, + BillingMode: billingMode, + InputPrice: r.InputPrice, + OutputPrice: r.OutputPrice, + CacheWritePrice: r.CacheWritePrice, + CacheWrite1hPrice: r.CacheWrite1hPrice, + CacheReadPrice: r.CacheReadPrice, + FastMultiplier: fastMultiplier, + FlexMultiplier: flexMultiplier, + ImageInputPrice: r.ImageInputPrice, + ImageOutputPrice: r.ImageOutputPrice, + PerRequestPrice: r.PerRequestPrice, + Intervals: intervals, + TimePricing: timePricingRequestToService(r.TimePricing), }) } return result @@ -597,15 +605,24 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) { response.Success(c, gin.H{"found": false}) return } + cacheWritePrice := pricing.CacheCreationPricePerToken + var cacheWrite1hPrice *float64 + if pricing.SupportsCacheBreakdown { + if pricing.CacheCreation5mPrice > 0 { + cacheWritePrice = pricing.CacheCreation5mPrice + } + cacheWrite1hPrice = &pricing.CacheCreation1hPrice + } response.Success(c, gin.H{ - "found": true, - "input_price": pricing.InputPricePerToken, - "output_price": pricing.OutputPricePerToken, - "cache_write_price": pricing.CacheCreationPricePerToken, - "cache_read_price": pricing.CacheReadPricePerToken, - "image_input_price": pricing.ImageInputPricePerToken, - "image_output_price": pricing.ImageOutputPricePerToken, + "found": true, + "input_price": pricing.InputPricePerToken, + "output_price": pricing.OutputPricePerToken, + "cache_write_price": cacheWritePrice, + "cache_write_1h_price": cacheWrite1hPrice, + "cache_read_price": pricing.CacheReadPricePerToken, + "image_input_price": pricing.ImageInputPricePerToken, + "image_output_price": pricing.ImageOutputPricePerToken, }) } diff --git a/backend/internal/handler/admin/channel_handler_test.go b/backend/internal/handler/admin/channel_handler_test.go index 2da7e02d00..cca67c9e47 100644 --- a/backend/internal/handler/admin/channel_handler_test.go +++ b/backend/internal/handler/admin/channel_handler_test.go @@ -320,15 +320,16 @@ func TestPricingRequestToService_Defaults(t *testing.T) { func TestPricingRequestToService_WithAllFields(t *testing.T) { reqs := []channelModelPricingRequest{ { - Platform: "openai", - Models: []string{"gpt-4", "gpt-4o"}, - BillingMode: "per_request", - InputPrice: float64Ptr(0.01), - OutputPrice: float64Ptr(0.03), - CacheWritePrice: float64Ptr(0.005), - CacheReadPrice: float64Ptr(0.002), - ImageOutputPrice: float64Ptr(0.04), - PerRequestPrice: float64Ptr(0.5), + Platform: "openai", + Models: []string{"gpt-4", "gpt-4o"}, + BillingMode: "per_request", + InputPrice: float64Ptr(0.01), + OutputPrice: float64Ptr(0.03), + CacheWritePrice: float64Ptr(0.005), + CacheWrite1hPrice: float64Ptr(0.008), + CacheReadPrice: float64Ptr(0.002), + ImageOutputPrice: float64Ptr(0.04), + PerRequestPrice: float64Ptr(0.5), }, } @@ -341,6 +342,7 @@ func TestPricingRequestToService_WithAllFields(t *testing.T) { require.Equal(t, float64Ptr(0.01), r.InputPrice) require.Equal(t, float64Ptr(0.03), r.OutputPrice) require.Equal(t, float64Ptr(0.005), r.CacheWritePrice) + require.Equal(t, float64Ptr(0.008), r.CacheWrite1hPrice) require.Equal(t, float64Ptr(0.002), r.CacheReadPrice) require.Equal(t, float64Ptr(0.04), r.ImageOutputPrice) require.Equal(t, float64Ptr(0.5), r.PerRequestPrice) @@ -353,15 +355,16 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) { BillingMode: "per_request", Intervals: []pricingIntervalRequest{ { - MinTokens: 0, - MaxTokens: intPtr(2000), - TierLabel: "small", - InputPrice: float64Ptr(0.01), - OutputPrice: float64Ptr(0.02), - CacheWritePrice: float64Ptr(0.003), - CacheReadPrice: float64Ptr(0.001), - PerRequestPrice: float64Ptr(0.1), - SortOrder: 1, + MinTokens: 0, + MaxTokens: intPtr(2000), + TierLabel: "small", + InputPrice: float64Ptr(0.01), + OutputPrice: float64Ptr(0.02), + CacheWritePrice: float64Ptr(0.003), + CacheWrite1hPrice: float64Ptr(0.006), + CacheReadPrice: float64Ptr(0.001), + PerRequestPrice: float64Ptr(0.1), + SortOrder: 1, }, { MinTokens: 2000, @@ -384,6 +387,7 @@ func TestPricingRequestToService_WithIntervals(t *testing.T) { require.Equal(t, float64Ptr(0.01), iv0.InputPrice) require.Equal(t, float64Ptr(0.02), iv0.OutputPrice) require.Equal(t, float64Ptr(0.003), iv0.CacheWritePrice) + require.Equal(t, float64Ptr(0.006), iv0.CacheWrite1hPrice) require.Equal(t, float64Ptr(0.001), iv0.CacheReadPrice) require.Equal(t, float64Ptr(0.1), iv0.PerRequestPrice) require.Equal(t, 1, iv0.SortOrder) @@ -558,3 +562,50 @@ func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) { require.NotNil(t, body.Data.Models, "models must not be null for platform=%s", platform) } } + +func setupModelDefaultPricingRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.New() + h := &ChannelHandler{billingService: service.NewBillingService(nil, nil)} + router.GET("/channels/model-pricing", h.GetModelDefaultPricing) + return router +} + +func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) { + router := setupModelDefaultPricingRouter() + req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-fable-5-1", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body struct { + Data struct { + Found bool `json:"found"` + CacheWritePrice float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + require.True(t, body.Data.Found) + require.InDelta(t, 12.5e-6, body.Data.CacheWritePrice, 1e-12) + require.NotNil(t, body.Data.CacheWrite1hPrice) + require.InDelta(t, 20e-6, *body.Data.CacheWrite1hPrice, 1e-12) +} + +func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) { + router := setupModelDefaultPricingRouter() + req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-sonnet-4", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + var body struct { + Data struct { + Found bool `json:"found"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + require.True(t, body.Data.Found) + require.Nil(t, body.Data.CacheWrite1hPrice) +} diff --git a/backend/internal/handler/available_channel_handler.go b/backend/internal/handler/available_channel_handler.go index a6b7a36a3c..5d35ff389e 100644 --- a/backend/internal/handler/available_channel_handler.go +++ b/backend/internal/handler/available_channel_handler.go @@ -66,27 +66,29 @@ type userAvailableGroup struct { // userSupportedModelPricing 用户可见的定价字段白名单。 type userSupportedModelPricing struct { - BillingMode string `json:"billing_mode"` - InputPrice *float64 `json:"input_price"` - OutputPrice *float64 `json:"output_price"` - CacheWritePrice *float64 `json:"cache_write_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - ImageInputPrice *float64 `json:"image_input_price"` - ImageOutputPrice *float64 `json:"image_output_price"` - PerRequestPrice *float64 `json:"per_request_price"` - Intervals []userPricingIntervalDTO `json:"intervals"` + BillingMode string `json:"billing_mode"` + InputPrice *float64 `json:"input_price"` + OutputPrice *float64 `json:"output_price"` + CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + CacheReadPrice *float64 `json:"cache_read_price"` + ImageInputPrice *float64 `json:"image_input_price"` + ImageOutputPrice *float64 `json:"image_output_price"` + PerRequestPrice *float64 `json:"per_request_price"` + Intervals []userPricingIntervalDTO `json:"intervals"` } // userPricingIntervalDTO 定价区间白名单(去掉内部 ID、SortOrder 等前端不渲染的字段)。 type userPricingIntervalDTO struct { - MinTokens int `json:"min_tokens"` - MaxTokens *int `json:"max_tokens"` - TierLabel string `json:"tier_label,omitempty"` - InputPrice *float64 `json:"input_price"` - OutputPrice *float64 `json:"output_price"` - CacheWritePrice *float64 `json:"cache_write_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - PerRequestPrice *float64 `json:"per_request_price"` + MinTokens int `json:"min_tokens"` + MaxTokens *int `json:"max_tokens"` + TierLabel string `json:"tier_label,omitempty"` + InputPrice *float64 `json:"input_price"` + OutputPrice *float64 `json:"output_price"` + CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + CacheReadPrice *float64 `json:"cache_read_price"` + PerRequestPrice *float64 `json:"per_request_price"` } // userSupportedModel 用户可见的支持模型条目。 @@ -292,14 +294,15 @@ func toUserPricingIntervals(src []service.PricingInterval) []userPricingInterval intervals := make([]userPricingIntervalDTO, 0, len(src)) for _, iv := range src { intervals = append(intervals, userPricingIntervalDTO{ - MinTokens: iv.MinTokens, - MaxTokens: iv.MaxTokens, - TierLabel: iv.TierLabel, - InputPrice: iv.InputPrice, - OutputPrice: iv.OutputPrice, - CacheWritePrice: iv.CacheWritePrice, - CacheReadPrice: iv.CacheReadPrice, - PerRequestPrice: iv.PerRequestPrice, + MinTokens: iv.MinTokens, + MaxTokens: iv.MaxTokens, + TierLabel: iv.TierLabel, + InputPrice: iv.InputPrice, + OutputPrice: iv.OutputPrice, + CacheWritePrice: iv.CacheWritePrice, + CacheWrite1hPrice: iv.CacheWrite1hPrice, + CacheReadPrice: iv.CacheReadPrice, + PerRequestPrice: iv.PerRequestPrice, }) } return intervals @@ -320,14 +323,15 @@ func toUserPricing(p *service.ChannelModelPricing) *userSupportedModelPricing { billingMode = string(service.BillingModeToken) } return &userSupportedModelPricing{ - BillingMode: billingMode, - InputPrice: p.InputPrice, - OutputPrice: p.OutputPrice, - CacheWritePrice: p.CacheWritePrice, - CacheReadPrice: p.CacheReadPrice, - ImageInputPrice: p.ImageInputPrice, - ImageOutputPrice: p.ImageOutputPrice, - PerRequestPrice: p.PerRequestPrice, - Intervals: intervals, + BillingMode: billingMode, + InputPrice: p.InputPrice, + OutputPrice: p.OutputPrice, + CacheWritePrice: p.CacheWritePrice, + CacheWrite1hPrice: p.CacheWrite1hPrice, + CacheReadPrice: p.CacheReadPrice, + ImageInputPrice: p.ImageInputPrice, + ImageOutputPrice: p.ImageOutputPrice, + PerRequestPrice: p.PerRequestPrice, + Intervals: intervals, } } diff --git a/backend/internal/pkg/antigravity/claude_types.go b/backend/internal/pkg/antigravity/claude_types.go index c0335c4156..a4ba557ef3 100644 --- a/backend/internal/pkg/antigravity/claude_types.go +++ b/backend/internal/pkg/antigravity/claude_types.go @@ -153,6 +153,7 @@ type modelDef struct { // Antigravity 支持的 Claude 模型 var claudeModels = []modelDef{ + {ID: "claude-fable-5-1", DisplayName: "Claude Fable 5.1", CreatedAt: "2026-09-01T00:00:00Z"}, {ID: "claude-fable-5", DisplayName: "Claude Fable 5", CreatedAt: "2026-06-09T00:00:00Z"}, {ID: "claude-opus-4-5-thinking", DisplayName: "Claude Opus 4.5 Thinking", CreatedAt: "2025-11-01T00:00:00Z"}, {ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5", CreatedAt: "2025-09-29T00:00:00Z"}, diff --git a/backend/internal/pkg/antigravity/claude_types_test.go b/backend/internal/pkg/antigravity/claude_types_test.go index 65c8078c33..8b75f092b5 100644 --- a/backend/internal/pkg/antigravity/claude_types_test.go +++ b/backend/internal/pkg/antigravity/claude_types_test.go @@ -12,6 +12,7 @@ func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) { } requiredIDs := []string{ + "claude-fable-5-1", "claude-fable-5", "claude-opus-4-8", "claude-opus-4-6-thinking", diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index 819db987f0..1750848348 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -222,6 +222,7 @@ type modelInfo struct { // 只有在此映射表中的模型才会注入身份提示词 // 注意:模型映射逻辑在网关层完成;这里仅用于按模型前缀判断是否注入身份提示词。 var modelInfoMap = map[string]modelInfo{ + "claude-fable-5-1": {DisplayName: "Claude Fable 5.1", CanonicalID: "claude-fable-5-1"}, "claude-fable-5": {DisplayName: "Claude Fable 5", CanonicalID: "claude-fable-5"}, "claude-opus-4-8": {DisplayName: "Claude Opus 4.8", CanonicalID: "claude-opus-4-8"}, "claude-opus-4-7": {DisplayName: "Claude Opus 4.7", CanonicalID: "claude-opus-4-7"}, diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 8698bd9337..c3c6854a58 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -128,6 +128,12 @@ type Model struct { // DefaultModels Claude Code 客户端支持的默认模型列表 var DefaultModels = []Model{ + { + ID: "claude-fable-5-1", + Type: "model", + DisplayName: "Claude Fable 5.1", + CreatedAt: "2026-09-01T00:00:00Z", + }, { ID: "claude-fable-5", Type: "model", diff --git a/backend/internal/pkg/claude/constants_model_test.go b/backend/internal/pkg/claude/constants_model_test.go new file mode 100644 index 0000000000..6cfa265bdc --- /dev/null +++ b/backend/internal/pkg/claude/constants_model_test.go @@ -0,0 +1,20 @@ +package claude + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefaultModelsContainsClaudeFable51(t *testing.T) { + t.Parallel() + + for _, model := range DefaultModels { + if model.ID == "claude-fable-5-1" { + require.Equal(t, "Claude Fable 5.1", model.DisplayName) + require.Equal(t, "2026-09-01T00:00:00Z", model.CreatedAt) + return + } + } + t.Fatal("claude-fable-5-1 missing from DefaultModels") +} diff --git a/backend/internal/repository/channel_repo_account_stats_pricing.go b/backend/internal/repository/channel_repo_account_stats_pricing.go index 9e00fed822..22922c689e 100644 --- a/backend/internal/repository/channel_repo_account_stats_pricing.go +++ b/backend/internal/repository/channel_repo_account_stats_pricing.go @@ -67,7 +67,7 @@ func (r *channelRepository) batchLoadAccountStatsModelPricing(ctx context.Contex rows, err := r.db.QueryContext(ctx, `SELECT id, rule_id, platform, models, billing_mode, input_price, output_price, - cache_write_price, cache_read_price, image_output_price, per_request_price, created_at, updated_at + cache_write_price, cache_write_1h_price, cache_read_price, image_output_price, per_request_price, created_at, updated_at FROM channel_account_stats_model_pricing WHERE rule_id = ANY($1) ORDER BY rule_id, id`, pq.Array(ruleIDs), ) @@ -83,7 +83,7 @@ func (r *channelRepository) batchLoadAccountStatsModelPricing(ctx context.Contex var modelsJSON []byte if err := rows.Scan( &p.ID, &ruleID, &p.Platform, &modelsJSON, &p.BillingMode, - &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheReadPrice, + &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice, &p.ImageOutputPrice, &p.PerRequestPrice, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan account stats model pricing: %w", err) @@ -178,10 +178,10 @@ func createAccountStatsModelPricingTx(ctx context.Context, tx *sql.Tx, ruleID in } platform := pricing.Platform err = tx.QueryRowContext(ctx, - `INSERT INTO channel_account_stats_model_pricing (rule_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, image_output_price, per_request_price) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at`, + `INSERT INTO channel_account_stats_model_pricing (rule_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, image_output_price, per_request_price) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at`, ruleID, platform, modelsJSON, billingMode, - pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheReadPrice, + pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt) if err != nil { @@ -202,10 +202,10 @@ func createAccountStatsModelPricingTx(ctx context.Context, tx *sql.Tx, ruleID in func createAccountStatsIntervalTx(ctx context.Context, tx *sql.Tx, iv *service.PricingInterval) error { return tx.QueryRowContext(ctx, `INSERT INTO channel_account_stats_pricing_intervals - (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_read_price, per_request_price, sort_order) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at`, + (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, per_request_price, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at`, iv.PricingID, iv.MinTokens, iv.MaxTokens, iv.TierLabel, - iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheReadPrice, + iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheWrite1hPrice, iv.CacheReadPrice, iv.PerRequestPrice, iv.SortOrder, ).Scan(&iv.ID, &iv.CreatedAt, &iv.UpdatedAt) } @@ -217,7 +217,7 @@ func (r *channelRepository) batchLoadAccountStatsIntervals(ctx context.Context, } rows, err := r.db.QueryContext(ctx, `SELECT id, pricing_id, min_tokens, max_tokens, tier_label, - input_price, output_price, cache_write_price, cache_read_price, + input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, per_request_price, sort_order, created_at, updated_at FROM channel_account_stats_pricing_intervals WHERE pricing_id = ANY($1) ORDER BY pricing_id, sort_order, id`, @@ -233,7 +233,7 @@ func (r *channelRepository) batchLoadAccountStatsIntervals(ctx context.Context, var iv service.PricingInterval if err := rows.Scan( &iv.ID, &iv.PricingID, &iv.MinTokens, &iv.MaxTokens, &iv.TierLabel, - &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheReadPrice, + &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheWrite1hPrice, &iv.CacheReadPrice, &iv.PerRequestPrice, &iv.SortOrder, &iv.CreatedAt, &iv.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan account stats pricing interval: %w", err) diff --git a/backend/internal/repository/channel_repo_pricing.go b/backend/internal/repository/channel_repo_pricing.go index e25642085a..1a88eeb977 100644 --- a/backend/internal/repository/channel_repo_pricing.go +++ b/backend/internal/repository/channel_repo_pricing.go @@ -16,7 +16,7 @@ import ( func (r *channelRepository) ListModelPricing(ctx context.Context, channelID int64) ([]service.ChannelModelPricing, error) { rows, err := r.db.QueryContext(ctx, - `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at + `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at FROM channel_model_pricing WHERE channel_id = $1 ORDER BY id`, channelID, ) if err != nil { @@ -61,9 +61,9 @@ func (r *channelRepository) UpdateModelPricing(ctx context.Context, pricing *ser } result, err := r.db.ExecContext(ctx, `UPDATE channel_model_pricing - SET models = $1, billing_mode = $2, input_price = $3, output_price = $4, cache_write_price = $5, cache_read_price = $6, fast_multiplier = $7, flex_multiplier = $8, image_input_price = $9, image_output_price = $10, per_request_price = $11, time_pricing = $12, platform = $13, updated_at = NOW() - WHERE id = $14`, - modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheReadPrice, + SET models = $1, billing_mode = $2, input_price = $3, output_price = $4, cache_write_price = $5, cache_write_1h_price = $6, cache_read_price = $7, fast_multiplier = $8, flex_multiplier = $9, image_input_price = $10, image_output_price = $11, per_request_price = $12, time_pricing = $13, platform = $14, updated_at = NOW() + WHERE id = $15`, + modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, pricing.FastMultiplier, pricing.FlexMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, timePricingJSON, pricing.Platform, pricing.ID, ) @@ -96,7 +96,7 @@ func (r *channelRepository) ReplaceModelPricing(ctx context.Context, channelID i // batchLoadModelPricing 批量加载多个渠道的模型定价(含区间) func (r *channelRepository) batchLoadModelPricing(ctx context.Context, channelIDs []int64) (map[int64][]service.ChannelModelPricing, error) { rows, err := r.db.QueryContext(ctx, - `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at + `SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at FROM channel_model_pricing WHERE channel_id = ANY($1) ORDER BY channel_id, id`, pq.Array(channelIDs), ) @@ -136,7 +136,7 @@ func (r *channelRepository) batchLoadModelPricing(ctx context.Context, channelID func (r *channelRepository) batchLoadIntervals(ctx context.Context, pricingIDs []int64) (map[int64][]service.PricingInterval, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, pricing_id, min_tokens, max_tokens, tier_label, - input_price, output_price, cache_write_price, cache_read_price, + input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, input_multiplier, output_multiplier, cache_write_multiplier, cache_read_multiplier, per_request_price, sort_order, created_at, updated_at FROM channel_pricing_intervals @@ -153,7 +153,7 @@ func (r *channelRepository) batchLoadIntervals(ctx context.Context, pricingIDs [ var iv service.PricingInterval if err := rows.Scan( &iv.ID, &iv.PricingID, &iv.MinTokens, &iv.MaxTokens, &iv.TierLabel, - &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheReadPrice, + &iv.InputPrice, &iv.OutputPrice, &iv.CacheWritePrice, &iv.CacheWrite1hPrice, &iv.CacheReadPrice, &iv.InputMultiplier, &iv.OutputMultiplier, &iv.CacheWriteMultiplier, &iv.CacheReadMultiplier, &iv.PerRequestPrice, &iv.SortOrder, &iv.CreatedAt, &iv.UpdatedAt, ); err != nil { @@ -179,7 +179,7 @@ func scanModelPricingRows(rows *sql.Rows) ([]service.ChannelModelPricing, []int6 var timePricingJSON []byte if err := rows.Scan( &p.ID, &p.ChannelID, &p.Platform, &modelsJSON, &p.BillingMode, - &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheReadPrice, + &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice, &p.FastMultiplier, &p.FlexMultiplier, &p.ImageInputPrice, &p.ImageOutputPrice, &p.PerRequestPrice, &timePricingJSON, &p.CreatedAt, &p.UpdatedAt, ); err != nil { @@ -247,10 +247,10 @@ func createModelPricingExec(ctx context.Context, exec dbExec, pricing *service.C platform = "anthropic" } err = exec.QueryRowContext(ctx, - `INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at`, + `INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at`, pricing.ChannelID, platform, modelsJSON, billingMode, - pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheReadPrice, + pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, pricing.FastMultiplier, pricing.FlexMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, timePricingJSON, ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt) @@ -293,10 +293,10 @@ func unmarshalChannelTimePricing(data []byte) (*service.ChannelTimePricing, erro func createIntervalExec(ctx context.Context, exec dbExec, iv *service.PricingInterval) error { return exec.QueryRowContext(ctx, `INSERT INTO channel_pricing_intervals - (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_read_price, input_multiplier, output_multiplier, cache_write_multiplier, cache_read_multiplier, per_request_price, sort_order) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at`, + (pricing_id, min_tokens, max_tokens, tier_label, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, input_multiplier, output_multiplier, cache_write_multiplier, cache_read_multiplier, per_request_price, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at`, iv.PricingID, iv.MinTokens, iv.MaxTokens, iv.TierLabel, - iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheReadPrice, + iv.InputPrice, iv.OutputPrice, iv.CacheWritePrice, iv.CacheWrite1hPrice, iv.CacheReadPrice, iv.InputMultiplier, iv.OutputMultiplier, iv.CacheWriteMultiplier, iv.CacheReadMultiplier, iv.PerRequestPrice, iv.SortOrder, ).Scan(&iv.ID, &iv.CreatedAt, &iv.UpdatedAt) diff --git a/backend/internal/repository/channel_repo_pricing_time_test.go b/backend/internal/repository/channel_repo_pricing_time_test.go index 38acdbdc73..9532cdf162 100644 --- a/backend/internal/repository/channel_repo_pricing_time_test.go +++ b/backend/internal/repository/channel_repo_pricing_time_test.go @@ -16,7 +16,7 @@ import ( var channelModelPricingTimePricingColumns = []string{ "id", "channel_id", "platform", "models", "billing_mode", "input_price", "output_price", - "cache_write_price", "cache_read_price", "fast_multiplier", "flex_multiplier", "image_input_price", "image_output_price", + "cache_write_price", "cache_write_1h_price", "cache_read_price", "fast_multiplier", "flex_multiplier", "image_input_price", "image_output_price", "per_request_price", "time_pricing", "created_at", "updated_at", } @@ -33,7 +33,7 @@ func newChannelModelPricingTimePricingRepo(t *testing.T) (*channelRepository, sq func modelPricingTimePricingRow(timePricing any) *sqlmock.Rows { return sqlmock.NewRows(channelModelPricingTimePricingColumns).AddRow( int64(11), int64(7), "openai", `["gpt-5"]`, service.BillingModeToken, - nil, nil, nil, nil, nil, nil, nil, nil, nil, timePricing, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, timePricing, time.Date(2026, 8, 17, 0, 0, 0, 0, time.UTC), time.Date(2026, 8, 17, 1, 0, 0, 0, time.UTC), ) } @@ -105,10 +105,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateRoundTrip(t *testing.T) { t.Run("create writes JSON", func(t *testing.T) { repo, mock := newChannelModelPricingTimePricingRepo(t) - mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")). + mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")). WithArgs( int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON, ). WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{})) @@ -118,10 +118,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateRoundTrip(t *testing.T) { t.Run("update writes JSON and entry ID", func(t *testing.T) { repo, mock := newChannelModelPricingTimePricingRepo(t) - mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$11, time_pricing = \$12, platform = \$13.*WHERE id = \$14`). + mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$12, time_pricing = \$13, platform = \$14.*WHERE id = \$15`). WithArgs( []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON, "openai", int64(11), + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON, "openai", int64(11), ). WillReturnResult(sqlmock.NewResult(0, 1)) @@ -153,10 +153,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateWriteNullWhenDisabled(t *t t.Run("create writes SQL NULL", func(t *testing.T) { repo, mock := newChannelModelPricingTimePricingRepo(t) - mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")). + mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO channel_model_pricing (channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")). WithArgs( int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, ). WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{})) @@ -166,10 +166,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateWriteNullWhenDisabled(t *t t.Run("update writes SQL NULL", func(t *testing.T) { repo, mock := newChannelModelPricingTimePricingRepo(t) - mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$11, time_pricing = \$12, platform = \$13.*WHERE id = \$14`). + mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$12, time_pricing = \$13, platform = \$14.*WHERE id = \$15`). WithArgs( []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "openai", int64(11), + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "openai", int64(11), ). WillReturnResult(sqlmock.NewResult(0, 1)) diff --git a/backend/internal/service/account_stats_pricing.go b/backend/internal/service/account_stats_pricing.go index af82bc15d6..8ac3dba383 100644 --- a/backend/internal/service/account_stats_pricing.go +++ b/backend/internal/service/account_stats_pricing.go @@ -188,11 +188,12 @@ func calculateTokenStatsCost(pricing *ChannelModelPricing, tokens UsageTokens) * totalTokens := tokens.InputTokens + tokens.OutputTokens + tokens.CacheCreationTokens + tokens.CacheReadTokens if iv := FindMatchingInterval(pricing.Intervals, totalTokens); iv != nil { p = &ChannelModelPricing{ - InputPrice: iv.InputPrice, - OutputPrice: iv.OutputPrice, - CacheWritePrice: iv.CacheWritePrice, - CacheReadPrice: iv.CacheReadPrice, - PerRequestPrice: iv.PerRequestPrice, + InputPrice: iv.InputPrice, + OutputPrice: iv.OutputPrice, + CacheWritePrice: iv.CacheWritePrice, + CacheWrite1hPrice: iv.CacheWrite1hPrice, + CacheReadPrice: iv.CacheReadPrice, + PerRequestPrice: iv.PerRequestPrice, } } } @@ -202,9 +203,17 @@ func calculateTokenStatsCost(pricing *ChannelModelPricing, tokens UsageTokens) * } return *ptr } + cacheCreationCost := float64(tokens.CacheCreationTokens) * deref(p.CacheWritePrice) + if p.CacheWrite1hPrice != nil { + cache5m, cache1h := normalizeCacheCreationBreakdown(tokens) + if cache5m > 0 || cache1h > 0 { + cacheCreationCost = float64(cache5m)*deref(p.CacheWritePrice) + + float64(cache1h)*deref(p.CacheWrite1hPrice) + } + } cost := float64(tokens.InputTokens)*deref(p.InputPrice) + float64(tokens.OutputTokens)*deref(p.OutputPrice) + - float64(tokens.CacheCreationTokens)*deref(p.CacheWritePrice) + + cacheCreationCost + float64(tokens.CacheReadTokens)*deref(p.CacheReadPrice) + float64(tokens.ImageOutputTokens)*deref(p.ImageOutputPrice) if cost <= 0 { diff --git a/backend/internal/service/account_stats_pricing_test.go b/backend/internal/service/account_stats_pricing_test.go index 84ca0897ec..97628f58c0 100644 --- a/backend/internal/service/account_stats_pricing_test.go +++ b/backend/internal/service/account_stats_pricing_test.go @@ -228,6 +228,24 @@ func TestCalculateStatsCost_TokenBilling_WithCache(t *testing.T) { require.InDelta(t, 0.95, *result, 1e-12) } +func TestCalculateStatsCost_TokenBilling_WithCacheTTLPrices(t *testing.T) { + pricing := &ChannelModelPricing{ + BillingMode: BillingModeToken, + CacheWritePrice: testPtrFloat64(0.003), + CacheWrite1hPrice: testPtrFloat64(0.005), + } + tokens := UsageTokens{ + CacheCreationTokens: 200, + CacheCreation5mTokens: 80, + CacheCreation1hTokens: 120, + } + + result := calculateStatsCost(pricing, tokens, 1) + require.NotNil(t, result) + // 80*0.003 + 120*0.005 = 0.84 + require.InDelta(t, 0.84, *result, 1e-12) +} + func TestCalculateStatsCost_TokenBilling_WithImageOutput(t *testing.T) { pricing := &ChannelModelPricing{ BillingMode: BillingModeToken, diff --git a/backend/internal/service/antigravity_model_mapping_test.go b/backend/internal/service/antigravity_model_mapping_test.go index 61cc7e31d6..ce6a01b209 100644 --- a/backend/internal/service/antigravity_model_mapping_test.go +++ b/backend/internal/service/antigravity_model_mapping_test.go @@ -76,6 +76,12 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { }, // 3. 默认映射中的透传(映射到自己) + { + name: "默认映射透传 - claude-fable-5-1", + requestedModel: "claude-fable-5-1", + accountMapping: nil, + expected: "claude-fable-5-1", + }, { name: "默认映射透传 - claude-fable-5", requestedModel: "claude-fable-5", @@ -233,6 +239,7 @@ func TestAntigravityGatewayService_IsModelSupported(t *testing.T) { expected bool }{ // 直接支持 + {"直接支持 - claude-fable-5-1", "claude-fable-5-1", true}, {"直接支持 - claude-fable-5", "claude-fable-5", true}, {"直接支持 - claude-sonnet-4-5", "claude-sonnet-4-5", true}, {"直接支持 - gemini-3-flash", "gemini-3-flash", true}, diff --git a/backend/internal/service/bedrock_request_test.go b/backend/internal/service/bedrock_request_test.go index d580f8c1ba..71c954f44c 100644 --- a/backend/internal/service/bedrock_request_test.go +++ b/backend/internal/service/bedrock_request_test.go @@ -541,6 +541,20 @@ func TestResolveBedrockModelID(t *testing.T) { assert.Equal(t, "anthropic.claude-fable-5", modelID) }) + t.Run("默认 Fable 5.1 映射使用官方 Bedrock 模型 ID", func(t *testing.T) { + account := &Account{ + Platform: PlatformAnthropic, + Type: AccountTypeBedrock, + Credentials: map[string]any{ + "aws_region": "eu-west-1", + }, + } + + modelID, ok := ResolveBedrockModelID(account, "claude-fable-5-1") + require.True(t, ok) + assert.Equal(t, "anthropic.claude-fable-5-1", modelID) + }) + t.Run("force global rewrites anthropic regional model id", func(t *testing.T) { account := &Account{ Platform: PlatformAnthropic, diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index ed4856e961..1f3a346f64 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -359,6 +359,27 @@ func (s *BillingService) initFallbackPricing() { s.fallbackPrices["claude-opus-4.8"] = pricingWithPriorityMultiplier(s.fallbackPrices["claude-opus-4.7"], 2) s.fallbackPrices["claude-opus-5"] = pricingWithPriorityMultiplier(s.fallbackPrices["claude-opus-4.8"], 2) + // Claude Fable 5.x uses the same input/output and cache-write prices, while + // Fable 5.1 reduces cache reads from $1 to $0.25 per MTok. + s.fallbackPrices["claude-fable-5"] = &ModelPricing{ + InputPricePerToken: 10e-6, + OutputPricePerToken: 50e-6, + CacheCreationPricePerToken: 12.5e-6, + CacheCreation5mPrice: 12.5e-6, + CacheCreation1hPrice: 20e-6, + CacheReadPricePerToken: 1e-6, + SupportsCacheBreakdown: true, + } + s.fallbackPrices["claude-fable-5-1"] = &ModelPricing{ + InputPricePerToken: 10e-6, + OutputPricePerToken: 50e-6, + CacheCreationPricePerToken: 12.5e-6, + CacheCreation5mPrice: 12.5e-6, + CacheCreation1hPrice: 20e-6, + CacheReadPricePerToken: 0.25e-6, + SupportsCacheBreakdown: true, + } + // Gemini 3.1 Pro s.fallbackPrices["gemini-3.1-pro"] = &ModelPricing{ InputPricePerToken: 2e-6, // $2 per MTok @@ -782,6 +803,13 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { modelLower := strings.ToLower(model) // 按模型系列匹配 + if strings.Contains(modelLower, "fable-5-1") || strings.Contains(modelLower, "fable-5.1") || + strings.Contains(modelLower, "fable5.1") || strings.Contains(modelLower, "fable51") { + return s.fallbackPrices["claude-fable-5-1"] + } + if strings.Contains(modelLower, "fable-5") || strings.Contains(modelLower, "fable5") { + return s.fallbackPrices["claude-fable-5"] + } if strings.Contains(modelLower, "opus") { // "opus-5" 必须先判:不能用裸 "5" 匹配,否则 claude-opus-4-5 会被误判。 if strings.Contains(modelLower, "opus-5") || strings.Contains(modelLower, "opus5") { @@ -1181,7 +1209,15 @@ func applyChannelTokenPriceOverrides(pricing *ModelPricing, channelPricing *Chan pricing.CacheCreationPricePerTokenPriority = priority pricing.CacheCreationPriceExplicit = true pricing.CacheCreation5mPrice = *channelPricing.CacheWritePrice - pricing.CacheCreation1hPrice = *channelPricing.CacheWritePrice + if channelPricing.CacheWrite1hPrice == nil { + // Preserve the pre-split behavior for existing configurations: a lone + // cache_write_price continues to override both TTL tiers. + pricing.CacheCreation1hPrice = *channelPricing.CacheWritePrice + } + } + if channelPricing.CacheWrite1hPrice != nil { + pricing.CacheCreation1hPrice = *channelPricing.CacheWrite1hPrice + pricing.SupportsCacheBreakdown = true } if channelPricing.CacheReadPrice != nil { priority := channelTierOverridePrice(pricing.CacheReadPricePerToken, pricing.CacheReadPricePerTokenPriority, *channelPricing.CacheReadPrice) diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 8bb351c559..956fe73d33 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -1746,6 +1746,32 @@ func TestGetModelPricingWithChannel_CacheWritePriceAffects5mAnd1h(t *testing.T) require.InDelta(t, 7e-6, pricing.CacheCreation1hPrice, 1e-12) } +func TestGetModelPricingWithChannel_CacheWriteTTLPricesCanDiffer(t *testing.T) { + svc := newTestBillingService() + + pricing, err := svc.GetModelPricingWithChannel("claude-fable-5-1", &ChannelModelPricing{ + CacheWritePrice: testPtrFloat64(13e-6), + CacheWrite1hPrice: testPtrFloat64(21e-6), + }) + require.NoError(t, err) + require.True(t, pricing.SupportsCacheBreakdown) + require.InDelta(t, 13e-6, pricing.CacheCreationPricePerToken, 1e-12) + require.InDelta(t, 13e-6, pricing.CacheCreation5mPrice, 1e-12) + require.InDelta(t, 21e-6, pricing.CacheCreation1hPrice, 1e-12) +} + +func TestGetModelPricing_Fable51FallbackPricing(t *testing.T) { + svc := newTestBillingService() + + pricing, err := svc.GetModelPricing("claude-fable-5-1") + require.NoError(t, err) + require.InDelta(t, 10e-6, pricing.InputPricePerToken, 1e-12) + require.InDelta(t, 50e-6, pricing.OutputPricePerToken, 1e-12) + require.InDelta(t, 12.5e-6, pricing.CacheCreation5mPrice, 1e-12) + require.InDelta(t, 20e-6, pricing.CacheCreation1hPrice, 1e-12) + require.InDelta(t, 0.25e-6, pricing.CacheReadPricePerToken, 1e-12) +} + func TestGetModelPricingWithChannel_CacheReadPriceAffectsPriority(t *testing.T) { svc := newTestBillingService() diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index 3e300edf99..df3b5f892a 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -87,24 +87,25 @@ type AccountStatsPricingRule struct { // ChannelModelPricing 渠道模型定价条目 type ChannelModelPricing struct { - ID int64 `json:"id,omitempty"` - ChannelID int64 `json:"channel_id,omitempty"` - Platform string `json:"platform"` // 所属平台(anthropic/openai/gemini/...) - Models []string `json:"models"` - BillingMode BillingMode `json:"billing_mode"` - InputPrice *float64 `json:"input_price"` - OutputPrice *float64 `json:"output_price"` - CacheWritePrice *float64 `json:"cache_write_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - FastMultiplier *float64 `json:"fast_multiplier"` - FlexMultiplier *float64 `json:"flex_multiplier"` - ImageInputPrice *float64 `json:"image_input_price"` - ImageOutputPrice *float64 `json:"image_output_price"` - PerRequestPrice *float64 `json:"per_request_price"` - Intervals []PricingInterval `json:"intervals"` - TimePricing *ChannelTimePricing `json:"time_pricing,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` + ID int64 `json:"id,omitempty"` + ChannelID int64 `json:"channel_id,omitempty"` + Platform string `json:"platform"` // 所属平台(anthropic/openai/gemini/...) + Models []string `json:"models"` + BillingMode BillingMode `json:"billing_mode"` + InputPrice *float64 `json:"input_price"` + OutputPrice *float64 `json:"output_price"` + CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + CacheReadPrice *float64 `json:"cache_read_price"` + FastMultiplier *float64 `json:"fast_multiplier"` + FlexMultiplier *float64 `json:"flex_multiplier"` + ImageInputPrice *float64 `json:"image_input_price"` + ImageOutputPrice *float64 `json:"image_output_price"` + PerRequestPrice *float64 `json:"per_request_price"` + Intervals []PricingInterval `json:"intervals"` + TimePricing *ChannelTimePricing `json:"time_pricing,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` } // ChannelTimePricing 渠道模型定价的分时倍率配置。 @@ -131,6 +132,7 @@ type PricingInterval struct { InputPrice *float64 `json:"input_price"` OutputPrice *float64 `json:"output_price"` CacheWritePrice *float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` CacheReadPrice *float64 `json:"cache_read_price"` InputMultiplier *float64 `json:"input_multiplier"` OutputMultiplier *float64 `json:"output_multiplier"` @@ -377,6 +379,7 @@ func validateIntervalPrices(iv *PricingInterval, idx int) error { {"input_price", iv.InputPrice}, {"output_price", iv.OutputPrice}, {"cache_write_price", iv.CacheWritePrice}, + {"cache_write_1h_price", iv.CacheWrite1hPrice}, {"cache_read_price", iv.CacheReadPrice}, {"per_request_price", iv.PerRequestPrice}, } diff --git a/backend/internal/service/channel_available.go b/backend/internal/service/channel_available.go index 6face6e1db..08b02eb423 100644 --- a/backend/internal/service/channel_available.go +++ b/backend/internal/service/channel_available.go @@ -141,13 +141,13 @@ func pricingNeedsFallback(p *ChannelModelPricing) bool { return true } if p.InputPrice != nil || p.OutputPrice != nil || - p.CacheWritePrice != nil || p.CacheReadPrice != nil || + p.CacheWritePrice != nil || p.CacheWrite1hPrice != nil || p.CacheReadPrice != nil || p.ImageOutputPrice != nil || p.PerRequestPrice != nil { return false } for _, iv := range p.Intervals { if iv.InputPrice != nil || iv.OutputPrice != nil || - iv.CacheWritePrice != nil || iv.CacheReadPrice != nil || + iv.CacheWritePrice != nil || iv.CacheWrite1hPrice != nil || iv.CacheReadPrice != nil || iv.PerRequestPrice != nil { return false } @@ -188,12 +188,13 @@ func synthesizePricingFromLiteLLM(lp *LiteLLMModelPricing, existing *ChannelMode } } return &ChannelModelPricing{ - BillingMode: mode, - InputPrice: nonZeroPtr(lp.InputCostPerToken), - OutputPrice: nonZeroPtr(lp.OutputCostPerToken), - CacheWritePrice: nonZeroPtr(lp.CacheCreationInputTokenCost), - CacheReadPrice: nonZeroPtr(lp.CacheReadInputTokenCost), - ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken), + BillingMode: mode, + InputPrice: nonZeroPtr(lp.InputCostPerToken), + OutputPrice: nonZeroPtr(lp.OutputCostPerToken), + CacheWritePrice: nonZeroPtr(lp.CacheCreationInputTokenCost), + CacheWrite1hPrice: nonZeroPtr(lp.CacheCreationInputTokenCostAbove1hr), + CacheReadPrice: nonZeroPtr(lp.CacheReadInputTokenCost), + ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken), } } diff --git a/backend/internal/service/channel_service.go b/backend/internal/service/channel_service.go index b3fe7dc63a..688b46e7e9 100644 --- a/backend/internal/service/channel_service.go +++ b/backend/internal/service/channel_service.go @@ -725,6 +725,7 @@ func checkPricesNotNegative(p ChannelModelPricing) error { {"input_price", p.InputPrice}, {"output_price", p.OutputPrice}, {"cache_write_price", p.CacheWritePrice}, + {"cache_write_1h_price", p.CacheWrite1hPrice}, {"cache_read_price", p.CacheReadPrice}, {"image_input_price", p.ImageInputPrice}, {"image_output_price", p.ImageOutputPrice}, @@ -752,7 +753,7 @@ func checkPricesNotNegative(p ChannelModelPricing) error { func checkIntervalsHavePrices(p ChannelModelPricing) error { for _, iv := range p.Intervals { if iv.InputPrice == nil && iv.OutputPrice == nil && - iv.CacheWritePrice == nil && iv.CacheReadPrice == nil && + iv.CacheWritePrice == nil && iv.CacheWrite1hPrice == nil && iv.CacheReadPrice == nil && iv.PerRequestPrice == nil && iv.InputMultiplier == nil && iv.OutputMultiplier == nil && iv.CacheWriteMultiplier == nil && iv.CacheReadMultiplier == nil { diff --git a/backend/internal/service/model_pricing_resolver.go b/backend/internal/service/model_pricing_resolver.go index 747c333dfa..1a5382e7c3 100644 --- a/backend/internal/service/model_pricing_resolver.go +++ b/backend/internal/service/model_pricing_resolver.go @@ -234,6 +234,17 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin } applyChannelTokenPriceOverrides(resolved.BasePricing, chPricing) + if chPricing.CacheWrite1hPrice != nil { + resolved.SupportsCacheBreakdown = true + resolved.BasePricing.SupportsCacheBreakdown = true + } + for i := range chPricing.Intervals { + if chPricing.Intervals[i].CacheWrite1hPrice != nil { + resolved.SupportsCacheBreakdown = true + resolved.BasePricing.SupportsCacheBreakdown = true + break + } + } resolved.BasePricing.FastMultiplier = chPricing.FastMultiplier resolved.BasePricing.FlexMultiplier = chPricing.FlexMultiplier // 渠道定价覆盖一切:显式配置则用配置值,未配置则归零(不回退到 LiteLLM) @@ -276,7 +287,7 @@ func filterValidIntervals(intervals []PricingInterval) []PricingInterval { var valid []PricingInterval for _, iv := range intervals { if iv.InputPrice != nil || iv.OutputPrice != nil || - iv.CacheWritePrice != nil || iv.CacheReadPrice != nil || + iv.CacheWritePrice != nil || iv.CacheWrite1hPrice != nil || iv.CacheReadPrice != nil || iv.PerRequestPrice != nil || iv.InputMultiplier != nil || iv.OutputMultiplier != nil || iv.CacheWriteMultiplier != nil || iv.CacheReadMultiplier != nil { @@ -336,13 +347,19 @@ func intervalToModelPricing(iv *PricingInterval, base *ModelPricing, chPricing * pricing.CacheCreationPricePerToken = *iv.CacheWritePrice pricing.CacheCreationPriceExplicit = true pricing.CacheCreation5mPrice = *iv.CacheWritePrice - pricing.CacheCreation1hPrice = *iv.CacheWritePrice + if iv.CacheWrite1hPrice == nil { + pricing.CacheCreation1hPrice = *iv.CacheWritePrice + } } else if iv.CacheWriteMultiplier != nil { pricing.CacheCreationPricePerToken = applyMultiplier(pricing.CacheCreationPricePerToken, iv.CacheWriteMultiplier) pricing.CacheCreationPricePerTokenPriority = applyMultiplier(pricing.CacheCreationPricePerTokenPriority, iv.CacheWriteMultiplier) pricing.CacheCreation5mPrice = applyMultiplier(pricing.CacheCreation5mPrice, iv.CacheWriteMultiplier) pricing.CacheCreation1hPrice = applyMultiplier(pricing.CacheCreation1hPrice, iv.CacheWriteMultiplier) } + if iv.CacheWrite1hPrice != nil { + pricing.CacheCreation1hPrice = *iv.CacheWrite1hPrice + pricing.SupportsCacheBreakdown = true + } if iv.CacheReadPrice != nil { pricing.CacheReadPricePerTokenPriority = channelTierOverridePrice(pricing.CacheReadPricePerToken, pricing.CacheReadPricePerTokenPriority, *iv.CacheReadPrice) pricing.CacheReadPricePerToken = *iv.CacheReadPrice diff --git a/backend/migrations/232_channel_cache_write_1h_pricing.sql b/backend/migrations/232_channel_cache_write_1h_pricing.sql new file mode 100644 index 0000000000..92da91603e --- /dev/null +++ b/backend/migrations/232_channel_cache_write_1h_pricing.sql @@ -0,0 +1,20 @@ +ALTER TABLE channel_model_pricing + ADD COLUMN IF NOT EXISTS cache_write_1h_price NUMERIC(20,12); + +ALTER TABLE channel_pricing_intervals + ADD COLUMN IF NOT EXISTS cache_write_1h_price NUMERIC(20,12); + +ALTER TABLE channel_account_stats_model_pricing + ADD COLUMN IF NOT EXISTS cache_write_1h_price NUMERIC(20,12); + +ALTER TABLE channel_account_stats_pricing_intervals + ADD COLUMN IF NOT EXISTS cache_write_1h_price NUMERIC(20,12); + +COMMENT ON COLUMN channel_model_pricing.cache_write_1h_price IS + '1h cache write price per token; NULL preserves legacy cache_write_price behavior'; +COMMENT ON COLUMN channel_pricing_intervals.cache_write_1h_price IS + 'Interval-specific 1h cache write price per token'; +COMMENT ON COLUMN channel_account_stats_model_pricing.cache_write_1h_price IS + '1h cache write price per token for account stats pricing'; +COMMENT ON COLUMN channel_account_stats_pricing_intervals.cache_write_1h_price IS + 'Interval-specific 1h cache write price per token for account stats pricing'; diff --git a/frontend/src/api/admin/channels.ts b/frontend/src/api/admin/channels.ts index 94967e3692..3b355bc85f 100644 --- a/frontend/src/api/admin/channels.ts +++ b/frontend/src/api/admin/channels.ts @@ -16,6 +16,7 @@ export interface PricingInterval { input_price: number | null output_price: number | null cache_write_price: number | null + cache_write_1h_price?: number | null cache_read_price: number | null input_multiplier: number | null output_multiplier: number | null @@ -45,6 +46,7 @@ export interface ChannelModelPricing { input_price: number | null output_price: number | null cache_write_price: number | null + cache_write_1h_price?: number | null cache_read_price: number | null fast_multiplier?: number | null flex_multiplier?: number | null @@ -173,6 +175,7 @@ export interface ModelDefaultPricing { input_price?: number // per-token price output_price?: number cache_write_price?: number + cache_write_1h_price?: number | null cache_read_price?: number image_input_price?: number image_output_price?: number diff --git a/frontend/src/api/channels.ts b/frontend/src/api/channels.ts index 46e350809f..6a88cc5ed8 100644 --- a/frontend/src/api/channels.ts +++ b/frontend/src/api/channels.ts @@ -29,6 +29,7 @@ export interface UserPricingInterval { input_price: number | null output_price: number | null cache_write_price: number | null + cache_write_1h_price?: number | null cache_read_price: number | null per_request_price: number | null } @@ -38,6 +39,7 @@ export interface UserSupportedModelPricing { input_price: number | null output_price: number | null cache_write_price: number | null + cache_write_1h_price?: number | null cache_read_price: number | null image_input_price: number | null image_output_price: number | null diff --git a/frontend/src/components/account/AccountStatusIndicator.vue b/frontend/src/components/account/AccountStatusIndicator.vue index 4130ecd8ec..64bcfbf594 100644 --- a/frontend/src/components/account/AccountStatusIndicator.vue +++ b/frontend/src/components/account/AccountStatusIndicator.vue @@ -224,6 +224,7 @@ const activeModelStatuses = computed(() => { const formatScopeName = (scope: string): string => { const aliases: Record = { // Claude 系列 + 'claude-fable-5-1': 'CFable51', 'claude-fable-5': 'CFable5', 'claude-opus-4-6': 'COpus46', 'claude-opus-4-6-thinking': 'COpus46T', diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 12c309cf4c..554295f97e 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -859,6 +859,7 @@ const antigravity3ImageUsageFromAPI = computed(() => // Claude from API (all Claude model variants) const antigravityClaudeUsageFromAPI = computed(() => getAntigravityUsageFromAPI([ + 'claude-fable-5-1', 'claude-fable-5', 'claude-sonnet-4-5', 'claude-opus-4-5-thinking', 'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-opus-4-6-thinking', diff --git a/frontend/src/components/admin/channel/IntervalRow.vue b/frontend/src/components/admin/channel/IntervalRow.vue index 0166e510a6..0f83dd7f53 100644 --- a/frontend/src/components/admin/channel/IntervalRow.vue +++ b/frontend/src/components/admin/channel/IntervalRow.vue @@ -3,7 +3,7 @@ :class="isEmpty ? 'border-red-400 bg-red-50 dark:border-red-500 dark:bg-red-950/20' : 'border-gray-200 bg-white dark:border-dark-500 dark:bg-dark-700'">