From b8d52fad34f70a2b3ac69dfb5c0ec8724a19099c Mon Sep 17 00:00:00 2001 From: haruka <1628615876@qq.com> Date: Sun, 20 Sep 2026 16:41:49 +0800 Subject: [PATCH] feat(billing): support configurable reasoning effort multipliers --- .../internal/handler/admin/channel_handler.go | 153 ++++++----- .../handler/admin/channel_handler_test.go | 45 ++-- .../handler/available_channel_handler.go | 44 ++-- .../handler/available_channel_handler_test.go | 4 +- ...oning_effort_migration_integration_test.go | 85 ++++++ ...nnel_reasoning_pricing_integration_test.go | 181 +++++++++++++ .../channel_repo_account_stats_pricing.go | 20 +- .../repository/channel_repo_pricing.go | 53 +++- .../channel_repo_pricing_reasoning_test.go | 175 +++++++++++++ .../channel_repo_pricing_time_test.go | 16 +- .../internal/service/account_stats_pricing.go | 6 +- .../service/account_stats_pricing_test.go | 4 +- backend/internal/service/billing_service.go | 107 ++++---- .../internal/service/billing_service_test.go | 3 +- .../billing_token_cost_request_test.go | 11 +- backend/internal/service/channel.go | 44 ++-- backend/internal/service/channel_available.go | 36 +-- .../service/channel_reasoning_pricing_test.go | 42 +++ backend/internal/service/channel_service.go | 22 ++ backend/internal/service/channel_test.go | 5 +- .../gateway_compat_reasoning_pricing_test.go | 115 ++++++++ .../gateway_forward_as_chat_completions.go | 15 +- .../service/gateway_forward_as_responses.go | 9 +- .../service/gateway_reasoning_pricing_test.go | 70 +++++ .../internal/service/model_plaza_service.go | 22 +- .../service/model_plaza_service_test.go | 19 +- .../service/model_pricing_resolver.go | 5 +- .../openai_gateway_chat_completions_raw.go | 8 +- ...penai_gateway_chat_completions_raw_test.go | 3 + .../service/openai_gateway_request_body.go | 4 +- .../openai_gateway_service_hotpath_test.go | 8 +- .../internal/service/openai_gateway_usage.go | 6 +- .../service/reasoning_effort_billing_test.go | 245 ++++++++++++++++++ ...9_channel_reasoning_effort_multipliers.sql | 56 ++++ ...oning_effort_multipliers_migration_test.go | 25 ++ docs/screenshots/reasoning-effort-pricing.png | Bin 0 -> 191361 bytes frontend/src/api/admin/channels.ts | 4 +- frontend/src/api/channels.ts | 2 +- .../admin/channel/PricingEntryCard.vue | 65 ++++- .../PricingEntryCard.timePricing.spec.ts | 83 +++++- .../admin/channel/__tests__/types.spec.ts | 28 ++ .../src/components/admin/channel/types.ts | 27 +- .../modelPlaza/PlazaModelPricingTable.vue | 19 +- .../__tests__/PlazaModelPricingTable.spec.ts | 22 +- frontend/src/constants/channel.ts | 4 + .../src/i18n/locales/en/admin/channels.ts | 11 +- frontend/src/i18n/locales/en/dashboard.ts | 4 +- .../src/i18n/locales/zh/admin/channels.ts | 11 +- frontend/src/i18n/locales/zh/dashboard.ts | 4 +- frontend/src/views/admin/ChannelsView.vue | 32 ++- frontend/src/views/admin/GroupsView.vue | 22 ++ .../__tests__/GroupsView.duplicate.spec.ts | 59 +++++ 52 files changed, 1709 insertions(+), 354 deletions(-) create mode 100644 backend/internal/repository/channel_reasoning_effort_migration_integration_test.go create mode 100644 backend/internal/repository/channel_reasoning_pricing_integration_test.go create mode 100644 backend/internal/repository/channel_repo_pricing_reasoning_test.go create mode 100644 backend/internal/service/channel_reasoning_pricing_test.go create mode 100644 backend/internal/service/gateway_compat_reasoning_pricing_test.go create mode 100644 backend/internal/service/gateway_reasoning_pricing_test.go create mode 100644 backend/internal/service/reasoning_effort_billing_test.go create mode 100644 backend/migrations/239_channel_reasoning_effort_multipliers.sql create mode 100644 backend/migrations/channel_reasoning_effort_multipliers_migration_test.go create mode 100644 docs/screenshots/reasoning-effort-pricing.png diff --git a/backend/internal/handler/admin/channel_handler.go b/backend/internal/handler/admin/channel_handler.go index 3b65ac97a8..376e08f88e 100644 --- a/backend/internal/handler/admin/channel_handler.go +++ b/backend/internal/handler/admin/channel_handler.go @@ -57,22 +57,22 @@ 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"` - 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"` - MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_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"` + ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"` + 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 { @@ -130,23 +130,23 @@ 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"` - CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - FastMultiplier *float64 `json:"fast_multiplier"` - FlexMultiplier *float64 `json:"flex_multiplier"` - MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_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"` + ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"` + 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 { @@ -260,23 +260,23 @@ 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, - CacheWrite1hPrice: p.CacheWrite1hPrice, - CacheReadPrice: p.CacheReadPrice, - FastMultiplier: p.FastMultiplier, - FlexMultiplier: p.FlexMultiplier, - MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier, - 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, + ReasoningEffortMultipliers: p.ReasoningEffortMultipliers, + ImageInputPrice: p.ImageInputPrice, + ImageOutputPrice: p.ImageOutputPrice, + PerRequestPrice: p.PerRequestPrice, + Intervals: intervals, + TimePricing: timePricingToResponse(p.TimePricing), } } @@ -353,29 +353,28 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult SortOrder: iv.SortOrder, }) } - var fastMultiplier, flexMultiplier, maxReasoningEffortMultiplier *float64 + var fastMultiplier, flexMultiplier *float64 if allowChannelMultipliers { fastMultiplier = r.FastMultiplier flexMultiplier = r.FlexMultiplier - maxReasoningEffortMultiplier = r.MaxReasoningEffortMultiplier } result = append(result, service.ChannelModelPricing{ - 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, - MaxReasoningEffortMultiplier: maxReasoningEffortMultiplier, - 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, + ReasoningEffortMultipliers: r.ReasoningEffortMultipliers, + ImageInputPrice: r.ImageInputPrice, + ImageOutputPrice: r.ImageOutputPrice, + PerRequestPrice: r.PerRequestPrice, + Intervals: intervals, + TimePricing: timePricingRequestToService(r.TimePricing), }) } return result @@ -620,15 +619,15 @@ func (h *ChannelHandler) GetModelDefaultPricing(c *gin.Context) { } response.Success(c, gin.H{ - "found": true, - "input_price": pricing.InputPricePerToken, - "output_price": pricing.OutputPricePerToken, - "cache_write_price": cacheWritePrice, - "cache_write_1h_price": cacheWrite1hPrice, - "cache_read_price": pricing.CacheReadPricePerToken, - "max_reasoning_effort_multiplier": pricing.MaxReasoningEffortMultiplier, - "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, + "reasoning_effort_multipliers": pricing.ReasoningEffortMultipliers, + "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 d735c54707..5678a9499b 100644 --- a/backend/internal/handler/admin/channel_handler_test.go +++ b/backend/internal/handler/admin/channel_handler_test.go @@ -449,15 +449,14 @@ func TestPricingRequestToService_TimePricingNil(t *testing.T) { require.Nil(t, got[0].TimePricing) } -// 账号成本统计规则不支持倍率:allowChannelMultipliers=false 时必须丢弃, -// 避免渠道倍率意外污染账号成本口径。 +// Fast/Flex 和区间倍率只用于渠道售价;思考等级倍率可在账号统计规则中独立配置。 func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) { req := channelModelPricingRequest{ - Models: []string{"gpt-5"}, - BillingMode: "token", - FastMultiplier: float64Ptr(2.5), - FlexMultiplier: float64Ptr(0.5), - MaxReasoningEffortMultiplier: float64Ptr(3), + Models: []string{"gpt-5"}, + BillingMode: "token", + FastMultiplier: float64Ptr(2.5), + FlexMultiplier: float64Ptr(0.5), + ReasoningEffortMultipliers: map[string]float64{"high": 1.5, "max": 3}, Intervals: []pricingIntervalRequest{{ MinTokens: 272000, InputMultiplier: float64Ptr(2), @@ -470,7 +469,7 @@ func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) { allowed := pricingRequestToService([]channelModelPricingRequest{req}, true) require.Equal(t, float64Ptr(2.5), allowed[0].FastMultiplier) require.Equal(t, float64Ptr(0.5), allowed[0].FlexMultiplier) - require.Equal(t, float64Ptr(3), allowed[0].MaxReasoningEffortMultiplier) + require.Equal(t, req.ReasoningEffortMultipliers, allowed[0].ReasoningEffortMultipliers) require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].InputMultiplier) require.Equal(t, float64Ptr(1.5), allowed[0].Intervals[0].OutputMultiplier) require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheWriteMultiplier) @@ -479,7 +478,7 @@ func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) { dropped := pricingRequestToService([]channelModelPricingRequest{req}, false) require.Nil(t, dropped[0].FastMultiplier) require.Nil(t, dropped[0].FlexMultiplier) - require.Nil(t, dropped[0].MaxReasoningEffortMultiplier) + require.Equal(t, req.ReasoningEffortMultipliers, dropped[0].ReasoningEffortMultipliers) require.Nil(t, dropped[0].Intervals[0].InputMultiplier) require.Nil(t, dropped[0].Intervals[0].OutputMultiplier) require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier) @@ -511,6 +510,23 @@ func TestPricingToResponse_TimePricingNil(t *testing.T) { require.Nil(t, got.TimePricing) } +func TestPricingRequestAndResponse_ReasoningEffortMultipliers(t *testing.T) { + var req channelModelPricingRequest + require.NoError(t, json.Unmarshal([]byte(`{"models":["custom-model"],"reasoning_effort_multipliers":{"none":0.5,"high":1.5,"max":3}}`), &req)) + pricing := pricingRequestToService([]channelModelPricingRequest{req}, true) + got := pricingToResponse(&pricing[0]) + require.Equal(t, map[string]float64{"none": 0.5, "high": 1.5, "max": 3}, got.ReasoningEffortMultipliers) + data, err := json.Marshal(got) + require.NoError(t, err) + require.Contains(t, string(data), `"reasoning_effort_multipliers":{"high":1.5,"max":3,"none":0.5}`) + require.NotContains(t, string(data), "max_reasoning_effort_multiplier") + + req = channelModelPricingRequest{} + require.NoError(t, json.Unmarshal([]byte(`{"models":["custom-model"],"reasoning_effort_multipliers":{}}`), &req)) + pricing = pricingRequestToService([]channelModelPricingRequest{req}, true) + require.Empty(t, pricing[0].ReasoningEffortMultipliers) +} + // --------------------------------------------------------------------------- // 3. SyncPricingModels handler // --------------------------------------------------------------------------- @@ -583,10 +599,10 @@ func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) { 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"` - MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"` + Found bool `json:"found"` + CacheWritePrice float64 `json:"cache_write_price"` + CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` + ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers"` } `json:"data"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) @@ -594,8 +610,7 @@ func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) { 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) - require.NotNil(t, body.Data.MaxReasoningEffortMultiplier) - require.Equal(t, 3.0, *body.Data.MaxReasoningEffortMultiplier) + require.Empty(t, body.Data.ReasoningEffortMultipliers) } func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) { diff --git a/backend/internal/handler/available_channel_handler.go b/backend/internal/handler/available_channel_handler.go index ae01687bdb..46e871a0c3 100644 --- a/backend/internal/handler/available_channel_handler.go +++ b/backend/internal/handler/available_channel_handler.go @@ -66,17 +66,17 @@ 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"` - CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier,omitempty"` - 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"` + ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers,omitempty"` + 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 等前端不渲染的字段)。 @@ -332,16 +332,16 @@ func toUserPricing(p *service.ChannelModelPricing) *userSupportedModelPricing { billingMode = string(service.BillingModeToken) } return &userSupportedModelPricing{ - BillingMode: billingMode, - InputPrice: p.InputPrice, - OutputPrice: p.OutputPrice, - CacheWritePrice: p.CacheWritePrice, - CacheWrite1hPrice: p.CacheWrite1hPrice, - CacheReadPrice: p.CacheReadPrice, - MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier, - 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, + ReasoningEffortMultipliers: p.ReasoningEffortMultipliers, + ImageInputPrice: p.ImageInputPrice, + ImageOutputPrice: p.ImageOutputPrice, + PerRequestPrice: p.PerRequestPrice, + Intervals: intervals, } } diff --git a/backend/internal/handler/available_channel_handler_test.go b/backend/internal/handler/available_channel_handler_test.go index 325f10133b..41b70022e7 100644 --- a/backend/internal/handler/available_channel_handler_test.go +++ b/backend/internal/handler/available_channel_handler_test.go @@ -118,7 +118,8 @@ func TestUserAvailableChannel_FieldWhitelist(t *testing.T) { cacheWriteMultiplier := 2.0 cacheReadMultiplier := 2.0 pricing := toUserPricing(&service.ChannelModelPricing{ - BillingMode: service.BillingModeToken, + BillingMode: service.BillingModeToken, + ReasoningEffortMultipliers: map[string]float64{"high": 1.5, "max": 3}, Intervals: []service.PricingInterval{ { ID: 7, MinTokens: 0, MaxTokens: nil, SortOrder: 3, @@ -128,6 +129,7 @@ func TestUserAvailableChannel_FieldWhitelist(t *testing.T) { }, }) require.NotNil(t, pricing) + require.Equal(t, map[string]float64{"high": 1.5, "max": 3}, pricing.ReasoningEffortMultipliers) require.Len(t, pricing.Intervals, 1) rawIv, err := json.Marshal(pricing.Intervals[0]) require.NoError(t, err) diff --git a/backend/internal/repository/channel_reasoning_effort_migration_integration_test.go b/backend/internal/repository/channel_reasoning_effort_migration_integration_test.go new file mode 100644 index 0000000000..ff92dd2fc8 --- /dev/null +++ b/backend/internal/repository/channel_reasoning_effort_migration_integration_test.go @@ -0,0 +1,85 @@ +//go:build integration + +package repository + +import ( + "context" + "testing" + + dbmigrations "github.com/Wei-Shaw/sub2api/migrations" + "github.com/stretchr/testify/require" +) + +func TestMigration239ReasoningEffortMultipliers(t *testing.T) { + tx := testTx(t) + ctx := context.Background() + _, err := tx.ExecContext(ctx, `ALTER TABLE channel_model_pricing DROP COLUMN reasoning_effort_multipliers`) + require.NoError(t, err) + _, err = tx.ExecContext(ctx, `ALTER TABLE channel_account_stats_model_pricing DROP COLUMN reasoning_effort_multipliers`) + require.NoError(t, err) + + var channelID, configuredID, unsetID int64 + require.NoError(t, tx.QueryRowContext(ctx, `INSERT INTO channels (name) VALUES ('migration-reasoning-multipliers') RETURNING id`).Scan(&channelID)) + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO channel_model_pricing (channel_id, models, max_reasoning_effort_multiplier) +VALUES ($1, '["custom-model"]', 2.5) RETURNING id`, channelID).Scan(&configuredID)) + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO channel_model_pricing (channel_id, models) +VALUES ($1, '["claude-fable-5-1"]') RETURNING id`, channelID).Scan(&unsetID)) + + migrationSQL, err := dbmigrations.FS.ReadFile("239_channel_reasoning_effort_multipliers.sql") + require.NoError(t, err) + _, err = tx.ExecContext(ctx, string(migrationSQL)) + require.NoError(t, err) + var configured, unset string + require.NoError(t, tx.QueryRowContext(ctx, `SELECT reasoning_effort_multipliers::text FROM channel_model_pricing WHERE id = $1`, configuredID).Scan(&configured)) + require.JSONEq(t, `{"max":2.5}`, configured) + require.NoError(t, tx.QueryRowContext(ctx, `SELECT reasoning_effort_multipliers::text FROM channel_model_pricing WHERE id = $1`, unsetID).Scan(&unset)) + require.JSONEq(t, `{}`, unset) + + // Changing or clearing the generic configuration must survive a migration replay. + for _, current := range []string{`{"high":1.5,"max":4}`, `{"low":0.75}`, `{}`} { + _, err = tx.ExecContext(ctx, `UPDATE channel_model_pricing SET reasoning_effort_multipliers = $1 WHERE id = $2`, current, configuredID) + require.NoError(t, err) + _, err = tx.ExecContext(ctx, string(migrationSQL)) + require.NoError(t, err) + require.NoError(t, tx.QueryRowContext(ctx, `SELECT reasoning_effort_multipliers::text FROM channel_model_pricing WHERE id = $1`, configuredID).Scan(&configured)) + require.JSONEq(t, current, configured) + } + + // Account statistics use the same empty-by-default shape. + var ruleID int64 + require.NoError(t, tx.QueryRowContext(ctx, `INSERT INTO channel_account_stats_pricing_rules (channel_id) VALUES ($1) RETURNING id`, channelID).Scan(&ruleID)) + var accountStatsDefault string + require.NoError(t, tx.QueryRowContext(ctx, `INSERT INTO channel_account_stats_model_pricing (rule_id) VALUES ($1) RETURNING reasoning_effort_multipliers::text`, ruleID).Scan(&accountStatsDefault)) + require.JSONEq(t, `{}`, accountStatsDefault) +} + +func TestMigration239GroupReasoningEffortMultipliers(t *testing.T) { + tx := testTx(t) + ctx := context.Background() + var groupID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO groups (name, platform, model_pricing) +VALUES ('migration-group-reasoning', 'anthropic', '[ + {"models":["custom-model"],"input_price":2,"max_reasoning_effort_multiplier":2.5}, + {"models":["existing-map"],"max_reasoning_effort_multiplier":3,"reasoning_effort_multipliers":{"high":1.5,"max":4}}, + {"models":["cleared-map"],"max_reasoning_effort_multiplier":3,"reasoning_effort_multipliers":{}}, + {"models":["claude-fable-5-1"],"max_reasoning_effort_multiplier":null} +]'::jsonb) RETURNING id`).Scan(&groupID)) + + migrationSQL, err := dbmigrations.FS.ReadFile("239_channel_reasoning_effort_multipliers.sql") + require.NoError(t, err) + for range 2 { + _, err = tx.ExecContext(ctx, string(migrationSQL)) + require.NoError(t, err) + var actual string + require.NoError(t, tx.QueryRowContext(ctx, `SELECT model_pricing::text FROM groups WHERE id = $1`, groupID).Scan(&actual)) + require.JSONEq(t, `[ + {"models":["custom-model"],"input_price":2,"reasoning_effort_multipliers":{"max":2.5}}, + {"models":["existing-map"],"reasoning_effort_multipliers":{"high":1.5,"max":4}}, + {"models":["cleared-map"],"reasoning_effort_multipliers":{}}, + {"models":["claude-fable-5-1"]} + ]`, actual) + } +} diff --git a/backend/internal/repository/channel_reasoning_pricing_integration_test.go b/backend/internal/repository/channel_reasoning_pricing_integration_test.go new file mode 100644 index 0000000000..325bdba2b4 --- /dev/null +++ b/backend/internal/repository/channel_reasoning_pricing_integration_test.go @@ -0,0 +1,181 @@ +//go:build integration + +package repository + +import ( + "context" + "math" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/require" +) + +func newReasoningPricingIntegrationChannel(t *testing.T, channel *service.Channel) *channelRepository { + t.Helper() + repo := &channelRepository{db: integrationDB} + channel.Name = t.Name() + channel.Status = service.StatusActive + channel.BillingModelSource = service.BillingModelSourceRequested + require.NoError(t, repo.Create(context.Background(), channel)) + // Channel writes manage their own transactions, so clean up committed fixtures. + t.Cleanup(func() { + require.NoError(t, repo.Delete(context.Background(), channel.ID)) + }) + return repo +} + +func requireReasoningPricingIntegrationMaps(t *testing.T, pricing []service.ChannelModelPricing, expected map[string]map[string]float64) { + t.Helper() + require.Len(t, pricing, len(expected)) + for _, entry := range pricing { + require.Len(t, entry.Models, 1) + multipliers, ok := expected[entry.Models[0]] + require.True(t, ok, "unexpected model %s", entry.Models[0]) + if len(multipliers) == 0 { + require.Empty(t, entry.ReasoningEffortMultipliers, entry.Models[0]) + } else { + require.Equal(t, multipliers, entry.ReasoningEffortMultipliers, entry.Models[0]) + } + } +} + +func TestChannelReasoningPricingCreateUpdateReplace(t *testing.T) { + ctx := context.Background() + channel := &service.Channel{} + repo := newReasoningPricingIntegrationChannel(t, channel) + allLevels := map[string]float64{"none": 0.5, "minimal": 0.75, "low": 0.9, "medium": 1.25, "high": 1.5, "xhigh": 2, "max": 3} + pricing := service.ChannelModelPricing{ + ChannelID: channel.ID, Platform: service.PlatformOpenAI, Models: []string{"custom-reasoning-model"}, + ReasoningEffortMultipliers: allLevels, + } + require.NoError(t, repo.CreateModelPricing(ctx, &pricing)) + require.NotZero(t, pricing.ID) + assertStored := func(expected map[string]map[string]float64) { + t.Helper() + listed, err := repo.ListModelPricing(ctx, channel.ID) + require.NoError(t, err) + requireReasoningPricingIntegrationMaps(t, listed, expected) + // Cache refresh uses this batch loading path instead of ListModelPricing. + batch, err := repo.batchLoadModelPricing(ctx, []int64{channel.ID}) + require.NoError(t, err) + requireReasoningPricingIntegrationMaps(t, batch[channel.ID], expected) + } + assertStored(map[string]map[string]float64{"custom-reasoning-model": allLevels}) + + pricing.ReasoningEffortMultipliers = map[string]float64{"high": 2.25} + require.NoError(t, repo.UpdateModelPricing(ctx, &pricing)) + assertStored(map[string]map[string]float64{"custom-reasoning-model": {"high": 2.25}}) + + pricing.ReasoningEffortMultipliers = nil + require.NoError(t, repo.UpdateModelPricing(ctx, &pricing)) + assertStored(map[string]map[string]float64{"custom-reasoning-model": nil}) + var stored string + require.NoError(t, integrationDB.QueryRowContext(ctx, + `SELECT reasoning_effort_multipliers::text FROM channel_model_pricing WHERE id = $1`, pricing.ID).Scan(&stored)) + require.JSONEq(t, `{}`, stored) + + replacement := []service.ChannelModelPricing{ + {Platform: service.PlatformOpenAI, Models: []string{"custom-reasoning-model"}, ReasoningEffortMultipliers: map[string]float64{"low": 0.8, "max": 4}}, + {Platform: service.PlatformAnthropic, Models: []string{"claude-fable-5-1"}, ReasoningEffortMultipliers: map[string]float64{}}, + } + require.NoError(t, repo.ReplaceModelPricing(ctx, channel.ID, replacement)) + expected := map[string]map[string]float64{"custom-reasoning-model": {"low": 0.8, "max": 4}, "claude-fable-5-1": nil} + assertStored(expected) + + // A serialization failure after deleting the old rows must roll back the replacement. + require.Error(t, repo.ReplaceModelPricing(ctx, channel.ID, []service.ChannelModelPricing{{ + Models: []string{"invalid"}, ReasoningEffortMultipliers: map[string]float64{"max": math.Inf(1)}, + }})) + assertStored(expected) +} + +func TestAccountStatsReasoningPricingRoundTrip(t *testing.T) { + ctx := context.Background() + channel := &service.Channel{ + ModelPricing: []service.ChannelModelPricing{{ + Platform: service.PlatformOpenAI, Models: []string{"custom-reasoning-model"}, + ReasoningEffortMultipliers: map[string]float64{"max": 4}, + }}, + AccountStatsPricingRules: []service.AccountStatsPricingRule{{ + Name: "independent account statistics", GroupIDs: []int64{}, AccountIDs: []int64{}, + Pricing: []service.ChannelModelPricing{ + {Platform: service.PlatformOpenAI, Models: []string{"custom-reasoning-model"}, ReasoningEffortMultipliers: map[string]float64{"none": 0.5, "high": 1.25, "max": 2.5}}, + {Platform: service.PlatformAnthropic, Models: []string{"claude-fable-5-1"}}, + }, + }}, + } + repo := newReasoningPricingIntegrationChannel(t, channel) + assertStored := func(multipliers map[string]float64) *service.Channel { + t.Helper() + loaded, err := repo.GetByID(ctx, channel.ID) + require.NoError(t, err) + require.Len(t, loaded.AccountStatsPricingRules, 1) + expected := map[string]map[string]float64{"custom-reasoning-model": multipliers, "claude-fable-5-1": nil} + requireReasoningPricingIntegrationMaps(t, loaded.AccountStatsPricingRules[0].Pricing, expected) + batch, err := repo.batchLoadAccountStatsPricingRules(ctx, []int64{channel.ID}) + require.NoError(t, err) + require.Len(t, batch[channel.ID], 1) + requireReasoningPricingIntegrationMaps(t, batch[channel.ID][0].Pricing, expected) + // Custom statistics prices must not alter the user-facing channel prices. + requireReasoningPricingIntegrationMaps(t, loaded.ModelPricing, map[string]map[string]float64{"custom-reasoning-model": {"max": 4}}) + return loaded + } + loaded := assertStored(map[string]float64{"none": 0.5, "high": 1.25, "max": 2.5}) + for _, multipliers := range []map[string]float64{{"low": 0.75, "max": 1.5}, nil, {"high": 2}, {}} { + loaded.AccountStatsPricingRules[0].Pricing[0].ReasoningEffortMultipliers = multipliers + require.NoError(t, repo.Update(ctx, loaded)) + loaded = assertStored(multipliers) + } + var stored string + require.NoError(t, integrationDB.QueryRowContext(ctx, + `SELECT reasoning_effort_multipliers::text FROM channel_account_stats_model_pricing WHERE id = $1`, loaded.AccountStatsPricingRules[0].Pricing[0].ID).Scan(&stored)) + require.JSONEq(t, `{}`, stored) +} + +func TestGroupReasoningPricingRoundTripAndBilling(t *testing.T) { + ctx := context.Background() + tx := testEntTx(t) + repo := newGroupRepositoryWithSQL(tx.Client(), tx) + inputPrice, outputPrice := 1e-6, 2e-6 + allLevels := map[string]float64{"none": 0.5, "minimal": 0.75, "low": 0.9, "medium": 1.25, "high": 1.5, "xhigh": 2, "max": 3} + group := &service.Group{ + Name: t.Name(), Platform: service.PlatformAnthropic, RateMultiplier: 0.8, + Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, + ModelPricing: []service.ChannelModelPricing{{ + Platform: service.PlatformAnthropic, Models: []string{"claude-fable-5-1"}, BillingMode: service.BillingModeToken, + InputPrice: &inputPrice, OutputPrice: &outputPrice, ReasoningEffortMultipliers: allLevels, + }}, + } + require.NoError(t, repo.Create(ctx, group)) + billing := service.NewBillingService(&config.Config{}, nil) + resolver := service.NewModelPricingResolver(nil, billing) + assertStoredAndBilled := func(expected map[string]float64) *service.Group { + t.Helper() + loaded, err := repo.GetByID(ctx, group.ID) + require.NoError(t, err) + requireReasoningPricingIntegrationMaps(t, loaded.ModelPricing, map[string]map[string]float64{"claude-fable-5-1": expected}) + for _, effort := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", ""} { + multiplier := 1.0 + if configured, ok := expected[effort]; ok { + multiplier = configured + } + cost, err := billing.CalculateTokenCostForRequest(service.TokenCostRequest{ + Ctx: ctx, Model: "claude-fable-5-1", Group: loaded, Resolver: resolver, + Tokens: service.UsageTokens{InputTokens: 1000, OutputTokens: 200}, + RateMultiplier: loaded.RateMultiplier, ReasoningEffort: effort, + }) + require.NoError(t, err) + require.InDelta(t, 0.0014*multiplier, cost.TotalCost, 1e-12, "effort=%q", effort) + require.InDelta(t, 0.0014*multiplier*0.8, cost.ActualCost, 1e-12, "effort=%q", effort) + } + return loaded + } + loaded := assertStoredAndBilled(allLevels) + for _, multipliers := range []map[string]float64{{"high": 2.25, "max": 4}, nil, {"high": 1.75}, {}} { + loaded.ModelPricing[0].ReasoningEffortMultipliers = multipliers + require.NoError(t, repo.Update(ctx, loaded)) + loaded = assertStoredAndBilled(multipliers) + } +} diff --git a/backend/internal/repository/channel_repo_account_stats_pricing.go b/backend/internal/repository/channel_repo_account_stats_pricing.go index 22922c689e..f2a4786ce1 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_write_1h_price, cache_read_price, image_output_price, per_request_price, created_at, updated_at + cache_write_price, cache_write_1h_price, cache_read_price, reasoning_effort_multipliers, 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), ) @@ -81,16 +81,22 @@ func (r *channelRepository) batchLoadAccountStatsModelPricing(ctx context.Contex var p service.ChannelModelPricing var ruleID int64 var modelsJSON []byte + var reasoningEffortMultipliersJSON []byte if err := rows.Scan( &p.ID, &ruleID, &p.Platform, &modelsJSON, &p.BillingMode, &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice, - &p.ImageOutputPrice, &p.PerRequestPrice, &p.CreatedAt, &p.UpdatedAt, + &reasoningEffortMultipliersJSON, &p.ImageOutputPrice, &p.PerRequestPrice, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan account stats model pricing: %w", err) } if err := json.Unmarshal(modelsJSON, &p.Models); err != nil { p.Models = []string{} } + multipliers, err := unmarshalReasoningEffortMultipliers(reasoningEffortMultipliersJSON) + if err != nil { + return nil, err + } + p.ReasoningEffortMultipliers = multipliers pricingMap[ruleID] = append(pricingMap[ruleID], p) } if err := rows.Err(); err != nil { @@ -172,17 +178,21 @@ func createAccountStatsModelPricingTx(ctx context.Context, tx *sql.Tx, ruleID in if err != nil { return fmt.Errorf("marshal models: %w", err) } + reasoningEffortMultipliersJSON, err := marshalReasoningEffortMultipliers(pricing.ReasoningEffortMultipliers) + if err != nil { + return err + } billingMode := pricing.BillingMode if billingMode == "" { billingMode = service.BillingModeToken } 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_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`, + `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, reasoning_effort_multipliers, image_output_price, per_request_price) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at`, ruleID, platform, modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, - pricing.ImageOutputPrice, pricing.PerRequestPrice, + reasoningEffortMultipliersJSON, pricing.ImageOutputPrice, pricing.PerRequestPrice, ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt) if err != nil { return fmt.Errorf("insert account stats model pricing: %w", err) diff --git a/backend/internal/repository/channel_repo_pricing.go b/backend/internal/repository/channel_repo_pricing.go index 8053d4453f..e800c223dc 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_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_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, reasoning_effort_multipliers, 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 { @@ -51,6 +51,10 @@ func (r *channelRepository) UpdateModelPricing(ctx context.Context, pricing *ser if err != nil { return fmt.Errorf("marshal models: %w", err) } + reasoningEffortMultipliersJSON, err := marshalReasoningEffortMultipliers(pricing.ReasoningEffortMultipliers) + if err != nil { + return err + } timePricingJSON, err := marshalChannelTimePricing(pricing.TimePricing) if err != nil { return err @@ -61,10 +65,10 @@ 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_write_1h_price = $6, cache_read_price = $7, fast_multiplier = $8, flex_multiplier = $9, max_reasoning_effort_multiplier = $10, image_input_price = $11, image_output_price = $12, per_request_price = $13, time_pricing = $14, platform = $15, updated_at = NOW() + 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, reasoning_effort_multipliers = $10, image_input_price = $11, image_output_price = $12, per_request_price = $13, time_pricing = $14, platform = $15, updated_at = NOW() WHERE id = $16`, modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, - pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, + pricing.FastMultiplier, pricing.FlexMultiplier, reasoningEffortMultipliersJSON, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, timePricingJSON, pricing.Platform, pricing.ID, ) if err != nil { @@ -96,7 +100,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_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_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, reasoning_effort_multipliers, 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), ) @@ -177,10 +181,11 @@ func scanModelPricingRows(rows *sql.Rows) ([]service.ChannelModelPricing, []int6 var p service.ChannelModelPricing var modelsJSON []byte var timePricingJSON []byte + var reasoningEffortMultipliersJSON []byte if err := rows.Scan( &p.ID, &p.ChannelID, &p.Platform, &modelsJSON, &p.BillingMode, &p.InputPrice, &p.OutputPrice, &p.CacheWritePrice, &p.CacheWrite1hPrice, &p.CacheReadPrice, - &p.FastMultiplier, &p.FlexMultiplier, &p.MaxReasoningEffortMultiplier, + &p.FastMultiplier, &p.FlexMultiplier, &reasoningEffortMultipliersJSON, &p.ImageInputPrice, &p.ImageOutputPrice, &p.PerRequestPrice, &timePricingJSON, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, nil, fmt.Errorf("scan model pricing: %w", err) @@ -188,6 +193,11 @@ func scanModelPricingRows(rows *sql.Rows) ([]service.ChannelModelPricing, []int6 if err := json.Unmarshal(modelsJSON, &p.Models); err != nil { p.Models = []string{} } + multipliers, err := unmarshalReasoningEffortMultipliers(reasoningEffortMultipliersJSON) + if err != nil { + return nil, nil, err + } + p.ReasoningEffortMultipliers = multipliers timePricing, err := unmarshalChannelTimePricing(timePricingJSON) if err != nil { return nil, nil, err @@ -234,6 +244,10 @@ func createModelPricingExec(ctx context.Context, exec dbExec, pricing *service.C if err != nil { return fmt.Errorf("marshal models: %w", err) } + reasoningEffortMultipliersJSON, err := marshalReasoningEffortMultipliers(pricing.ReasoningEffortMultipliers) + if err != nil { + return err + } timePricingJSON, err := marshalChannelTimePricing(pricing.TimePricing) if err != nil { return err @@ -247,11 +261,11 @@ 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_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing) + `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, reasoning_effort_multipliers, 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, $16) RETURNING id, created_at, updated_at`, pricing.ChannelID, platform, modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice, - pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, + pricing.FastMultiplier, pricing.FlexMultiplier, reasoningEffortMultipliersJSON, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice, timePricingJSON, ).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt) if err != nil { @@ -268,6 +282,31 @@ func createModelPricingExec(ctx context.Context, exec dbExec, pricing *service.C return nil } +func marshalReasoningEffortMultipliers(multipliers map[string]float64) (string, error) { + if len(multipliers) == 0 { + return "{}", nil + } + data, err := json.Marshal(multipliers) + if err != nil { + return "", fmt.Errorf("marshal reasoning effort multipliers: %w", err) + } + return string(data), nil +} + +func unmarshalReasoningEffortMultipliers(data []byte) (map[string]float64, error) { + multipliers := make(map[string]float64) + if len(data) == 0 { + return multipliers, nil + } + if err := json.Unmarshal(data, &multipliers); err != nil { + return nil, fmt.Errorf("unmarshal reasoning effort multipliers: %w", err) + } + if multipliers == nil { + multipliers = make(map[string]float64) + } + return multipliers, nil +} + func marshalChannelTimePricing(config *service.ChannelTimePricing) (any, error) { if config == nil || len(config.Periods) == 0 { return nil, nil diff --git a/backend/internal/repository/channel_repo_pricing_reasoning_test.go b/backend/internal/repository/channel_repo_pricing_reasoning_test.go new file mode 100644 index 0000000000..2cf412c670 --- /dev/null +++ b/backend/internal/repository/channel_repo_pricing_reasoning_test.go @@ -0,0 +1,175 @@ +//go:build unit + +package repository + +import ( + "context" + "database/sql/driver" + "math" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/lib/pq" + "github.com/stretchr/testify/require" +) + +type capturedReasoningMultipliersJSON struct { + value string +} + +func (c *capturedReasoningMultipliersJSON) Match(value driver.Value) bool { + data, ok := value.(string) + if ok { + c.value = data + } + return ok +} + +func reasoningPricingRow(multipliers any) *sqlmock.Rows { + return sqlmock.NewRows(channelModelPricingTimePricingColumns).AddRow( + int64(11), int64(7), "openai", `["custom-model"]`, service.BillingModeToken, + nil, nil, nil, nil, nil, nil, nil, multipliers, nil, nil, nil, nil, + time.Time{}, time.Time{}, + ) +} + +func TestChannelReasoningEffortMultipliersRoundTrip(t *testing.T) { + tests := []struct { + name string + multipliers map[string]float64 + wantJSON string + }{ + {"custom levels", map[string]float64{"low": 0.75, "high": 1.5, "max": 4}, `{"low":0.75,"high":1.5,"max":4}`}, + {"nil clears configuration", nil, `{}`}, + {"empty clears configuration", map[string]float64{}, `{}`}, + } + for _, tt := range tests { + for _, operation := range []string{"create", "update", "replace"} { + t.Run(tt.name+"/"+operation, func(t *testing.T) { + repo, mock := newChannelModelPricingTimePricingRepo(t) + ctx := context.Background() + pricing := &service.ChannelModelPricing{ + ID: 11, ChannelID: 7, Platform: "openai", Models: []string{"custom-model"}, + ReasoningEffortMultipliers: tt.multipliers, + } + stored := &capturedReasoningMultipliersJSON{} + if operation == "update" { + mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*reasoning_effort_multipliers = \$10.*WHERE id = \$16`). + WithArgs([]byte(`["custom-model"]`), service.BillingModeToken, + nil, nil, nil, nil, nil, nil, nil, stored, nil, nil, nil, nil, "openai", int64(11)). + WillReturnResult(sqlmock.NewResult(0, 1)) + require.NoError(t, repo.UpdateModelPricing(ctx, pricing)) + } else { + if operation == "replace" { + mock.ExpectBegin() + mock.ExpectExec(`DELETE FROM channel_model_pricing WHERE channel_id = \$1`). + WithArgs(int64(7)).WillReturnResult(sqlmock.NewResult(0, 1)) + } + mock.ExpectQuery(`INSERT INTO channel_model_pricing .*reasoning_effort_multipliers`). + WithArgs(int64(7), "openai", []byte(`["custom-model"]`), service.BillingModeToken, + nil, nil, nil, nil, nil, nil, nil, stored, nil, nil, nil, nil). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{})) + if operation == "replace" { + mock.ExpectCommit() + require.NoError(t, repo.ReplaceModelPricing(ctx, 7, []service.ChannelModelPricing{*pricing})) + } else { + require.NoError(t, repo.CreateModelPricing(ctx, pricing)) + } + } + require.JSONEq(t, tt.wantJSON, stored.value) + + mock.ExpectQuery(`(?s)SELECT .*reasoning_effort_multipliers.*FROM channel_model_pricing.*channel_id = \$1`). + WithArgs(int64(7)).WillReturnRows(reasoningPricingRow(stored.value)) + expectEmptyModelPricingIntervals(mock) + loaded, err := repo.ListModelPricing(ctx, 7) + require.NoError(t, err) + require.Len(t, loaded, 1) + if len(tt.multipliers) == 0 { + require.Empty(t, loaded[0].ReasoningEffortMultipliers) + } else { + require.Equal(t, tt.multipliers, loaded[0].ReasoningEffortMultipliers) + } + require.NoError(t, mock.ExpectationsWereMet()) + }) + } + } +} + +func TestChannelReasoningEffortMultipliersBatchLoad(t *testing.T) { + repo, mock := newChannelModelPricingTimePricingRepo(t) + mock.ExpectQuery(`(?s)SELECT .*reasoning_effort_multipliers.*FROM channel_model_pricing.*channel_id = ANY`). + WithArgs(pq.Array([]int64{7})).WillReturnRows(reasoningPricingRow(`{"medium":1.25,"high":2}`)) + expectEmptyModelPricingIntervals(mock) + pricing, err := repo.batchLoadModelPricing(context.Background(), []int64{7}) + require.NoError(t, err) + require.Len(t, pricing[7], 1) + require.Equal(t, map[string]float64{"medium": 1.25, "high": 2}, pricing[7][0].ReasoningEffortMultipliers) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestChannelReasoningEffortMultipliersInvalidJSON(t *testing.T) { + for _, data := range []string{`{"high":`, `{"high":"2"}`, `[]`} { + t.Run(data, func(t *testing.T) { + repo, mock := newChannelModelPricingTimePricingRepo(t) + mock.ExpectQuery(`SELECT .*reasoning_effort_multipliers`).WithArgs(int64(7)).WillReturnRows(reasoningPricingRow(data)) + _, err := repo.ListModelPricing(context.Background(), 7) + require.ErrorContains(t, err, "unmarshal reasoning effort multipliers") + require.NoError(t, mock.ExpectationsWereMet()) + }) + } + for _, operation := range []string{"create", "update"} { + t.Run(operation+" invalid number", func(t *testing.T) { + repo, mock := newChannelModelPricingTimePricingRepo(t) + pricing := &service.ChannelModelPricing{ReasoningEffortMultipliers: map[string]float64{"max": math.NaN()}} + var err error + if operation == "create" { + err = repo.CreateModelPricing(context.Background(), pricing) + } else { + err = repo.UpdateModelPricing(context.Background(), pricing) + } + require.ErrorContains(t, err, "marshal reasoning effort multipliers") + require.NoError(t, mock.ExpectationsWereMet()) + }) + } +} + +func TestAccountStatsReasoningEffortMultipliersRoundTrip(t *testing.T) { + repo, mock := newChannelModelPricingTimePricingRepo(t) + ctx := context.Background() + pricing := &service.ChannelModelPricing{ + Platform: "anthropic", Models: []string{"custom-model"}, + ReasoningEffortMultipliers: map[string]float64{"medium": 0.5, "high": 2}, + } + stored := &capturedReasoningMultipliersJSON{} + mock.ExpectBegin() + tx, err := repo.db.BeginTx(ctx, nil) + require.NoError(t, err) + mock.ExpectQuery(`INSERT INTO channel_account_stats_model_pricing .*reasoning_effort_multipliers`). + WithArgs(int64(9), "anthropic", []byte(`["custom-model"]`), service.BillingModeToken, + nil, nil, nil, nil, nil, stored, nil, nil). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{})) + require.NoError(t, createAccountStatsModelPricingTx(ctx, tx, 9, pricing)) + mock.ExpectCommit() + require.NoError(t, tx.Commit()) + require.JSONEq(t, `{"medium":0.5,"high":2}`, stored.value) + + columns := []string{ + "id", "rule_id", "platform", "models", "billing_mode", "input_price", "output_price", + "cache_write_price", "cache_write_1h_price", "cache_read_price", "reasoning_effort_multipliers", + "image_output_price", "per_request_price", "created_at", "updated_at", + } + mock.ExpectQuery(`(?s)SELECT .*reasoning_effort_multipliers.*FROM channel_account_stats_model_pricing`). + WithArgs(pq.Array([]int64{9})).WillReturnRows(sqlmock.NewRows(columns).AddRow( + int64(11), int64(9), "anthropic", `["custom-model"]`, service.BillingModeToken, + nil, nil, nil, nil, nil, stored.value, nil, nil, time.Time{}, time.Time{}, + )) + mock.ExpectQuery(`(?s)SELECT .*FROM channel_account_stats_pricing_intervals`). + WithArgs(pq.Array([]int64{11})).WillReturnRows(sqlmock.NewRows([]string{"id"})) + loaded, err := repo.batchLoadAccountStatsModelPricing(ctx, []int64{9}) + require.NoError(t, err) + require.Len(t, loaded[9], 1) + require.Equal(t, pricing.ReasoningEffortMultipliers, loaded[9][0].ReasoningEffortMultipliers) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/backend/internal/repository/channel_repo_pricing_time_test.go b/backend/internal/repository/channel_repo_pricing_time_test.go index c22630abcf..a58cb4e5d9 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_write_1h_price", "cache_read_price", "fast_multiplier", "flex_multiplier", "max_reasoning_effort_multiplier", "image_input_price", "image_output_price", + "cache_write_price", "cache_write_1h_price", "cache_read_price", "fast_multiplier", "flex_multiplier", "reasoning_effort_multipliers", "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, 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_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_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, reasoning_effort_multipliers, 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, 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{})) @@ -121,7 +121,7 @@ func TestChannelModelPricingTimePricingCreateAndUpdateRoundTrip(t *testing.T) { mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$13, time_pricing = \$14, platform = \$15.*WHERE id = \$16`). WithArgs( []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, 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_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, max_reasoning_effort_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, reasoning_effort_multipliers, 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, nil, nil, ). WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{})) @@ -169,7 +169,7 @@ func TestChannelModelPricingTimePricingCreateAndUpdateWriteNullWhenDisabled(t *t mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$13, time_pricing = \$14, platform = \$15.*WHERE id = \$16`). WithArgs( []byte(`["gpt-5"]`), service.BillingModeToken, - nil, nil, 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 f27d5bb367..4a738ccf77 100644 --- a/backend/internal/service/account_stats_pricing.go +++ b/backend/internal/service/account_stats_pricing.go @@ -19,7 +19,7 @@ import ( // totalCost 是本次请求的客户计费(倍率前),用于优先级 2。 // serviceTier 是最终参与用户计费的 OpenAI 服务层级,用于优先级 3。 // pricingAt 与本次客户计费使用同一时刻,避免跨峰谷请求的成本与售价错位。 -// reasoningEffort 是最终转发等级;Fable 5.1 max 默认按 3 倍额度消耗。 +// reasoningEffort 是最终转发等级;按账号统计定价中配置的等级倍率计费。 func resolveAccountStatsCost( ctx context.Context, channelService *ChannelService, @@ -116,7 +116,7 @@ func tryCustomRules( } cost := calculateStatsCost(pricing, tokens, requestCount) if cost != nil { - *cost *= maxReasoningEffortBillingMultiplier(model, reasoningEffort, nil) + *cost *= reasoningEffortBillingMultiplier(reasoningEffort, pricing.ReasoningEffortMultipliers) } return cost } @@ -192,7 +192,7 @@ func calculateStatsCost(pricing *ChannelModelPricing, tokens UsageTokens, reques return nil } switch pricing.BillingMode { - case BillingModePerRequest, BillingModeImage: + case BillingModePerRequest, BillingModeImage, BillingModeVideo: return calculatePerRequestStatsCost(pricing, requestCount) default: return calculateTokenStatsCost(pricing, tokens) diff --git a/backend/internal/service/account_stats_pricing_test.go b/backend/internal/service/account_stats_pricing_test.go index e53810b279..622fc73f3c 100644 --- a/backend/internal/service/account_stats_pricing_test.go +++ b/backend/internal/service/account_stats_pricing_test.go @@ -478,7 +478,7 @@ func TestTryModelFilePricing_Success(t *testing.T) { require.InDelta(t, 0.2, *result, 1e-12) } -func TestTryModelFilePricing_Fable51MaxEffortUsesTripleQuota(t *testing.T) { +func TestTryModelFilePricing_Fable51HasNoImplicitReasoningMultiplier(t *testing.T) { bs := newTestBillingServiceWithPrices(map[string]*ModelPricing{ "claude-fable-5-1": {InputPricePerToken: 0.001}, }) @@ -487,7 +487,7 @@ func TestTryModelFilePricing_Fable51MaxEffortUsesTripleQuota(t *testing.T) { max := tryModelFilePricing(bs, "claude-fable-5-1", tokens, "", time.Time{}, "max") require.NotNil(t, standard) require.NotNil(t, max) - require.InDelta(t, *standard*3, *max, 1e-12) + require.Equal(t, *standard, *max) } func TestTryModelFilePricing_AppliesLongContextPricing(t *testing.T) { diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 55e4f08d9b..29161ce338 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "maps" "math" "strings" "sync" @@ -93,29 +94,29 @@ type BillingCache interface { // ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致) type ModelPricing struct { - InputPricePerToken float64 // 每token输入价格 (USD) - InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD) - ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken - ImageCacheReadPricePerToken float64 // 图片缓存输入价格;无独立价格时沿用缓存读取价 - OutputPricePerToken float64 // 每token输出价格 (USD) - OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD) - CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD) - CacheCreationPricePerTokenPriority float64 // priority service tier 下缓存创建每token价格 (USD) - CacheCreationPriceExplicit bool // 是否由渠道/区间定价显式设定(为 true 时即使 == 0 也不回退) - CacheReadPricePerToken float64 // 缓存读取每token价格 (USD) - CacheReadPricePerTokenPriority float64 // priority service tier 下缓存读取每token价格 (USD) - FastMultiplier *float64 // 渠道显式 Fast/priority 倍率;nil 时沿用模型目录行为 - FlexMultiplier *float64 // 渠道显式 Flex 倍率;nil 时沿用默认行为 - MaxReasoningEffortMultiplier *float64 // max 推理等级的额度/计费倍率;nil 时沿用模型默认行为 - CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD) - CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD) - SupportsCacheBreakdown bool // 是否支持详细的缓存分类 - LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格 - LongContextThresholdInclusive bool // 达到阈值即应用(xAI);默认保持严格大于以兼容既有模型 - LongContextInputMultiplier float64 // 长上下文整次会话输入倍率 - LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率 - ImageOutputPricePerToken float64 // 图片输出 token 价格 (USD) - ImageOutputPriceExplicit bool // 是否由渠道定价显式设定(为 true 时即使 == 0 也不回退) + InputPricePerToken float64 // 每token输入价格 (USD) + InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD) + ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken + ImageCacheReadPricePerToken float64 // 图片缓存输入价格;无独立价格时沿用缓存读取价 + OutputPricePerToken float64 // 每token输出价格 (USD) + OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD) + CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD) + CacheCreationPricePerTokenPriority float64 // priority service tier 下缓存创建每token价格 (USD) + CacheCreationPriceExplicit bool // 是否由渠道/区间定价显式设定(为 true 时即使 == 0 也不回退) + CacheReadPricePerToken float64 // 缓存读取每token价格 (USD) + CacheReadPricePerTokenPriority float64 // priority service tier 下缓存读取每token价格 (USD) + FastMultiplier *float64 // 渠道显式 Fast/priority 倍率;nil 时沿用模型目录行为 + FlexMultiplier *float64 // 渠道显式 Flex 倍率;nil 时沿用默认行为 + ReasoningEffortMultipliers map[string]float64 // 最终转发的推理等级对应的计费倍率;未配置的等级按 1 倍计费 + CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD) + CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD) + SupportsCacheBreakdown bool // 是否支持详细的缓存分类 + LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格 + LongContextThresholdInclusive bool // 达到阈值即应用(xAI);默认保持严格大于以兼容既有模型 + LongContextInputMultiplier float64 // 长上下文整次会话输入倍率 + LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率 + ImageOutputPricePerToken float64 // 图片输出 token 价格 (USD) + ImageOutputPriceExplicit bool // 是否由渠道定价显式设定(为 true 时即使 == 0 也不回退) } func normalizeBillingServiceTier(serviceTier string) string { @@ -217,8 +218,6 @@ func applyCostBreakdownMultiplier(cost *CostBreakdown, multiplier float64) { cost.ActualCost *= multiplier } -const claudeFable51MaxReasoningEffortMultiplier = 3.0 - func isClaudeFable51Model(model string) bool { model = strings.ToLower(strings.TrimSpace(model)) for _, marker := range []string{"fable-5-1", "fable-5.1", "fable5.1", "fable51"} { @@ -232,25 +231,19 @@ func isClaudeFable51Model(model string) bool { return false } -func defaultMaxReasoningEffortMultiplier(model string) *float64 { - if !isClaudeFable51Model(model) { - return nil +func reasoningEffortBillingMultiplier(effort string, multipliers map[string]float64) float64 { + effort = strings.ToLower(strings.TrimSpace(effort)) + if effort != "none" { + effort = NormalizeMaxReasoningEffort(effort) } - multiplier := claudeFable51MaxReasoningEffortMultiplier - return &multiplier -} - -func maxReasoningEffortBillingMultiplier(model, effort string, pricing *ModelPricing) float64 { - if NormalizeMaxReasoningEffort(effort) != "max" { + if effort == "" { return 1 } - if pricing != nil && pricing.MaxReasoningEffortMultiplier != nil && *pricing.MaxReasoningEffortMultiplier > 0 { - return *pricing.MaxReasoningEffortMultiplier + multiplier := multipliers[effort] + if multiplier <= 0 || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) { + return 1 } - if multiplier := defaultMaxReasoningEffortMultiplier(model); multiplier != nil { - return *multiplier - } - return 1 + return multiplier } func resolvedChannelTimeMultiplier(resolved *ResolvedPricing, at time.Time) float64 { @@ -1311,9 +1304,7 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing applyChannelTokenPriceOverrides(pricing, channelPricing) pricing.FastMultiplier = channelPricing.FastMultiplier pricing.FlexMultiplier = channelPricing.FlexMultiplier - if channelPricing.MaxReasoningEffortMultiplier != nil { - pricing.MaxReasoningEffortMultiplier = channelPricing.MaxReasoningEffortMultiplier - } + pricing.ReasoningEffortMultipliers = maps.Clone(channelPricing.ReasoningEffortMultipliers) if channelPricing.ImageOutputPrice != nil { pricing.ImageOutputPricePerToken = *channelPricing.ImageOutputPrice } else { @@ -1386,7 +1377,7 @@ type CostInput struct { RateMultiplier float64 PricingAt time.Time // 渠道分时定价使用的计费时刻 ServiceTier string // "priority","flex","" 等 - ReasoningEffort string // 最终转发的推理等级;max 可触发模型/渠道倍率 + ReasoningEffort string // 最终转发的推理等级,按匹配定价中配置的等级倍率计费 Resolver *ModelPricingResolver // 定价解析器 Resolved *ResolvedPricing // 可选:预解析的定价结果(避免重复 Resolve 调用) LongContextBillingEnabled *bool @@ -1401,18 +1392,13 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown, if input.LongContextBillingEnabled != nil { applyLongContextBilling = *input.LongContextBillingEnabled } - breakdown, err := s.calculateCostInternalWithPolicy( - input.Model, - input.Tokens, - input.RateMultiplier, - input.ServiceTier, - nil, - applyLongContextBilling, - ) - if err == nil { - applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(input.Model, input.ReasoningEffort, nil)) + pricing, err := s.GetModelPricing(input.Model) + if err != nil { + return nil, err } - return breakdown, err + breakdown := s.computeTokenBreakdown(pricing, input.Tokens, input.RateMultiplier, input.ServiceTier, applyLongContextBilling) + applyCostBreakdownMultiplier(breakdown, reasoningEffortBillingMultiplier(input.ReasoningEffort, pricing.ReasoningEffortMultipliers)) + return breakdown, nil } // 优先使用预解析结果,避免重复 Resolve 调用 @@ -1435,6 +1421,9 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown, switch resolved.Mode { case BillingModePerRequest, BillingModeImage, BillingModeVideo: breakdown, err = s.calculatePerRequestCost(resolved, input) + if err == nil && resolved.channelPricing != nil { + applyCostBreakdownMultiplier(breakdown, reasoningEffortBillingMultiplier(input.ReasoningEffort, resolved.channelPricing.ReasoningEffortMultipliers)) + } default: // BillingModeToken breakdown, err = s.calculateTokenCost(resolved, input) } @@ -1498,7 +1487,7 @@ func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input Cos breakdown := s.computeTokenBreakdown(pricing, input.Tokens, input.RateMultiplier, input.ServiceTier, applyLongCtx) applyCostBreakdownMultiplier(breakdown, resolvedChannelTimeMultiplier(resolved, input.PricingAt)) - applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(input.Model, input.ReasoningEffort, pricing)) + applyCostBreakdownMultiplier(breakdown, reasoningEffortBillingMultiplier(input.ReasoningEffort, pricing.ReasoningEffortMultipliers)) return breakdown, nil } @@ -1790,17 +1779,13 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing } normalized := normalizeKnownOpenAICodexModel(model) isGPT56 := isOpenAIGPT56Model(normalized) - needsMaxReasoningEffortMultiplier := isClaudeFable51Model(model) && pricing.MaxReasoningEffortMultiplier == nil needsCacheCreationPolicy := isGPT56 && !pricing.CacheCreationPriceExplicit && (pricing.CacheCreationPricePerToken <= 0 || (pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0)) fastRatio := openAIModelFastPricingRatio(normalized) - if !needsCacheCreationPolicy && fastRatio <= 0 && !needsMaxReasoningEffortMultiplier { + if !needsCacheCreationPolicy && fastRatio <= 0 { return pricing } cloned := *pricing - if needsMaxReasoningEffortMultiplier { - cloned.MaxReasoningEffortMultiplier = defaultMaxReasoningEffortMultiplier(model) - } if isGPT56 && !cloned.CacheCreationPriceExplicit { if cloned.CacheCreationPricePerToken <= 0 { cloned.CacheCreationPricePerToken = cloned.InputPricePerToken * 1.25 diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 0a68e07ad7..59f20e0762 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -1807,8 +1807,7 @@ func TestGetModelPricing_Fable51FallbackPricing(t *testing.T) { 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) - require.NotNil(t, pricing.MaxReasoningEffortMultiplier) - require.Equal(t, 3.0, *pricing.MaxReasoningEffortMultiplier) + require.Empty(t, pricing.ReasoningEffortMultipliers) } func TestGetModelPricingWithChannel_CacheReadPriceAffectsPriority(t *testing.T) { diff --git a/backend/internal/service/billing_token_cost_request_test.go b/backend/internal/service/billing_token_cost_request_test.go index 23fae53557..fe4390b227 100644 --- a/backend/internal/service/billing_token_cost_request_test.go +++ b/backend/internal/service/billing_token_cost_request_test.go @@ -217,7 +217,7 @@ func TestCalculateTokenCostForRequest_NoResolverFallsBackToCatalog(t *testing.T) require.Equal(t, want, got) } -func TestCalculateTokenCostForRequest_Fable51MaxEffortUsesDefaultMultiplier(t *testing.T) { +func TestCalculateTokenCostForRequest_Fable51HasNoImplicitReasoningMultiplier(t *testing.T) { bs := NewBillingService(&config.Config{}, nil) tokens := UsageTokens{InputTokens: 1000, OutputTokens: 10} standard, err := bs.CalculateTokenCostForRequest(TokenCostRequest{ @@ -228,18 +228,15 @@ func TestCalculateTokenCostForRequest_Fable51MaxEffortUsesDefaultMultiplier(t *t Model: "claude-fable-5-1", Tokens: tokens, RateMultiplier: 1, ReasoningEffort: "max", }) require.NoError(t, err) - require.InDelta(t, standard.TotalCost*3, max.TotalCost, 1e-12) - require.InDelta(t, standard.ActualCost*3, max.ActualCost, 1e-12) - require.InDelta(t, standard.InputCost*3, max.InputCost, 1e-12) - require.InDelta(t, standard.OutputCost*3, max.OutputCost, 1e-12) + require.Equal(t, standard, max) } -func TestCalculateTokenCostForRequest_ChannelOverridesFable51MaxEffortMultiplier(t *testing.T) { +func TestCalculateTokenCostForRequest_ChannelConfiguresReasoningEffortMultiplier(t *testing.T) { configured := 1.5 bs, resolver := newTokenCostTestEnv(t, PlatformAnthropic, []ChannelModelPricing{{ Platform: PlatformAnthropic, Models: []string{"claude-fable-5-1"}, BillingMode: BillingModeToken, InputPrice: testPtrFloat64(10e-6), OutputPrice: testPtrFloat64(50e-6), - MaxReasoningEffortMultiplier: &configured, + ReasoningEffortMultipliers: map[string]float64{"max": configured}, }}, nil) group := &Group{ID: 100, Platform: PlatformAnthropic} gid := group.ID diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index f9fea3bcc9..43dfbc2407 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "maps" "sort" "strings" "time" @@ -87,26 +88,26 @@ 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"` - CacheWrite1hPrice *float64 `json:"cache_write_1h_price"` - CacheReadPrice *float64 `json:"cache_read_price"` - FastMultiplier *float64 `json:"fast_multiplier"` - FlexMultiplier *float64 `json:"flex_multiplier"` - MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_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"` + ReasoningEffortMultipliers map[string]float64 `json:"reasoning_effort_multipliers,omitempty"` + 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 渠道模型定价的分时倍率配置。 @@ -208,9 +209,10 @@ func (p *ChannelModelPricing) GetTierByLabel(label string) *PricingInterval { return nil } -// Clone 返回 ChannelModelPricing 的拷贝(切片独立,指针字段共享,调用方只读安全) +// Clone 返回 ChannelModelPricing 的拷贝(切片和映射独立,价格指针字段共享,调用方只读安全) func (p ChannelModelPricing) Clone() ChannelModelPricing { cp := p + cp.ReasoningEffortMultipliers = maps.Clone(p.ReasoningEffortMultipliers) if p.Models != nil { cp.Models = make([]string, len(p.Models)) copy(cp.Models, p.Models) diff --git a/backend/internal/service/channel_available.go b/backend/internal/service/channel_available.go index 46eb15d6bb..262a384821 100644 --- a/backend/internal/service/channel_available.go +++ b/backend/internal/service/channel_available.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "maps" "sort" "strings" ) @@ -117,7 +118,7 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel, // 1. Pricing == nil(渠道完全没声明该模型的定价条目) // 2. Pricing 非 nil 但所有价格字段为空(admin UI 建了条目但没填价格) // -// 当 pricingService 为 nil(测试场景),跳过价格回落,但仍补充内置模型倍率。 +// 当 pricingService 为 nil(测试场景),跳过价格回落。 // 可用渠道与模型广场共用。 func fillGlobalPricingFallback(pricingService *PricingService, models []SupportedModel) { for i := range models { @@ -126,7 +127,6 @@ func fillGlobalPricingFallback(pricingService *PricingService, models []Supporte models[i].Pricing = synthesizePricingFromLiteLLM(lp, models[i].Pricing) } } - models[i].Pricing = withDefaultMaxReasoningEffortMultiplier(models[i].Pricing, models[i].Name) } } @@ -176,31 +176,31 @@ func synthesizePricingFromLiteLLM(lp *LiteLLMModelPricing, existing *ChannelMode if mode == BillingModeImage || mode == BillingModePerRequest { return &ChannelModelPricing{ - BillingMode: mode, - PerRequestPrice: nonZeroPtr(lp.OutputCostPerImage), - ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken), - InputPrice: nonZeroPtr(lp.InputCostPerToken), - OutputPrice: nonZeroPtr(lp.OutputCostPerToken), - MaxReasoningEffortMultiplier: maxReasoningEffortMultiplierFromPricing(existing), + BillingMode: mode, + PerRequestPrice: nonZeroPtr(lp.OutputCostPerImage), + ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken), + InputPrice: nonZeroPtr(lp.InputCostPerToken), + OutputPrice: nonZeroPtr(lp.OutputCostPerToken), + ReasoningEffortMultipliers: reasoningEffortMultipliersFromPricing(existing), } } return &ChannelModelPricing{ - 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), - MaxReasoningEffortMultiplier: maxReasoningEffortMultiplierFromPricing(existing), + 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), + ReasoningEffortMultipliers: reasoningEffortMultipliersFromPricing(existing), } } -func maxReasoningEffortMultiplierFromPricing(pricing *ChannelModelPricing) *float64 { +func reasoningEffortMultipliersFromPricing(pricing *ChannelModelPricing) map[string]float64 { if pricing == nil { return nil } - return pricing.MaxReasoningEffortMultiplier + return maps.Clone(pricing.ReasoningEffortMultipliers) } func nonZeroPtr(v float64) *float64 { diff --git a/backend/internal/service/channel_reasoning_pricing_test.go b/backend/internal/service/channel_reasoning_pricing_test.go new file mode 100644 index 0000000000..b1b03e3ce5 --- /dev/null +++ b/backend/internal/service/channel_reasoning_pricing_test.go @@ -0,0 +1,42 @@ +//go:build unit + +package service + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidatePricingEntries_ReasoningEffortMultipliers(t *testing.T) { + for _, tc := range []struct { + name string + multipliers map[string]float64 + valid bool + }{ + {"unset", nil, true}, + {"cleared", map[string]float64{}, true}, + {"all levels", map[string]float64{"none": 0.5, "minimal": 1, "low": 1.25, "medium": 1.5, "high": 2, "xhigh": 2.5, "max": 3}, true}, + {"unknown level", map[string]float64{"ultra": 2}, false}, + {"blank level", map[string]float64{"": 2}, false}, + {"noncanonical level", map[string]float64{"MAX": 2}, false}, + {"zero", map[string]float64{"high": 0}, false}, + {"negative", map[string]float64{"high": -1}, false}, + {"nan", map[string]float64{"high": math.NaN()}, false}, + {"infinity", map[string]float64{"high": math.Inf(1)}, false}, + } { + t.Run(tc.name, func(t *testing.T) { + pricing := []ChannelModelPricing{{Models: []string{"custom-model"}, ReasoningEffortMultipliers: tc.multipliers}} + err := validatePricingEntries(pricing) + statsErr := validateAccountStatsPricingRules([]AccountStatsPricingRule{{Pricing: pricing}}) + if tc.valid { + require.NoError(t, err) + require.NoError(t, statsErr) + } else { + require.Error(t, err) + require.Error(t, statsErr) + } + }) + } +} diff --git a/backend/internal/service/channel_service.go b/backend/internal/service/channel_service.go index 0d34f44014..fb59d7f7bb 100644 --- a/backend/internal/service/channel_service.go +++ b/backend/internal/service/channel_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "math" "strings" "sync/atomic" "time" @@ -687,9 +688,30 @@ func validatePricingEntries(pricing []ChannelModelPricing) error { if err := validatePricingBillingMode(pricing); err != nil { return err } + if err := validateReasoningEffortMultipliers(pricing); err != nil { + return err + } return validatePricingTimePricing(pricing) } +func validateReasoningEffortMultipliers(pricing []ChannelModelPricing) error { + for _, p := range pricing { + for effort, multiplier := range p.ReasoningEffortMultipliers { + switch effort { + case "none", "minimal", "low", "medium", "high", "xhigh", "max": + default: + return infraerrors.BadRequest("INVALID_REASONING_EFFORT_MULTIPLIER", + fmt.Sprintf("unsupported reasoning effort %q for models %v", effort, p.Models)) + } + if math.IsNaN(multiplier) || math.IsInf(multiplier, 0) || multiplier <= 0 { + return infraerrors.BadRequest("INVALID_REASONING_EFFORT_MULTIPLIER", + fmt.Sprintf("reasoning_effort_multipliers.%s must be a finite number > 0", effort)) + } + } + } + return nil +} + func validatePricingTimePricing(pricing []ChannelModelPricing) error { for i := range pricing { config := pricing[i].TimePricing diff --git a/backend/internal/service/channel_test.go b/backend/internal/service/channel_test.go index 42c2c46448..973a8cfd08 100644 --- a/backend/internal/service/channel_test.go +++ b/backend/internal/service/channel_test.go @@ -193,7 +193,8 @@ func TestChannelClone_Nil(t *testing.T) { func TestChannelModelPricingClone(t *testing.T) { original := ChannelModelPricing{ - Models: []string{"a", "b"}, + ReasoningEffortMultipliers: map[string]float64{"high": 1.5}, + Models: []string{"a", "b"}, Intervals: []PricingInterval{ {MinTokens: 0, TierLabel: "tier1"}, }, @@ -209,6 +210,8 @@ func TestChannelModelPricingClone(t *testing.T) { } cloned := original.Clone() + cloned.ReasoningEffortMultipliers["high"] = 2 + require.Equal(t, 1.5, original.ReasoningEffortMultipliers["high"]) // Modify clone slices — original unchanged cloned.Models[0] = "hacked" diff --git a/backend/internal/service/gateway_compat_reasoning_pricing_test.go b/backend/internal/service/gateway_compat_reasoning_pricing_test.go new file mode 100644 index 0000000000..d06c623d06 --- /dev/null +++ b/backend/internal/service/gateway_compat_reasoning_pricing_test.go @@ -0,0 +1,115 @@ +//go:build unit + +package service + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestGatewayAnthropicCompatReasoningPricingUsesForwardedEffort(t *testing.T) { + gin.SetMode(gin.TestMode) + endpoints := []struct { + name string + path string + call func(*GatewayService, context.Context, *gin.Context, *Account, []byte) (*ForwardResult, error) + }{ + {"chat_completions", "/v1/chat/completions", func(s *GatewayService, ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) { + return s.ForwardAsChatCompletions(ctx, c, account, body, nil) + }}, + {"responses", "/v1/responses", func(s *GatewayService, ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) { + return s.ForwardAsResponses(ctx, c, account, body, nil) + }}, + } + for _, endpoint := range endpoints { + for _, stream := range []bool{false, true} { + for _, tc := range []struct { + name string + model string + effort string + thinking string + wantEffort string + multiplier float64 + }{ + {"xhigh_converts_to_max", "claude-fable-5-1", "xhigh", "", "max", 3}, + {"native_max", "claude-fable-5-1", "max", "", "max", 3}, + {"missing_effort", "kimi-k3", "", "", "", 1}, + {"disabled_thinking", "kimi-k3", "", "disabled", "", 1}, + {"unforwarded_thinking", "kimi-k3", "", "enabled", "", 1}, + } { + mode := "buffered" + if stream { + mode = "streaming" + } + t.Run(endpoint.name+"/"+mode+"/"+tc.name, func(t *testing.T) { + request := map[string]any{"model": tc.model, "stream": stream} + if endpoint.name == "chat_completions" { + request["messages"] = []map[string]string{{"role": "user", "content": "hello"}} + if tc.effort != "" { + request["reasoning_effort"] = tc.effort + } + } else { + request["input"] = "hello" + if tc.effort != "" { + request["reasoning"] = map[string]string{"effort": tc.effort} + } + } + if tc.thinking != "" { + request["thinking"] = map[string]string{"type": tc.thinking} + } + body, err := json.Marshal(request) + require.NoError(t, err) + upstream := &queuedHTTPUpstreamStub{responses: []*http.Response{{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}}, + Body: io.NopCloser(strings.NewReader(namespaceToolAnthropicStream())), + }}} + svc := &GatewayService{ + cfg: &config.Config{}, httpUpstream: upstream, + tlsFPProfileService: &TLSFingerprintProfileService{}, + } + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest(http.MethodPost, endpoint.path, strings.NewReader(string(body))) + account := &Account{ + ID: 1, Platform: PlatformAnthropic, Type: AccountTypeAPIKey, + Credentials: map[string]any{"api_key": "test-key"}, + } + result, err := endpoint.call(svc, context.Background(), c, account, body) + require.NoError(t, err) + require.Len(t, upstream.requestBodies, 1) + require.Equal(t, tc.wantEffort, gjson.GetBytes(upstream.requestBodies[0], "output_config.effort").String()) + require.Equal(t, tc.wantEffort, optionalStringValue(result.ReasoningEffort)) + if tc.wantEffort == "" { + require.Nil(t, result.ReasoningEffort) + require.False(t, OpenAIBodyHasThinkingEnabled(upstream.requestBodies[0])) + } + + billing := NewBillingService(&config.Config{}, nil) + group := &Group{ID: 1, Platform: PlatformAnthropic, ModelPricing: []ChannelModelPricing{{ + Models: []string{tc.model}, BillingMode: BillingModeToken, + InputPrice: testPtrFloat64(1e-6), OutputPrice: testPtrFloat64(2e-6), + ReasoningEffortMultipliers: map[string]float64{"xhigh": 2, "max": 3, "high": 1.5}, + }}} + cost, err := billing.CalculateTokenCostForRequest(TokenCostRequest{ + Ctx: context.Background(), Model: result.Model, Group: group, + Tokens: UsageTokens{InputTokens: result.Usage.InputTokens, OutputTokens: result.Usage.OutputTokens}, + ReasoningEffort: optionalStringValue(result.ReasoningEffort), RateMultiplier: 1, + Resolver: NewModelPricingResolver(nil, billing), + }) + require.NoError(t, err) + require.InDelta(t, 20e-6*tc.multiplier, cost.TotalCost, 1e-12) + }) + } + } + } +} diff --git a/backend/internal/service/gateway_forward_as_chat_completions.go b/backend/internal/service/gateway_forward_as_chat_completions.go index 32d38cf40d..894e853556 100644 --- a/backend/internal/service/gateway_forward_as_chat_completions.go +++ b/backend/internal/service/gateway_forward_as_chat_completions.go @@ -119,11 +119,15 @@ func (s *GatewayService) ForwardAsChatCompletions( // 10. Build upstream request upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, reqStream) - upstreamReq, _, err := s.buildUpstreamRequest(upstreamCtx, c, account, anthropicBody, token, tokenType, mappedModel, reqStream, shouldMimicClaudeCode) + upstreamReq, forwardedBody, err := s.buildUpstreamRequest(upstreamCtx, c, account, anthropicBody, token, tokenType, mappedModel, reqStream, shouldMimicClaudeCode) releaseUpstreamCtx() if err != nil { return nil, fmt.Errorf("build upstream request: %w", err) } + // Bill the final Anthropic effort after conversion and account normalization. + // For example, OpenAI xhigh is forwarded as output_config.effort=max. + reasoningEffort := NormalizeClaudeOutputEffort(gjson.GetBytes(forwardedBody, "output_config.effort").String()) + reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, forwardedBody, mappedModel) // 11. Send request resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) @@ -173,14 +177,7 @@ func (s *GatewayService) ForwardAsChatCompletions( return nil, fmt.Errorf("upstream error: %d %s", resp.StatusCode, upstreamMsg) } - // 13. Extract reasoning effort from CC request body - reasoningEffort := extractCCReasoningEffortFromBody(body, mappedModel, originalModel) - // 国产模型默认 effort 补充:本路径是客户端 CC 请求 → Anthropic 上游, - // 如果上游是 passback-required 国产模型 (Kimi-anthropic / GLM-anthropic / MiniMax) - // 且客户端在 body 里传了 thinking.type=enabled,补中默认 effort。 - reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel) - - // 14. Handle normal response + // 13. Handle normal response // Read Anthropic SSE → convert to Responses events → convert to CC format var result *ForwardResult var handleErr error diff --git a/backend/internal/service/gateway_forward_as_responses.go b/backend/internal/service/gateway_forward_as_responses.go index 1b97a73961..999b544adc 100644 --- a/backend/internal/service/gateway_forward_as_responses.go +++ b/backend/internal/service/gateway_forward_as_responses.go @@ -85,9 +85,6 @@ func (s *GatewayService) ForwardAsResponses( mappedModel = normalized } } - reasoningEffort := ExtractResponsesReasoningEffortFromBody(body, mappedModel, originalModel) - // 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 mapping 完成之后。 - reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, mappedModel) anthropicReq.Model = mappedModel logger.L().Debug("gateway forward_as_responses: model mapping applied", @@ -132,11 +129,15 @@ func (s *GatewayService) ForwardAsResponses( // 10. Build upstream request upstreamCtx, releaseUpstreamCtx := detachStreamUpstreamContext(ctx, reqStream) - upstreamReq, _, err := s.buildUpstreamRequest(upstreamCtx, c, account, anthropicBody, token, tokenType, mappedModel, reqStream, shouldMimicClaudeCode) + upstreamReq, forwardedBody, err := s.buildUpstreamRequest(upstreamCtx, c, account, anthropicBody, token, tokenType, mappedModel, reqStream, shouldMimicClaudeCode) releaseUpstreamCtx() if err != nil { return nil, fmt.Errorf("build upstream request: %w", err) } + // Bill the final Anthropic effort after conversion and account normalization. + // For example, OpenAI xhigh is forwarded as output_config.effort=max. + reasoningEffort := NormalizeClaudeOutputEffort(gjson.GetBytes(forwardedBody, "output_config.effort").String()) + reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, forwardedBody, mappedModel) // 11. Send request resp, err := s.httpUpstream.DoWithTLS(upstreamReq, proxyURL, account.ID, account.Concurrency, s.tlsFPProfileService.ResolveTLSProfile(account)) diff --git a/backend/internal/service/gateway_reasoning_pricing_test.go b/backend/internal/service/gateway_reasoning_pricing_test.go new file mode 100644 index 0000000000..1aeaba6233 --- /dev/null +++ b/backend/internal/service/gateway_reasoning_pricing_test.go @@ -0,0 +1,70 @@ +//go:build unit + +package service + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRecordUsage_ReasoningPricingUsesForwardedEffort(t *testing.T) { + for _, platform := range []string{PlatformAnthropic, PlatformOpenAI} { + t.Run(platform, func(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + userRepo := &openAIRecordUsageUserRepoStub{} + subRepo := &openAIRecordUsageSubRepoStub{} + groupID := int64(77) + inputPrice, outputPrice := 0.001, 0.002 + requested, forwarded := "max", "high" + model := "gpt-5.4" + if platform == PlatformAnthropic { + model = "claude-fable-5-1" + } + apiKey := &APIKey{ + ID: 10, GroupID: &groupID, + Group: &Group{ + ID: groupID, Platform: platform, Status: StatusActive, Hydrated: true, RateMultiplier: 0.5, + ModelPricing: []ChannelModelPricing{{ + Models: []string{model}, BillingMode: BillingModeToken, + InputPrice: &inputPrice, OutputPrice: &outputPrice, + ReasoningEffortMultipliers: map[string]float64{"high": 1.5, "max": 3}, + }}, + }, + } + account := &Account{ID: 30, Platform: platform, Type: AccountTypeAPIKey} + user := &User{ID: 20} + if platform == PlatformAnthropic { + svc := newGatewayRecordUsageServiceForTest(usageRepo, userRepo, subRepo) + svc.resolver = NewModelPricingResolver(nil, svc.billingService) + require.NoError(t, svc.RecordUsage(context.Background(), &RecordUsageInput{ + Result: &ForwardResult{ + RequestID: "reasoning_pricing", Model: model, Duration: time.Second, + Usage: ClaudeUsage{InputTokens: 100, OutputTokens: 50}, + ReasoningEffort: &forwarded, RequestedReasoningEffort: &requested, + }, + APIKey: apiKey, User: user, Account: account, + })) + } else { + svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil) + svc.resolver = NewModelPricingResolver(nil, svc.billingService) + require.NoError(t, svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "reasoning_pricing", Model: model, Duration: time.Second, + Usage: OpenAIUsage{InputTokens: 100, OutputTokens: 50}, + ReasoningEffort: &forwarded, RequestedReasoningEffort: &requested, + }, + APIKey: apiKey, User: user, Account: account, + })) + } + require.NotNil(t, usageRepo.lastLog) + require.InDelta(t, 0.3, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.15, usageRepo.lastLog.ActualCost, 1e-12) + require.Equal(t, forwarded, *usageRepo.lastLog.ReasoningEffort) + require.Equal(t, requested, *usageRepo.lastLog.RequestedReasoningEffort) + require.InDelta(t, 0.15, userRepo.lastAmount, 1e-12) + }) + } +} diff --git a/backend/internal/service/model_plaza_service.go b/backend/internal/service/model_plaza_service.go index b1618acf78..17cb9b1b90 100644 --- a/backend/internal/service/model_plaza_service.go +++ b/backend/internal/service/model_plaza_service.go @@ -224,6 +224,9 @@ func (s *ModelPlazaService) ListGroups(ctx context.Context) ([]PlazaGroup, error // token 模型取计费阶梯表(单价与档位均由真实计费函数得出), // 图片/按次模型(或阶梯表不可用时)沿用渠道定价与分组图片档位价。 func (s *ModelPlazaService) fillDisplayPricing(ctx context.Context, m *PlazaModel, g *Group) { + if groupPricing := matchGroupModelPricing(g, m.Name); groupPricing != nil { + m.Pricing = groupPricing + } if s.billingService != nil && s.resolver != nil { sched, err := s.billingService.ResolveContextPricingSchedule(ctx, s.resolver, ContextPricingScheduleInput{ Model: m.Name, @@ -231,7 +234,7 @@ func (s *ModelPlazaService) fillDisplayPricing(ctx context.Context, m *PlazaMode Platform: m.Platform, }) if err == nil && sched != nil && len(sched.Tiers) > 0 { - m.Pricing = withDefaultMaxReasoningEffortMultiplier(plazaPricingFromSchedule(m.Pricing, sched), m.Name) + m.Pricing = plazaPricingFromSchedule(m.Pricing, sched) if len(sched.Tiers) > 1 { m.LongContextBasis = sched.Basis } @@ -239,20 +242,7 @@ func (s *ModelPlazaService) fillDisplayPricing(ctx context.Context, m *PlazaMode return } } - m.Pricing = withDefaultMaxReasoningEffortMultiplier(plazaImageDisplayPricing(m.Pricing, g), m.Name) -} - -func withDefaultMaxReasoningEffortMultiplier(pricing *ChannelModelPricing, model string) *ChannelModelPricing { - if pricing == nil || pricing.MaxReasoningEffortMultiplier != nil { - return pricing - } - multiplier := defaultMaxReasoningEffortMultiplier(model) - if multiplier == nil { - return pricing - } - cloned := pricing.Clone() - cloned.MaxReasoningEffortMultiplier = multiplier - return &cloned + m.Pricing = plazaImageDisplayPricing(m.Pricing, g) } // plazaPricingFromSchedule 把阶梯表压成展示用的 ChannelModelPricing: @@ -263,7 +253,7 @@ func plazaPricingFromSchedule(raw *ChannelModelPricing, sched *ContextPricingSch out.ImageInputPrice = raw.ImageInputPrice out.ImageOutputPrice = raw.ImageOutputPrice out.PerRequestPrice = raw.PerRequestPrice - out.MaxReasoningEffortMultiplier = raw.MaxReasoningEffortMultiplier + out.ReasoningEffortMultipliers = reasoningEffortMultipliersFromPricing(raw) } first := sched.Tiers[0] out.InputPrice = first.Input diff --git a/backend/internal/service/model_plaza_service_test.go b/backend/internal/service/model_plaza_service_test.go index e7c5f4ffb9..fa2e7a8345 100644 --- a/backend/internal/service/model_plaza_service_test.go +++ b/backend/internal/service/model_plaza_service_test.go @@ -57,17 +57,14 @@ func TestListPlazaGroups_GroupCentricAggregation(t *testing.T) { require.Equal(t, "claude-sonnet", out[0].Models[1].Name) } -func TestWithDefaultMaxReasoningEffortMultiplier_Fable51(t *testing.T) { - base := &ChannelModelPricing{BillingMode: BillingModeToken} - got := withDefaultMaxReasoningEffortMultiplier(base, "claude-fable-5-1") - require.NotSame(t, base, got) - require.NotNil(t, got.MaxReasoningEffortMultiplier) - require.Equal(t, 3.0, *got.MaxReasoningEffortMultiplier) - require.Nil(t, base.MaxReasoningEffortMultiplier) - - configured := 1.25 - custom := &ChannelModelPricing{MaxReasoningEffortMultiplier: &configured} - require.Same(t, custom, withDefaultMaxReasoningEffortMultiplier(custom, "claude-fable-5-1")) +func TestListPlazaGroups_Fable51HasNoImplicitReasoningMultiplier(t *testing.T) { + ch := plazaPricedChannel(1, "ch", []int64{10}, "anthropic", "claude-fable-5-1") + svc := newPlazaService([]Channel{ch}, []Group{{ID: 10, Platform: "anthropic"}}, nil) + groups, err := svc.ListGroups(context.Background()) + require.NoError(t, err) + require.Len(t, groups, 1) + require.Len(t, groups[0].Models, 1) + require.Empty(t, groups[0].Models[0].Pricing.ReasoningEffortMultipliers) } func TestListPlazaGroups_DedupFirstWinsWithPricingUpgrade(t *testing.T) { diff --git a/backend/internal/service/model_pricing_resolver.go b/backend/internal/service/model_pricing_resolver.go index ecd3d45209..12c569fedd 100644 --- a/backend/internal/service/model_pricing_resolver.go +++ b/backend/internal/service/model_pricing_resolver.go @@ -3,6 +3,7 @@ package service import ( "context" "log/slog" + "maps" "strings" ) @@ -247,9 +248,7 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin } resolved.BasePricing.FastMultiplier = chPricing.FastMultiplier resolved.BasePricing.FlexMultiplier = chPricing.FlexMultiplier - if chPricing.MaxReasoningEffortMultiplier != nil { - resolved.BasePricing.MaxReasoningEffortMultiplier = chPricing.MaxReasoningEffortMultiplier - } + resolved.BasePricing.ReasoningEffortMultipliers = maps.Clone(chPricing.ReasoningEffortMultipliers) // 渠道定价覆盖一切:显式配置则用配置值,未配置则归零(不回退到 LiteLLM) if chPricing.ImageOutputPrice != nil { resolved.BasePricing.ImageOutputPricePerToken = *chPricing.ImageOutputPrice diff --git a/backend/internal/service/openai_gateway_chat_completions_raw.go b/backend/internal/service/openai_gateway_chat_completions_raw.go index e5ddc972f8..c92528f2d7 100644 --- a/backend/internal/service/openai_gateway_chat_completions_raw.go +++ b/backend/internal/service/openai_gateway_chat_completions_raw.go @@ -80,10 +80,6 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions( // anchored to the client's stable conversation prefix. grokCacheIdentity = resolveGrokCacheIdentity(c, body, "", upstreamModel) } - reasoningEffort := extractOpenAIReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel) - // 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 billingModel 算出之后。 - reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel) - // 3. Rewrite model in body (no protocol conversion) upstreamBody := body if upstreamModel != originalModel { @@ -188,6 +184,10 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions( if customUA == "" && account.IsGrokOAuth() { customUA = defaultGrokUpstreamUserAgent() } + // Record the final provider-normalized effort, so usage and pricing match + // the outbound request (for example GLM xhigh is forwarded as max). + reasoningEffort := extractOpenAIReasoningEffortFromBody(upstreamBody, upstreamModel, billingModel, originalModel) + reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, upstreamBody, upstreamModel) resp, err := s.sendCCUpstreamRequest(ctx, c, account, targetURL, upstreamBody, clientStream, token, customUA, grokCacheIdentity) if err != nil { return nil, err diff --git a/backend/internal/service/openai_gateway_chat_completions_raw_test.go b/backend/internal/service/openai_gateway_chat_completions_raw_test.go index a67ad5fca9..433cb8cfaf 100644 --- a/backend/internal/service/openai_gateway_chat_completions_raw_test.go +++ b/backend/internal/service/openai_gateway_chat_completions_raw_test.go @@ -456,6 +456,9 @@ func TestForwardAsRawChatCompletions_NormalizesGLMReasoningEffortForUpstream(t * require.NoError(t, err) require.NotNil(t, result) require.Equal(t, "max", gjson.GetBytes(upstream.lastBody, "reasoning_effort").String()) + require.NotNil(t, result.ReasoningEffort) + require.Equal(t, "max", *result.ReasoningEffort) + require.Equal(t, 3.0, reasoningEffortBillingMultiplier(*result.ReasoningEffort, map[string]float64{"xhigh": 2, "max": 3})) } func TestForwardAsRawChatCompletions_SilentRefusalTriggersFailover(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_request_body.go b/backend/internal/service/openai_gateway_request_body.go index 2c8aa8d4b4..9d2d1e4e3c 100644 --- a/backend/internal/service/openai_gateway_request_body.go +++ b/backend/internal/service/openai_gateway_request_body.go @@ -2312,9 +2312,7 @@ func normalizeOpenAIReasoningEffort(raw string) string { value = strings.NewReplacer("-", "", "_", "", " ", "").Replace(value) switch value { - case "none", "minimal": - return "" - case "low", "medium", "high": + case "none", "minimal", "low", "medium", "high": return value case "xhigh", "extrahigh", "max": return "xhigh" diff --git a/backend/internal/service/openai_gateway_service_hotpath_test.go b/backend/internal/service/openai_gateway_service_hotpath_test.go index 3b7e80bc78..5142b7934d 100644 --- a/backend/internal/service/openai_gateway_service_hotpath_test.go +++ b/backend/internal/service/openai_gateway_service_hotpath_test.go @@ -932,10 +932,10 @@ func TestExtractOpenAIReasoningEffortFromBody(t *testing.T) { wantValue: "xhigh", }, { - name: "minimal 归一化为空", - body: []byte(`{"reasoning":{"effort":"minimal"}}`), - model: "gpt-5-high", - wantNil: true, + name: "minimal 保留用于计费", + body: []byte(`{"reasoning":{"effort":"minimal"}}`), + model: "gpt-5-high", + wantValue: "minimal", }, { name: "缺失字段时从模型后缀推导", diff --git a/backend/internal/service/openai_gateway_usage.go b/backend/internal/service/openai_gateway_usage.go index 6ae03bb5a2..3e253e378f 100644 --- a/backend/internal/service/openai_gateway_usage.go +++ b/backend/internal/service/openai_gateway_usage.go @@ -726,17 +726,13 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost( LongContextBillingEnabled: longContextBillingGate, }) } - breakdown, err := s.billingService.calculateCostWithServiceTierPolicy( + return s.billingService.calculateCostWithServiceTierPolicy( billingModel, tokens, multiplier, serviceTier, longContextBillingGate == nil || *longContextBillingGate, ) - if err == nil { - applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(billingModel, reasoningEffort, nil)) - } - return breakdown, err } func (s *OpenAIGatewayService) calculateOpenAIImageCost( diff --git a/backend/internal/service/reasoning_effort_billing_test.go b/backend/internal/service/reasoning_effort_billing_test.go new file mode 100644 index 0000000000..c62b4bf387 --- /dev/null +++ b/backend/internal/service/reasoning_effort_billing_test.go @@ -0,0 +1,245 @@ +//go:build unit + +package service + +import ( + "context" + "math" + "testing" + "time" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/stretchr/testify/require" +) + +func TestReasoningEffortBillingMultiplier(t *testing.T) { + multipliers := map[string]float64{"none": 0.5, "minimal": 0.75, "low": 0.9, "medium": 1.25, "high": 1.5, "xhigh": 2, "max": 3} + for _, tc := range []struct { + effort string + want float64 + }{ + {"none", 0.5}, {"minimal", 0.75}, {"low", 0.9}, {"medium", 1.25}, + {"high", 1.5}, {"xhigh", 2}, {"max", 3}, {" Extra-High ", 2}, + {" MAX ", 3}, {"", 1}, {"unknown", 1}, + } { + t.Run(tc.effort, func(t *testing.T) { + require.Equal(t, tc.want, reasoningEffortBillingMultiplier(tc.effort, multipliers)) + }) + } + require.Equal(t, 1.0, reasoningEffortBillingMultiplier("max", nil)) + for _, invalid := range []float64{0, -1, math.NaN(), math.Inf(1)} { + require.Equal(t, 1.0, reasoningEffortBillingMultiplier("max", map[string]float64{"max": invalid})) + } +} + +func TestReasoningEffortBillingSupportsAllModelsAndLevels(t *testing.T) { + for _, model := range []string{"claude-fable-5-1", "gpt-5.4", "gemini-2.5-pro", "custom-model"} { + t.Run(model, func(t *testing.T) { + bs, resolver := newTokenCostTestEnv(t, PlatformOpenAI, []ChannelModelPricing{{ + Platform: PlatformOpenAI, Models: []string{model}, BillingMode: BillingModeToken, + InputPrice: testPtrFloat64(1e-6), OutputPrice: testPtrFloat64(2e-6), + ReasoningEffortMultipliers: map[string]float64{"none": 0.5, "high": 1.5, "max": 3}, + }}, nil) + group := &Group{ID: 100, Platform: PlatformOpenAI} + for effort, multiplier := range map[string]float64{"none": 0.5, "high": 1.5, "max": 3, "low": 1, "": 1} { + cost, err := bs.CalculateTokenCostForRequest(TokenCostRequest{ + Ctx: context.Background(), Model: model, Group: group, Resolver: resolver, + Tokens: UsageTokens{InputTokens: 1000, OutputTokens: 200}, RateMultiplier: 0.8, + ReasoningEffort: effort, + }) + require.NoError(t, err) + require.InDelta(t, 0.0014*multiplier, cost.TotalCost, 1e-12, effort) + require.InDelta(t, cost.TotalCost*0.8, cost.ActualCost, 1e-12, effort) + } + }) + } +} + +func TestReasoningEffortBillingStacksWithContextTierServiceTierAndTimePricing(t *testing.T) { + for _, interval := range []bool{false, true} { + name := "catalog long context" + if interval { + name = "channel interval" + } + t.Run(name, func(t *testing.T) { + resolved := &ResolvedPricing{ + Mode: BillingModeToken, Source: PricingSourceChannel, + BasePricing: &ModelPricing{ + InputPricePerToken: 1e-6, ImageInputPricePerToken: 2e-6, + OutputPricePerToken: 3e-6, ImageOutputPricePerToken: 4e-6, + CacheCreationPricePerToken: 5e-6, CacheReadPricePerToken: 6e-6, + FastMultiplier: testPtrFloat64(2.5), + ReasoningEffortMultipliers: map[string]float64{"high": 1.7}, + LongContextInputThreshold: 100, LongContextInputMultiplier: 2, LongContextOutputMultiplier: 3, + }, + longContextPricingEnabled: true, + channelPricing: &ChannelModelPricing{TimePricing: &ChannelTimePricing{ + Timezone: "UTC", Periods: []ChannelTimePricingPeriod{{StartTime: "08:00", EndTime: "16:00", Multiplier: 1.2}}, + }}, + } + if interval { + resolved.Intervals = []PricingInterval{{MinTokens: 100, InputMultiplier: testPtrFloat64(2), OutputMultiplier: testPtrFloat64(3)}} + } + input := CostInput{ + Model: "custom-model", RateMultiplier: 0.8, ServiceTier: "fast", + Tokens: UsageTokens{InputTokens: 200, ImageInputTokens: 20, OutputTokens: 10, ImageOutputTokens: 2, CacheCreationTokens: 5, CacheReadTokens: 3}, + Resolver: &ModelPricingResolver{}, Resolved: resolved, + PricingAt: time.Date(2026, 9, 18, 12, 0, 0, 0, time.UTC), + } + bs := &BillingService{} + standard, err := bs.CalculateCostUnified(input) + require.NoError(t, err) + input.ReasoningEffort = "high" + adjusted, err := bs.CalculateCostUnified(input) + require.NoError(t, err) + for _, costs := range [][2]float64{ + {standard.InputCost, adjusted.InputCost}, {standard.ImageInputCost, adjusted.ImageInputCost}, + {standard.OutputCost, adjusted.OutputCost}, {standard.ImageOutputCost, adjusted.ImageOutputCost}, + {standard.CacheCreationCost, adjusted.CacheCreationCost}, {standard.CacheReadCost, adjusted.CacheReadCost}, + {standard.TotalCost, adjusted.TotalCost}, {standard.ActualCost, adjusted.ActualCost}, + } { + require.Positive(t, costs[0]) + require.InDelta(t, costs[0]*1.7, costs[1], 1e-12) + } + require.Equal(t, standard.LongContextBillingApplied, adjusted.LongContextBillingApplied) + }) + } +} + +func TestReasoningEffortBillingGroupOverrideAndUnitBilling(t *testing.T) { + for _, mode := range []BillingMode{BillingModeToken, BillingModePerRequest, BillingModeImage, BillingModeVideo} { + t.Run(string(mode), func(t *testing.T) { + bs, resolver := newTokenCostTestEnv(t, PlatformOpenAI, []ChannelModelPricing{{ + Platform: PlatformOpenAI, Models: []string{"custom-model"}, BillingMode: mode, + InputPrice: testPtrFloat64(1e-6), PerRequestPrice: testPtrFloat64(0.1), + ReasoningEffortMultipliers: map[string]float64{"high": 2, "max": 3}, + }}, nil) + group := &Group{ID: 100, Platform: PlatformOpenAI, ModelPricing: []ChannelModelPricing{{ + Models: []string{"custom-model"}, BillingMode: mode, + InputPrice: testPtrFloat64(1e-6), PerRequestPrice: testPtrFloat64(0.1), + ReasoningEffortMultipliers: map[string]float64{"high": 1.25}, + }}} + for effort, multiplier := range map[string]float64{"high": 1.25, "max": 1} { + input := CostInput{ + Ctx: context.Background(), Model: "custom-model", Group: group, GroupID: &group.ID, + Tokens: UsageTokens{InputTokens: 1000}, RequestCount: 2, RateMultiplier: 0.8, + ReasoningEffort: effort, Resolver: resolver, + } + cost, err := bs.CalculateCostUnified(input) + require.NoError(t, err) + base := 0.2 + if mode == BillingModeToken { + base = 0.001 + } + require.InDelta(t, base*multiplier, cost.TotalCost, 1e-12) + require.InDelta(t, cost.TotalCost*0.8, cost.ActualCost, 1e-12) + } + }) + } +} + +func TestAccountStatsCustomRulesUseOwnReasoningEffortMultipliers(t *testing.T) { + for _, mode := range []BillingMode{BillingModeToken, BillingModePerRequest, BillingModeImage, BillingModeVideo} { + t.Run(string(mode), func(t *testing.T) { + channel := &Channel{AccountStatsPricingRules: []AccountStatsPricingRule{{ + AccountIDs: []int64{1}, Pricing: []ChannelModelPricing{{ + Models: []string{"claude-fable-5-1"}, BillingMode: mode, + InputPrice: testPtrFloat64(0.001), PerRequestPrice: testPtrFloat64(0.1), + ReasoningEffortMultipliers: map[string]float64{"high": 1.5}, + }}, + }}} + for effort, multiplier := range map[string]float64{"high": 1.5, "max": 1} { + cost := tryCustomRules(channel, 1, 100, PlatformAnthropic, "claude-fable-5-1", UsageTokens{InputTokens: 100}, 2, effort) + require.NotNil(t, cost) + base := 0.2 + if mode == BillingModeToken { + base = 0.1 + } + require.InDelta(t, base*multiplier, *cost, 1e-12) + } + }) + } +} + +func TestDisplayPricingPreservesConfiguredReasoningEffortMultipliers(t *testing.T) { + raw := &ChannelModelPricing{ReasoningEffortMultipliers: map[string]float64{"high": 1.5, "max": 3}} + for _, mode := range []BillingMode{BillingModeToken, BillingModePerRequest, BillingModeImage} { + raw.BillingMode = mode + display := synthesizePricingFromLiteLLM(&LiteLLMModelPricing{InputCostPerToken: 1e-6, OutputCostPerImage: 0.1}, raw) + require.Equal(t, raw.ReasoningEffortMultipliers, display.ReasoningEffortMultipliers) + display.ReasoningEffortMultipliers["high"] = 9 + require.Equal(t, 1.5, raw.ReasoningEffortMultipliers["high"]) + } + display := plazaPricingFromSchedule(raw, &ContextPricingSchedule{Tiers: []ContextPricingTier{{Input: testPtrFloat64(1e-6)}}}) + require.Equal(t, raw.ReasoningEffortMultipliers, display.ReasoningEffortMultipliers) + display.ReasoningEffortMultipliers["max"] = 9 + require.Equal(t, 3.0, raw.ReasoningEffortMultipliers["max"]) +} + +func TestPlazaUsesGroupReasoningEffortMultipliers(t *testing.T) { + m := &PlazaModel{Name: "custom-model", Platform: PlatformOpenAI, Pricing: &ChannelModelPricing{ + ReasoningEffortMultipliers: map[string]float64{"max": 3}, + }} + g := &Group{ModelPricing: []ChannelModelPricing{{ + Models: []string{"custom-model"}, BillingMode: BillingModeToken, + ReasoningEffortMultipliers: map[string]float64{"high": 1.5}, + }}} + (&ModelPlazaService{}).fillDisplayPricing(context.Background(), m, g) + require.Equal(t, map[string]float64{"high": 1.5}, m.Pricing.ReasoningEffortMultipliers) + m.Pricing.ReasoningEffortMultipliers["high"] = 9 + require.Equal(t, 1.5, g.ModelPricing[0].ReasoningEffortMultipliers["high"]) +} + +func TestReasoningEffortBillingNoResolverPreservesCatalogPolicies(t *testing.T) { + bs := NewBillingService(&config.Config{}, nil) + tokens := UsageTokens{InputTokens: 300000, OutputTokens: 100, CacheCreationTokens: 20, CacheReadTokens: 10} + for _, applyLongContext := range []bool{false, true} { + for _, serviceTier := range []string{"", "priority", "flex"} { + for _, rateMultiplier := range []float64{-1, 0, 0.8} { + want, err := bs.calculateCostInternalWithPolicy("gpt-5.4", tokens, rateMultiplier, serviceTier, nil, applyLongContext) + require.NoError(t, err) + got, err := bs.CalculateCostUnified(CostInput{ + Model: "gpt-5.4", Tokens: tokens, RateMultiplier: rateMultiplier, + ServiceTier: serviceTier, LongContextBillingEnabled: &applyLongContext, ReasoningEffort: "max", + }) + require.NoError(t, err) + require.Equal(t, want, got) + } + } + } +} + +func TestReasoningEffortBillingPreservesForwardedNoneAndMinimal(t *testing.T) { + for _, effort := range []string{"none", "minimal"} { + t.Run(effort, func(t *testing.T) { + for _, body := range [][]byte{ + []byte(`{"model":"gpt-5.4","reasoning":{"effort":"` + effort + `"}}`), + []byte(`{"model":"gpt-5.4","reasoning_effort":"` + effort + `"}`), + } { + got := extractOpenAIReasoningEffortFromBody(body, "gpt-5.4") + require.NotNil(t, got) + require.Equal(t, effort, *got) + require.Equal(t, 0.5, reasoningEffortBillingMultiplier(*got, map[string]float64{effort: 0.5})) + require.Equal(t, got, extractCCReasoningEffortFromBody(body, "gpt-5.4")) + } + for _, body := range []map[string]any{ + {"reasoning": map[string]any{"effort": effort}}, + {"reasoning_effort": effort}, + } { + got := extractOpenAIReasoningEffort(body, "gpt-5.4") + require.NotNil(t, got) + require.Equal(t, effort, *got) + } + body := []byte(`{"model":"gpt-5.4","reasoning":{"effort":"` + effort + `"}}`) + got := ExtractResponsesReasoningEffortFromBody(body, "gpt-5.4") + require.NotNil(t, got) + require.Equal(t, effort, *got) + }) + } + + // A compatibility endpoint that strips none must not bill the removed request preference. + account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"base_url": "https://compat.example/v1"}} + filtered, err := filterOpenAIResponsesNoneReasoningEffortForAccount(account, []byte(`{"model":"custom-model","reasoning":{"effort":"none"}}`)) + require.NoError(t, err) + require.Nil(t, extractOpenAIReasoningEffortFromBody(filtered, "custom-model")) +} diff --git a/backend/migrations/239_channel_reasoning_effort_multipliers.sql b/backend/migrations/239_channel_reasoning_effort_multipliers.sql new file mode 100644 index 0000000000..6fd3f43ed1 --- /dev/null +++ b/backend/migrations/239_channel_reasoning_effort_multipliers.sql @@ -0,0 +1,56 @@ +-- Migrate only explicitly configured legacy max pricing. There is no model-specific default. +-- Run the backfill only when introducing the column: replaying this migration must +-- preserve current settings, including maps that have deliberately been cleared. +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_attribute + WHERE attrelid = 'channel_model_pricing'::regclass + AND attname = 'reasoning_effort_multipliers' + AND NOT attisdropped + ) THEN + ALTER TABLE channel_model_pricing + ADD COLUMN IF NOT EXISTS reasoning_effort_multipliers JSONB NOT NULL DEFAULT '{}'::jsonb; + + UPDATE channel_model_pricing + SET reasoning_effort_multipliers = jsonb_build_object('max', max_reasoning_effort_multiplier) + WHERE max_reasoning_effort_multiplier IS NOT NULL; + END IF; +END $$; + +ALTER TABLE channel_account_stats_model_pricing + ADD COLUMN IF NOT EXISTS reasoning_effort_multipliers JSONB NOT NULL DEFAULT '{}'::jsonb; + +-- Group pricing stores the same entries as JSON. An existing generic map wins, +-- even when empty. Remove the consumed legacy key so replay cannot revive it. +UPDATE groups AS g +SET model_pricing = ( + SELECT jsonb_agg( + CASE + WHEN jsonb_typeof(entry) = 'object' AND entry ? 'max_reasoning_effort_multiplier' THEN + (entry - 'max_reasoning_effort_multiplier') || + CASE + WHEN NOT (entry ? 'reasoning_effort_multipliers') + AND jsonb_typeof(entry->'max_reasoning_effort_multiplier') = 'number' THEN + jsonb_build_object('reasoning_effort_multipliers', + jsonb_build_object('max', entry->'max_reasoning_effort_multiplier')) + ELSE '{}'::jsonb + END + ELSE entry + END ORDER BY ordinal + ) + FROM jsonb_array_elements(g.model_pricing) WITH ORDINALITY AS pricing(entry, ordinal) +) +WHERE jsonb_typeof(g.model_pricing) = 'array' + AND EXISTS ( + SELECT 1 + FROM jsonb_array_elements( + CASE WHEN jsonb_typeof(g.model_pricing) = 'array' THEN g.model_pricing ELSE '[]'::jsonb END + ) AS pricing(entry) + WHERE jsonb_typeof(entry) = 'object' AND entry ? 'max_reasoning_effort_multiplier' + ); + +COMMENT ON COLUMN channel_model_pricing.reasoning_effort_multipliers IS + 'Custom billing multipliers by reasoning effort; omitted efforts use 1x'; +COMMENT ON COLUMN channel_account_stats_model_pricing.reasoning_effort_multipliers IS + 'Custom account statistics billing multipliers by reasoning effort; omitted efforts use 1x'; diff --git a/backend/migrations/channel_reasoning_effort_multipliers_migration_test.go b/backend/migrations/channel_reasoning_effort_multipliers_migration_test.go new file mode 100644 index 0000000000..ed3da3ba21 --- /dev/null +++ b/backend/migrations/channel_reasoning_effort_multipliers_migration_test.go @@ -0,0 +1,25 @@ +package migrations + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestChannelReasoningEffortMultipliersMigration(t *testing.T) { + content, err := FS.ReadFile("239_channel_reasoning_effort_multipliers.sql") + require.NoError(t, err) + sql := strings.Join(strings.Fields(string(content)), " ") + require.Contains(t, sql, "ALTER TABLE channel_model_pricing ADD COLUMN IF NOT EXISTS reasoning_effort_multipliers JSONB NOT NULL DEFAULT '{}'::jsonb") + require.Contains(t, sql, "ALTER TABLE channel_account_stats_model_pricing ADD COLUMN IF NOT EXISTS reasoning_effort_multipliers JSONB NOT NULL DEFAULT '{}'::jsonb") + require.Contains(t, sql, "WHERE max_reasoning_effort_multiplier IS NOT NULL") + require.Contains(t, sql, "jsonb_build_object('max', max_reasoning_effort_multiplier)") + require.Contains(t, sql, "attrelid = 'channel_model_pricing'::regclass AND attname = 'reasoning_effort_multipliers' AND NOT attisdropped") + require.Less(t, strings.Index(sql, "UPDATE channel_model_pricing"), strings.Index(sql, "END IF;")) + require.Contains(t, sql, "UPDATE groups AS g SET model_pricing") + require.Contains(t, sql, "NOT (entry ? 'reasoning_effort_multipliers')") + require.Contains(t, sql, "entry - 'max_reasoning_effort_multiplier'") + require.Contains(t, sql, "END ORDER BY ordinal") + require.NotContains(t, strings.ToLower(sql), "fable") +} diff --git a/docs/screenshots/reasoning-effort-pricing.png b/docs/screenshots/reasoning-effort-pricing.png new file mode 100644 index 0000000000000000000000000000000000000000..df7b1ef7a2567dd730c57f267760ebb7a574e1ba GIT binary patch literal 191361 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 z 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 z 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π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
a3{$T>!!W?LMun$4?K9UdOV6 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%0ANx7-p6pj2#cwk_G7Ts83e@%jY;sZm@c_m6 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

1cBKF|1PONO_r5XlB^at3Htv4 D{|Ow- literal 0 HcmV?d00001 diff --git a/frontend/src/api/admin/channels.ts b/frontend/src/api/admin/channels.ts index d956bb92c7..a04660ca97 100644 --- a/frontend/src/api/admin/channels.ts +++ b/frontend/src/api/admin/channels.ts @@ -50,7 +50,7 @@ export interface ChannelModelPricing { cache_read_price: number | null fast_multiplier?: number | null flex_multiplier?: number | null - max_reasoning_effort_multiplier?: number | null + reasoning_effort_multipliers?: Record | null image_input_price: number | null image_output_price: number | null per_request_price: number | null @@ -180,7 +180,7 @@ export interface ModelDefaultPricing { cache_read_price?: number image_input_price?: number image_output_price?: number - max_reasoning_effort_multiplier?: number | null + reasoning_effort_multipliers?: Record | null } export async function getModelDefaultPricing(model: string): Promise { diff --git a/frontend/src/api/channels.ts b/frontend/src/api/channels.ts index 5c3f83542e..e229bbcba3 100644 --- a/frontend/src/api/channels.ts +++ b/frontend/src/api/channels.ts @@ -45,7 +45,7 @@ export interface UserSupportedModelPricing { cache_write_price: number | null cache_write_1h_price?: number | null cache_read_price: number | null - max_reasoning_effort_multiplier?: number | null + reasoning_effort_multipliers?: Record | null image_input_price: number | null image_output_price: number | null per_request_price: number | null diff --git a/frontend/src/components/admin/channel/PricingEntryCard.vue b/frontend/src/components/admin/channel/PricingEntryCard.vue index ba6fe3cf2f..f2448d6d08 100644 --- a/frontend/src/components/admin/channel/PricingEntryCard.vue +++ b/frontend/src/components/admin/channel/PricingEntryCard.vue @@ -144,7 +144,7 @@ -

+
-
- - -
@@ -261,6 +256,43 @@ />
+ +
+
+ + +
+

{{ t('admin.channels.form.reasoningEffortMultipliersHint') }}

+
+ +
+ +
@@ -275,7 +307,8 @@ import IntervalRow from './IntervalRow.vue' import ModelTagInput from './ModelTagInput.vue' import TimePricingSection from './TimePricingSection.vue' import type { PricingFormEntry, IntervalFormEntry } from './types' -import { perTokenToMTok, getPlatformTagClass } from './types' +import { perTokenToMTok, getPlatformTagClass, isValidPositiveMultiplier, validateReasoningEffortMultipliers } from './types' +import { REASONING_EFFORT_LEVELS, type ReasoningEffortLevel } from '@/constants/channel' import type { BillingMode } from '@/api/admin/channels' import channelsAPI from '@/api/admin/channels' @@ -313,12 +346,20 @@ const billingModeLabel = computed(() => { return opt ? opt.label : props.entry.billing_mode }) -const maxReasoningEffortMultiplierPlaceholder = computed(() => - props.entry.models.some(model => /fable(?:-5-1|-5\.1|5\.1|51)(?!\d)/i.test(model)) - ? t('admin.channels.form.fable51DefaultMaxReasoningMultiplier') - : t('admin.channels.form.multiplierPlaceholder') +const reasoningEffortMultiplierError = computed(() => + validateReasoningEffortMultipliers(props.entry.reasoning_effort_multipliers, t) ) +function updateReasoningEffortMultiplier(effort: ReasoningEffortLevel, value: string) { + const multipliers = { ...props.entry.reasoning_effort_multipliers } + if (value === '') delete multipliers[effort] + else multipliers[effort] = value + emit('update', { + ...props.entry, + reasoning_effort_multipliers: Object.keys(multipliers).length ? multipliers : null, + }) +} + function emitField(field: keyof PricingFormEntry, value: string) { emit('update', { ...props.entry, [field]: value === '' ? null : value }) } @@ -394,7 +435,7 @@ async function onModelsUpdate(newModels: string[]) { cache_read_price: perTokenToMTok(result.cache_read_price ?? null), image_input_price: perTokenToMTok(result.image_input_price ?? null), image_output_price: perTokenToMTok(result.image_output_price ?? null), - max_reasoning_effort_multiplier: result.max_reasoning_effort_multiplier ?? null, + reasoning_effort_multipliers: props.entry.reasoning_effort_multipliers ?? result.reasoning_effort_multipliers ?? null, }) } } catch { diff --git a/frontend/src/components/admin/channel/__tests__/PricingEntryCard.timePricing.spec.ts b/frontend/src/components/admin/channel/__tests__/PricingEntryCard.timePricing.spec.ts index 947ac41c1f..deee992b31 100644 --- a/frontend/src/components/admin/channel/__tests__/PricingEntryCard.timePricing.spec.ts +++ b/frontend/src/components/admin/channel/__tests__/PricingEntryCard.timePricing.spec.ts @@ -1,7 +1,12 @@ -import { shallowMount } from '@vue/test-utils' +import { flushPromises, shallowMount } from '@vue/test-utils' import { describe, expect, it, vi } from 'vitest' import PricingEntryCard from '../PricingEntryCard.vue' import type { PricingFormEntry } from '../types' +import channelsAPI from '@/api/admin/channels' + +vi.mock('@/api/admin/channels', () => ({ + default: { getModelDefaultPricing: vi.fn() }, +})) vi.mock('vue-i18n', async importOriginal => ({ ...await importOriginal(), @@ -18,13 +23,14 @@ function createEntry(billingMode: PricingFormEntry['billing_mode'] = 'token'): P cache_read_price: null, fast_multiplier: null, flex_multiplier: null, - max_reasoning_effort_multiplier: null, + reasoning_effort_multipliers: null, image_input_price: null, image_output_price: null, per_request_price: null, intervals: [], time_pricing: { timezone: 'Asia/Shanghai', + weekdays_only: false, periods: [{ start_time: '09:00', end_time: '12:00', multiplier: '2.00' }], }, } @@ -67,14 +73,14 @@ describe('PricingEntryCard time pricing visibility', () => { ...entry, billing_mode: 'image', intervals: [], - time_pricing: { timezone: 'Asia/Shanghai', periods: [] }, + time_pricing: { timezone: 'Asia/Shanghai', weekdays_only: false, periods: [] }, }) expect(entry.time_pricing.periods).toHaveLength(1) }) }) describe('PricingEntryCard request multipliers', () => { - it('shows Fast, Flex, and Max effort controls only when explicitly enabled', () => { + it('shows Fast and Flex controls only when explicitly enabled', () => { const hidden = shallowMount(PricingEntryCard, { props: { entry: createEntry() } }) expect(hidden.text()).not.toContain('admin.channels.form.fastMultiplier') @@ -83,6 +89,73 @@ describe('PricingEntryCard request multipliers', () => { }) expect(shown.text()).toContain('admin.channels.form.fastMultiplier') expect(shown.text()).toContain('admin.channels.form.flexMultiplier') - expect(shown.text()).toContain('admin.channels.form.maxReasoningEffortMultiplier') + expect(shown.text()).toContain('admin.channels.form.reasoningEffortMultipliers') + }) + + it.each(['token', 'per_request', 'image', 'video'] as const)('supports every reasoning level for %s pricing and account statistics', billingMode => { + const wrapper = shallowMount(PricingEntryCard, { props: { entry: createEntry(billingMode) } }) + expect(wrapper.findAll('[data-reasoning-effort]').map(input => input.attributes('data-reasoning-effort'))) + .toEqual(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']) + }) + + it('edits levels independently, then clears individual and all overrides', async () => { + const entry = { ...createEntry(), reasoning_effort_multipliers: { high: 1.5, max: 3 } } + const wrapper = shallowMount(PricingEntryCard, { props: { entry } }) + const update = () => wrapper.emitted('update')!.at(-1)![0] as PricingFormEntry + + await wrapper.get('[data-reasoning-effort="high"]').setValue('0.75') + expect(update().reasoning_effort_multipliers).toEqual({ high: '0.75', max: 3 }) + expect(entry.reasoning_effort_multipliers.high).toBe(1.5) + await wrapper.setProps({ entry: update() }) + + await wrapper.get('[data-reasoning-effort="max"]').setValue('') + expect(update().reasoning_effort_multipliers).toEqual({ high: '0.75' }) + await wrapper.setProps({ entry: update() }) + + await wrapper.get('[data-testid="reasoning-effort-multipliers"] button').trigger('click') + expect(update().reasoning_effort_multipliers).toBeNull() + await wrapper.setProps({ entry: update() }) + expect(wrapper.get('[data-reasoning-effort="high"]').element.value).toBe('') + }) + + it('clearing the last configured level restores default billing', async () => { + const wrapper = shallowMount(PricingEntryCard, { + props: { entry: { ...createEntry(), reasoning_effort_multipliers: { max: 3 } } }, + }) + await wrapper.get('[data-reasoning-effort="max"]').setValue('') + expect((wrapper.emitted('update')![0][0] as PricingFormEntry).reasoning_effort_multipliers).toBeNull() + }) + + it('marks invalid multipliers and shows a validation error', () => { + const wrapper = shallowMount(PricingEntryCard, { + props: { entry: { ...createEntry(), reasoning_effort_multipliers: { high: 0, max: 2 } } }, + }) + expect(wrapper.get('[data-reasoning-effort="high"]').attributes('aria-invalid')).toBe('true') + expect(wrapper.get('[data-reasoning-effort="max"]').attributes('aria-invalid')).toBe('false') + expect(wrapper.get('[role="alert"]').text()).toContain('reasoningEffortMultiplierPositive') + }) + + it('does not assign a model-specific multiplier to Fable', () => { + const wrapper = shallowMount(PricingEntryCard, { + props: { entry: { ...createEntry(), models: ['claude-fable-5-1'] } }, + }) + expect(wrapper.get('[data-reasoning-effort="max"]').element.value).toBe('') + expect(wrapper.get('[data-reasoning-effort="max"]').attributes('placeholder')) + .toBe('admin.channels.form.reasoningEffortMultiplierDefault') + }) + + it('keeps custom effort multipliers when auto-filling model token prices', async () => { + vi.mocked(channelsAPI.getModelDefaultPricing).mockResolvedValue({ + found: true, input_price: 3e-6, output_price: 15e-6, + }) + const wrapper = shallowMount(PricingEntryCard, { + props: { entry: { ...createEntry(), reasoning_effort_multipliers: { high: 0.5 } } }, + }) + wrapper.findComponent({ name: 'ModelTagInput' }).vm.$emit('update:models', ['example-model']) + await flushPromises() + expect(wrapper.emitted('update')!.at(-1)![0]).toMatchObject({ + models: ['example-model'], input_price: 3, output_price: 15, + reasoning_effort_multipliers: { high: 0.5 }, + }) }) }) diff --git a/frontend/src/components/admin/channel/__tests__/types.spec.ts b/frontend/src/components/admin/channel/__tests__/types.spec.ts index 58b5724239..3a5efd0526 100644 --- a/frontend/src/components/admin/channel/__tests__/types.spec.ts +++ b/frontend/src/components/admin/channel/__tests__/types.spec.ts @@ -4,15 +4,43 @@ import { apiTimePricingToForm, createDefaultTimePricingForm, formIntervalsToAPI, + formReasoningEffortMultipliersToAPI, formTimePricingToAPI, isValidPositiveMultiplier, validateIntervals, + validateReasoningEffortMultipliers, validateTimePricing, type IntervalFormEntry, type TimePricingFormEntry, type TimePricingPeriodFormEntry, } from '../types' +describe('reasoning effort multipliers', () => { + it('serializes independent overrides without altering their values', () => { + expect(formReasoningEffortMultipliersToAPI({ none: '0.5', high: 1, max: '3', low: '' })) + .toEqual({ none: 0.5, high: 1, max: 3 }) + }) + + it.each([null, undefined, {}, { max: '' }])('clears empty overrides with null: %j', value => { + expect(formReasoningEffortMultipliersToAPI(value)).toBeNull() + expect(validateReasoningEffortMultipliers(value, t)).toBeNull() + }) + + it('accepts supported levels with positive finite multipliers, including discounts', () => { + expect(validateReasoningEffortMultipliers({ + none: 0.01, minimal: 0.5, low: 1, medium: '1.2', high: 2, xhigh: 2.5, max: 3, + }, t)).toBeNull() + }) + + it.each([0, -1, Infinity, NaN, 'invalid', 'Infinity'])('rejects invalid multiplier %s', multiplier => { + expect(validateReasoningEffortMultipliers({ high: multiplier }, t)).toContain('reasoningEffortMultiplierPositive') + }) + + it('rejects unsupported effort keys', () => { + expect(validateReasoningEffortMultipliers({ unknown: 2 }, t)).toContain('reasoningEffortLevelInvalid') + }) +}) + describe('interval multiplier conversion', () => { it('preserves component multipliers without MTok conversion', () => { const form = apiIntervalsToForm([{ diff --git a/frontend/src/components/admin/channel/types.ts b/frontend/src/components/admin/channel/types.ts index 0575e1b8c6..bd818ea4e5 100644 --- a/frontend/src/components/admin/channel/types.ts +++ b/frontend/src/components/admin/channel/types.ts @@ -1,4 +1,5 @@ import type { BillingMode, ChannelTimePricing, PricingInterval } from '@/api/admin/channels' +import { REASONING_EFFORT_LEVELS } from '@/constants/channel' type TranslateFn = (key: string, params?: Record) => string @@ -29,7 +30,7 @@ export interface PricingFormEntry { cache_read_price: number | string | null fast_multiplier?: number | string | null flex_multiplier?: number | string | null - max_reasoning_effort_multiplier?: number | string | null + reasoning_effort_multipliers?: Record | null image_input_price: number | string | null image_output_price: number | string | null per_request_price: number | string | null @@ -182,6 +183,30 @@ export function isValidPositiveMultiplier(val: number | string | null | undefine return Number.isFinite(multiplier) && multiplier > 0 } +export function formReasoningEffortMultipliersToAPI( + value: PricingFormEntry['reasoning_effort_multipliers'], +): Record | null { + const entries = Object.entries(value || {}) + .filter(([, multiplier]) => multiplier !== '') + .map(([effort, multiplier]) => [effort, Number(multiplier)]) + return entries.length ? Object.fromEntries(entries) : null +} + +export function validateReasoningEffortMultipliers( + value: PricingFormEntry['reasoning_effort_multipliers'], + t: TranslateFn, +): string | null { + for (const [effort, multiplier] of Object.entries(value || {})) { + if (!REASONING_EFFORT_LEVELS.some(level => level === effort)) { + return t('admin.channels.form.reasoningEffortLevelInvalid', { effort }) + } + if (multiplier !== '' && !isValidPositiveMultiplier(multiplier)) { + return t('admin.channels.form.reasoningEffortMultiplierPositive', { effort }) + } + } + return null +} + /** 前端显示值($/MTok) → 后端存储值(per-token) */ export function mTokToPerToken(val: number | string | null | undefined): number | null { const num = toNullableNumber(val) diff --git a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue index 125154f2e9..71f5ec024d 100644 --- a/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue +++ b/frontend/src/components/modelPlaza/PlazaModelPricingTable.vue @@ -100,11 +100,13 @@ {{ t('modelPlaza.table.marginalBadge') }} - {{ t('modelPlaza.table.maxReasoningMultiplierBadge', { multiplier: m.pricing.max_reasoning_effort_multiplier }) }} + {{ t('modelPlaza.table.reasoningMultiplierBadge', { effort, multiplier }) }} @@ -311,11 +313,22 @@ import { platformAccentColor, platformBadgeLightClass, platformLabel } from '@/u import { BILLING_MODE_TOKEN, BILLING_MODE_IMAGE, + REASONING_EFFORT_LEVELS, type BillingMode } from '@/constants/channel' import type { PlazaModel, PlazaTimePricingPeriod } from '@/api/modelPlaza' import type { UserPricingInterval } from '@/api/channels' +function reasoningEffortMultipliers(model: PlazaModel): [string, number][] { + const multipliers = model.pricing?.reasoning_effort_multipliers + return REASONING_EFFORT_LEVELS.flatMap(effort => { + const multiplier = multipliers?.[effort] + return typeof multiplier === 'number' && Number.isFinite(multiplier) && multiplier > 0 + ? [[effort, multiplier] as [string, number]] + : [] + }) +} + const props = defineProps<{ models: PlazaModel[] /** 分组平台;实付分区底色随平台着色,未知平台回退品牌青。 */ diff --git a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts index 9813e40fa5..c8c6bbe77e 100644 --- a/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts +++ b/frontend/src/components/modelPlaza/__tests__/PlazaModelPricingTable.spec.ts @@ -68,13 +68,27 @@ describe('PlazaModelPricingTable', () => { expect(text).toContain('1x') }) - it('shows the Max reasoning billing multiplier', () => { + it('shows all configured reasoning multipliers in level order', () => { const model = tokenModel() - model.pricing!.max_reasoning_effort_multiplier = 3 + model.pricing!.reasoning_effort_multipliers = { max: 3, none: 0.5, high: 1.5 } const wrapper = mountTable([model], 1) - expect(wrapper.text()).toContain('modelPlaza.table.maxReasoningMultiplierBadge') - expect(wrapper.find('[title="modelPlaza.table.maxReasoningMultiplierHint"]').exists()).toBe(true) + const badges = wrapper.findAll('[data-reasoning-effort]') + expect(badges.map(badge => badge.attributes('data-reasoning-effort'))).toEqual(['none', 'high', 'max']) + expect(badges.every(badge => badge.attributes('title') === 'modelPlaza.table.reasoningMultiplierHint')).toBe(true) + expect(wrapper.text()).toContain('modelPlaza.table.reasoningMultiplierBadge') + }) + + it('does not show automatic reasoning charges for an unconfigured Fable model', () => { + const wrapper = mountTable([tokenModel({ name: 'claude-fable-5-1' })], 1) + expect(wrapper.find('[data-reasoning-effort]').exists()).toBe(false) + }) + + it('omits invalid or unsupported multipliers from display', () => { + const model = tokenModel() + model.pricing!.reasoning_effort_multipliers = { max: 0, high: Infinity, unknown: 2, low: 1 } + const wrapper = mountTable([model], 1) + expect(wrapper.findAll('[data-reasoning-effort]').map(badge => badge.attributes('data-reasoning-effort'))).toEqual(['low']) }) it('倍率 ≠ 1 时价格列为折后实付价,官方价列保持原价', () => { diff --git a/frontend/src/constants/channel.ts b/frontend/src/constants/channel.ts index 1e1b9205de..0d548d1fbc 100644 --- a/frontend/src/constants/channel.ts +++ b/frontend/src/constants/channel.ts @@ -14,6 +14,10 @@ export type BillingMode = | typeof BILLING_MODE_IMAGE | typeof BILLING_MODE_VIDEO +/** Supported request reasoning effort levels, ordered from lowest to highest. */ +export const REASONING_EFFORT_LEVELS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const +export type ReasoningEffortLevel = typeof REASONING_EFFORT_LEVELS[number] + /** Billing-model-source values (must match service.BillingModelSource* constants in Go). */ export const BILLING_MODEL_SOURCE_REQUESTED = 'requested' as const export const BILLING_MODEL_SOURCE_UPSTREAM = 'upstream' as const diff --git a/frontend/src/i18n/locales/en/admin/channels.ts b/frontend/src/i18n/locales/en/admin/channels.ts index 77f5c00644..db867cfc6f 100644 --- a/frontend/src/i18n/locales/en/admin/channels.ts +++ b/frontend/src/i18n/locales/en/admin/channels.ts @@ -140,10 +140,15 @@ export default { pricePlaceholder: 'Default', fastMultiplier: 'Fast Multiplier', flexMultiplier: 'Flex Multiplier', - maxReasoningEffortMultiplier: 'Max Effort Multiplier', - fable51DefaultMaxReasoningMultiplier: 'Default: 3', + reasoningEffortMultipliers: 'Custom Reasoning Effort Multipliers (optional)', + reasoningEffortMultipliersHint: 'Apply the multiplier for the final forwarded reasoning effort to the entire request cost and quota usage. Unconfigured levels use 1×. These multiply with other billing multipliers.', + reasoningEffortMultiplierLabel: '{effort} reasoning effort multiplier', + reasoningEffortMultiplierDefault: 'Default: 1', + clearReasoningEffortMultipliers: 'Clear multipliers', + reasoningEffortMultiplierPositive: 'The {effort} reasoning effort multiplier must be a finite number greater than 0', + reasoningEffortLevelInvalid: 'Unsupported reasoning effort: {effort}', multiplierPlaceholder: 'Not set', - multiplierPositive: 'Fast/Flex/Max effort multipliers must be greater than 0', + multiplierPositive: 'Fast/Flex multipliers must be finite numbers greater than 0', inputMultiplier: 'Input Mult.', outputMultiplier: 'Output Mult.', cacheWriteMultiplier: 'Cache Write Mult.', diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts index 76be95b4d2..0a8d8d352a 100644 --- a/frontend/src/i18n/locales/en/dashboard.ts +++ b/frontend/src/i18n/locales/en/dashboard.ts @@ -673,8 +673,8 @@ export default { cacheReadShort: 'R', tierHint: 'The whole request is billed at the tier matching its total context (input + cache write + cache read)', tierHintMarginal: 'Only the portion above the threshold is billed at this tier; output is unaffected', - maxReasoningMultiplierBadge: 'Max ×{multiplier}', - maxReasoningMultiplierHint: 'When the forwarded reasoning effort is max, billing and quota usage for the request are multiplied by {multiplier}', + reasoningMultiplierBadge: '{effort} ×{multiplier}', + reasoningMultiplierHint: 'When the forwarded reasoning effort is {effort}, billing and quota usage for the request are multiplied by {multiplier}. Unconfigured levels use 1×', marginalBadge: 'excess-only tiers', timePricingRowHint: 'Requests made within this period ({timezone} time) are billed at the prices in this row', timePricingRowHintWeekdays: diff --git a/frontend/src/i18n/locales/zh/admin/channels.ts b/frontend/src/i18n/locales/zh/admin/channels.ts index 7680091195..6d841f9323 100644 --- a/frontend/src/i18n/locales/zh/admin/channels.ts +++ b/frontend/src/i18n/locales/zh/admin/channels.ts @@ -140,10 +140,15 @@ export default { pricePlaceholder: '默认', fastMultiplier: 'Fast 倍率', flexMultiplier: 'Flex 倍率', - maxReasoningEffortMultiplier: 'Max 推理倍率', - fable51DefaultMaxReasoningMultiplier: '默认 3', + reasoningEffortMultipliers: '自定义思考等级倍率(可选)', + reasoningEffortMultipliersHint: '按请求最终转发的思考等级,对整次请求的费用与额度消耗应用对应倍率。未配置的等级按 1 倍计费,可与其他计费倍率叠加。', + reasoningEffortMultiplierLabel: '{effort} 思考等级倍率', + reasoningEffortMultiplierDefault: '默认 1', + clearReasoningEffortMultipliers: '清空倍率', + reasoningEffortMultiplierPositive: '{effort} 思考等级倍率必须为大于 0 的有限数字', + reasoningEffortLevelInvalid: '不支持的思考等级:{effort}', multiplierPlaceholder: '未配置', - multiplierPositive: 'Fast/Flex/Max 推理倍率必须大于 0', + multiplierPositive: 'Fast/Flex 倍率必须为大于 0 的有限数字', inputMultiplier: '输入倍率', outputMultiplier: '输出倍率', cacheWriteMultiplier: '缓存写倍率', diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts index d74ff78d99..7393964b0f 100644 --- a/frontend/src/i18n/locales/zh/dashboard.ts +++ b/frontend/src/i18n/locales/zh/dashboard.ts @@ -678,8 +678,8 @@ export default { cacheReadShort: '读', tierHint: '按单次请求的总上下文(输入 + 缓存写入 + 缓存读取)所在档位对整单计价', tierHintMarginal: '仅超过阈值的部分按该档计价,输出不加价', - maxReasoningMultiplierBadge: 'Max ×{multiplier}', - maxReasoningMultiplierHint: '最终转发的推理强度为 max 时,整次请求的计费与额度消耗乘以 {multiplier}', + reasoningMultiplierBadge: '{effort} ×{multiplier}', + reasoningMultiplierHint: '最终转发的思考等级为 {effort} 时,整次请求的计费与额度消耗乘以 {multiplier};未配置的等级按 1 倍计费', marginalBadge: '超出部分计价', timePricingRowHint: '按 {timezone} 时间,在该时段内发起的请求按本行价格计费', timePricingRowHintWeekdays: diff --git a/frontend/src/views/admin/ChannelsView.vue b/frontend/src/views/admin/ChannelsView.vue index ace4b14519..6e5e351d6a 100644 --- a/frontend/src/views/admin/ChannelsView.vue +++ b/frontend/src/views/admin/ChannelsView.vue @@ -634,7 +634,7 @@ import { extractApiErrorMessage } from '@/utils/apiError' import { adminAPI } from '@/api/admin' import type { Channel, ChannelModelPricing, CreateChannelRequest, UpdateChannelRequest, AccountStatsPricingRule } from '@/api/admin/channels' import type { PricingFormEntry } from '@/components/admin/channel/types' -import { apiIntervalsToForm, apiTimePricingToForm, createDefaultTimePricingForm, findModelConflict, formIntervalsToAPI, formTimePricingToAPI, isValidPositiveMultiplier, mTokToPerToken, perTokenToMTok, validateIntervals, validateTimePricing } from '@/components/admin/channel/types' +import { apiIntervalsToForm, apiTimePricingToForm, createDefaultTimePricingForm, findModelConflict, formIntervalsToAPI, formReasoningEffortMultipliersToAPI, formTimePricingToAPI, isValidPositiveMultiplier, mTokToPerToken, perTokenToMTok, validateIntervals, validateReasoningEffortMultipliers, validateTimePricing } from '@/components/admin/channel/types' import type { AdminGroup, GroupPlatform } from '@/types' import type { Column } from '@/components/common/types' import { platformTextClass, platformBadgeLightClass } from '@/utils/platformColors' @@ -865,7 +865,7 @@ function addPricingEntry(sectionIdx: number) { cache_read_price: null, fast_multiplier: null, flex_multiplier: null, - max_reasoning_effort_multiplier: null, + reasoning_effort_multipliers: null, image_input_price: null, image_output_price: null, per_request_price: null, @@ -903,7 +903,7 @@ async function syncLatestModels(sectionIdx: number) { cache_read_price: null, fast_multiplier: null, flex_multiplier: null, - max_reasoning_effort_multiplier: null, + reasoning_effort_multipliers: null, image_input_price: null, image_output_price: null, per_request_price: null, @@ -1089,6 +1089,7 @@ function accountStatsRulesToAPI(): AccountStatsPricingRule[] { cache_write_price: mTokToPerToken(p.cache_write_price), cache_write_1h_price: mTokToPerToken(p.cache_write_1h_price), cache_read_price: mTokToPerToken(p.cache_read_price), + reasoning_effort_multipliers: formReasoningEffortMultipliersToAPI(p.reasoning_effort_multipliers), image_input_price: mTokToPerToken(p.image_input_price), image_output_price: mTokToPerToken(p.image_output_price), per_request_price: p.per_request_price != null && p.per_request_price !== '' ? Number(p.per_request_price) : null, @@ -1134,7 +1135,7 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[ cache_read_price: mTokToPerToken(entry.cache_read_price), fast_multiplier: entry.fast_multiplier != null && entry.fast_multiplier !== '' ? Number(entry.fast_multiplier) : null, flex_multiplier: entry.flex_multiplier != null && entry.flex_multiplier !== '' ? Number(entry.flex_multiplier) : null, - max_reasoning_effort_multiplier: entry.max_reasoning_effort_multiplier != null && entry.max_reasoning_effort_multiplier !== '' ? Number(entry.max_reasoning_effort_multiplier) : null, + reasoning_effort_multipliers: formReasoningEffortMultipliersToAPI(entry.reasoning_effort_multipliers), image_input_price: mTokToPerToken(entry.image_input_price), image_output_price: mTokToPerToken(entry.image_output_price), per_request_price: entry.per_request_price != null && entry.per_request_price !== '' ? Number(entry.per_request_price) : null, @@ -1237,7 +1238,7 @@ function apiToForm(channel: Channel): PlatformSection[] { cache_read_price: perTokenToMTok(p.cache_read_price), fast_multiplier: p.fast_multiplier, flex_multiplier: p.flex_multiplier, - max_reasoning_effort_multiplier: p.max_reasoning_effort_multiplier, + reasoning_effort_multipliers: p.reasoning_effort_multipliers ? { ...p.reasoning_effort_multipliers } : null, image_input_price: perTokenToMTok(p.image_input_price), image_output_price: perTokenToMTok(p.image_output_price), per_request_price: p.per_request_price, @@ -1429,6 +1430,7 @@ function distributeRulesToPlatforms(apiRules: AccountStatsPricingRule[]) { cache_write_price: perTokenToMTok(p.cache_write_price), cache_write_1h_price: perTokenToMTok(p.cache_write_1h_price), cache_read_price: perTokenToMTok(p.cache_read_price), + reasoning_effort_multipliers: p.reasoning_effort_multipliers ? { ...p.reasoning_effort_multipliers } : null, image_input_price: perTokenToMTok(p.image_input_price), image_output_price: perTokenToMTok(p.image_output_price), per_request_price: p.per_request_price, @@ -1541,12 +1543,28 @@ async function handleSubmit() { } } + // 思考等级倍率同时适用于渠道计价和独立的账号统计计价规则。 + for (const section of form.platforms.filter(s => s.enabled)) { + const entries = [ + ...section.model_pricing, + ...section.account_stats_pricing_rules.flatMap(rule => rule.pricing), + ] + for (const entry of entries) { + const error = validateReasoningEffortMultipliers(entry.reasoning_effort_multipliers, t) + if (!error) continue + const platformLabel = t('admin.groups.platforms.' + section.platform, section.platform) + const modelLabel = entry.models.join(', ') || t('admin.channels.form.unnamed') + appStore.showError(`${platformLabel} - ${modelLabel}: ${error}`) + activeTab.value = section.platform + return + } + } + // 校验区间合法性(范围、重叠等) for (const section of form.platforms.filter(s => s.enabled)) { for (const entry of section.model_pricing) { if (!isValidPositiveMultiplier(entry.fast_multiplier) || - !isValidPositiveMultiplier(entry.flex_multiplier) || - !isValidPositiveMultiplier(entry.max_reasoning_effort_multiplier)) { + !isValidPositiveMultiplier(entry.flex_multiplier)) { const platformLabel = t('admin.groups.platforms.' + section.platform, section.platform) const modelLabel = entry.models.join(', ') || t('admin.channels.form.unnamed') appStore.showError(`${platformLabel} - ${modelLabel}: ${t('admin.channels.form.multiplierPositive')}`) diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 85da60ed41..471c4be44c 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -4309,9 +4309,11 @@ import { apiIntervalsToForm, createDefaultTimePricingForm, formIntervalsToAPI, + formReasoningEffortMultipliersToAPI, mTokToPerToken, perTokenToMTok, toNullableNumber, + validateReasoningEffortMultipliers, } from "@/components/admin/channel/types"; import type { ChannelModelPricing } from "@/api/admin/channels"; import { VueDraggable } from "vue-draggable-plus"; @@ -4386,6 +4388,7 @@ const emptyGroupPricing = (): PricingFormEntry => ({ cache_write_price: null, cache_write_1h_price: null, cache_read_price: null, + reasoning_effort_multipliers: null, image_input_price: null, image_output_price: null, per_request_price: null, @@ -4407,6 +4410,9 @@ const groupPricingFromAPI = ( cache_write_price: perTokenToMTok(entry.cache_write_price), cache_write_1h_price: perTokenToMTok(entry.cache_write_1h_price), cache_read_price: perTokenToMTok(entry.cache_read_price), + reasoning_effort_multipliers: entry.reasoning_effort_multipliers + ? { ...entry.reasoning_effort_multipliers } + : null, image_input_price: perTokenToMTok(entry.image_input_price), image_output_price: perTokenToMTok(entry.image_output_price), per_request_price: entry.per_request_price, @@ -4429,6 +4435,9 @@ const groupPricingToAPI = ( cache_write_price: mTokToPerToken(entry.cache_write_price), cache_write_1h_price: mTokToPerToken(entry.cache_write_1h_price), cache_read_price: mTokToPerToken(entry.cache_read_price), + reasoning_effort_multipliers: formReasoningEffortMultipliersToAPI( + entry.reasoning_effort_multipliers, + ), image_input_price: mTokToPerToken(entry.image_input_price), image_output_price: mTokToPerToken(entry.image_output_price), per_request_price: toNullableNumber(entry.per_request_price), @@ -5857,6 +5866,17 @@ const validateProfitControlForm = (form: ProfitControlFormState): boolean => { return true; }; +const validateGroupReasoningMultipliers = (pricing: PricingFormEntry[]): boolean => { + for (const entry of pricing) { + const error = validateReasoningEffortMultipliers(entry.reasoning_effort_multipliers, t); + if (error) { + appStore.showError(`${entry.models.join(", ") || t("admin.channels.form.unnamed")}: ${error}`); + return false; + } + } + return true; +}; + const handleCreateGroup = async () => { if (!createForm.name.trim()) { appStore.showError(t("admin.groups.nameRequired")); @@ -5872,6 +5892,7 @@ const handleCreateGroup = async () => { if (!validateProfitControlForm(createForm)) { return; } + if (!validateGroupReasoningMultipliers(createForm.model_pricing)) return; // 模型白名单:开启且没有任何条目时阻止提交,与后端 400 对齐。 if ( createModelAllowlistState.enabled && @@ -6203,6 +6224,7 @@ const handleUpdateGroup = async () => { if (!validateProfitControlForm(editForm)) { return; } + if (!validateGroupReasoningMultipliers(editForm.model_pricing)) return; // 模型白名单:开启且没有任何条目时阻止提交,与后端 400 对齐。 if ( editModelAllowlistState.enabled && diff --git a/frontend/src/views/admin/__tests__/GroupsView.duplicate.spec.ts b/frontend/src/views/admin/__tests__/GroupsView.duplicate.spec.ts index e10e5b9869..d125725228 100644 --- a/frontend/src/views/admin/__tests__/GroupsView.duplicate.spec.ts +++ b/frontend/src/views/admin/__tests__/GroupsView.duplicate.spec.ts @@ -325,6 +325,65 @@ describe('GroupsView duplicate action', () => { wrapper.unmount() }) + it('loads, edits, and saves custom reasoning multipliers for group pricing', async () => { + const group = { + ...sourceGroup, + model_pricing: [{ + platform: 'openai', models: ['example-model'], billing_mode: 'token', + input_price: 3e-6, output_price: 15e-6, cache_write_price: null, cache_read_price: null, + image_input_price: null, image_output_price: null, per_request_price: null, + reasoning_effort_multipliers: { high: 1.5, max: 3 }, intervals: [], time_pricing: null, + }], + } + listGroups.mockResolvedValue({ items: [group], total: 1, page: 1, page_size: 20, pages: 1 }) + updateGroup.mockResolvedValue(group) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === 'common.edit')!.trigger('click') + await flushPromises() + + expect(wrapper.get('[data-reasoning-effort="high"]').element.value).toBe('1.5') + await wrapper.get('[data-reasoning-effort="high"]').setValue('0.5') + await wrapper.get('[data-reasoning-effort="max"]').setValue('') + await wrapper.get('#edit-group-form').trigger('submit') + await flushPromises() + expect(updateGroup).toHaveBeenCalledWith(42, expect.objectContaining({ + model_pricing: [expect.objectContaining({ reasoning_effort_multipliers: { high: 0.5 } })], + })) + wrapper.unmount() + }) + + it('blocks saving an invalid group reasoning multiplier and allows clearing it', async () => { + const group = { + ...sourceGroup, + model_pricing: [{ + platform: 'openai', models: ['example-model'], billing_mode: 'token', + input_price: 3e-6, output_price: 15e-6, cache_write_price: null, cache_read_price: null, + image_input_price: null, image_output_price: null, per_request_price: null, + reasoning_effort_multipliers: { high: 1.5 }, intervals: [], time_pricing: null, + }], + } + listGroups.mockResolvedValue({ items: [group], total: 1, page: 1, page_size: 20, pages: 1 }) + updateGroup.mockResolvedValue(group) + const wrapper = mountView() + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === 'common.edit')!.trigger('click') + await flushPromises() + + await wrapper.get('[data-reasoning-effort="high"]').setValue('0') + await wrapper.get('#edit-group-form').trigger('submit') + expect(updateGroup).not.toHaveBeenCalled() + expect(showError).toHaveBeenCalledWith(expect.stringContaining('reasoningEffortMultiplierPositive')) + + await wrapper.get('[data-testid="reasoning-effort-multipliers"] button').trigger('click') + await wrapper.get('#edit-group-form').trigger('submit') + await flushPromises() + expect(updateGroup).toHaveBeenCalledWith(42, expect.objectContaining({ + model_pricing: [expect.objectContaining({ reasoning_effort_multipliers: null })], + })) + wrapper.unmount() + }) + it('updates manifest controls immediately and submits the displayed selection', async () => { vi.useFakeTimers() vi.mocked(adminAPI.accounts.list).mockResolvedValue({