Merge pull request #6514 from StarryKira/codex/anthropic-reasoning-pricing

feat: add Anthropic reasoning effort and Fable 5.1 max pricing
This commit is contained in:
Wesley Liddick
2026-09-05 16:19:18 +08:00
committed by GitHub
47 changed files with 664 additions and 247 deletions
@@ -57,21 +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"`
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"`
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"`
}
type channelTimePricingRequest struct {
@@ -129,22 +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"`
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"`
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"`
}
type channelTimePricingResponse struct {
@@ -258,22 +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,
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,
MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
ImageInputPrice: p.ImageInputPrice,
ImageOutputPrice: p.ImageOutputPrice,
PerRequestPrice: p.PerRequestPrice,
Intervals: intervals,
TimePricing: timePricingToResponse(p.TimePricing),
}
}
@@ -350,27 +353,29 @@ func pricingRequestToService(reqs []channelModelPricingRequest, allowChannelMult
SortOrder: iv.SortOrder,
})
}
var fastMultiplier, flexMultiplier *float64
var fastMultiplier, flexMultiplier, maxReasoningEffortMultiplier *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,
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,
MaxReasoningEffortMultiplier: maxReasoningEffortMultiplier,
ImageInputPrice: r.ImageInputPrice,
ImageOutputPrice: r.ImageOutputPrice,
PerRequestPrice: r.PerRequestPrice,
Intervals: intervals,
TimePricing: timePricingRequestToService(r.TimePricing),
})
}
return result
@@ -615,14 +620,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,
"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,
"max_reasoning_effort_multiplier": pricing.MaxReasoningEffortMultiplier,
"image_input_price": pricing.ImageInputPricePerToken,
"image_output_price": pricing.ImageOutputPricePerToken,
})
}
@@ -453,10 +453,11 @@ func TestPricingRequestToService_TimePricingNil(t *testing.T) {
// 避免渠道倍率意外污染账号成本口径。
func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
req := channelModelPricingRequest{
Models: []string{"gpt-5"},
BillingMode: "token",
FastMultiplier: float64Ptr(2.5),
FlexMultiplier: float64Ptr(0.5),
Models: []string{"gpt-5"},
BillingMode: "token",
FastMultiplier: float64Ptr(2.5),
FlexMultiplier: float64Ptr(0.5),
MaxReasoningEffortMultiplier: float64Ptr(3),
Intervals: []pricingIntervalRequest{{
MinTokens: 272000,
InputMultiplier: float64Ptr(2),
@@ -469,6 +470,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, 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)
@@ -477,6 +479,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.Nil(t, dropped[0].Intervals[0].InputMultiplier)
require.Nil(t, dropped[0].Intervals[0].OutputMultiplier)
require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier)
@@ -580,9 +583,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"`
Found bool `json:"found"`
CacheWritePrice float64 `json:"cache_write_price"`
CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
@@ -590,6 +594,8 @@ 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)
}
func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) {
@@ -158,11 +158,11 @@ type CreateGroupRequest struct {
CodexModelsManifestConfig service.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config"`
// 分组 RPM 上限(0 = 不限制)
RPMLimit int `json:"rpm_limit"`
// OpenAI/Codex 请求推理强度上限,空字符串表示不限制。
// Anthropic/OpenAI 请求推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort"`
// 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit"`
// OpenAI/Codex 推理强度映射,可按模型精确名、前缀或后缀限定。
// Anthropic/OpenAI 推理强度映射,可按模型精确名、前缀或后缀限定。
ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
// 从指定分组复制账号(创建后自动绑定)
CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
@@ -233,7 +233,7 @@ type UpdateGroupRequest struct {
CodexModelsManifestConfig *service.GroupCodexModelsManifestConfig `json:"codex_models_manifest_config"`
// 分组 RPM 上限(0 = 不限制);nil 表示未提供不改动
RPMLimit *int `json:"rpm_limit"`
// OpenAI/Codex 请求推理强度上限;空字符串清除,nil 不修改。
// Anthropic/OpenAI 请求推理强度上限;空字符串清除,nil 不修改。
MaxReasoningEffort *string `json:"max_reasoning_effort"`
// 超过上限时的访问控制;空字符串视为 downgrade,nil 不修改。
MaxReasoningEffortOverLimit *string `json:"max_reasoning_effort_over_limit"`
@@ -66,16 +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"`
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"`
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"`
}
// userPricingIntervalDTO 定价区间白名单(去掉内部 ID、SortOrder 等前端不渲染的字段)。
@@ -323,15 +324,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,
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,
MaxReasoningEffortMultiplier: p.MaxReasoningEffortMultiplier,
ImageInputPrice: p.ImageInputPrice,
ImageOutputPrice: p.ImageOutputPrice,
PerRequestPrice: p.PerRequestPrice,
Intervals: intervals,
}
}
+43 -3
View File
@@ -64,13 +64,45 @@ func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKe
if apiKey == nil || apiKey.Group == nil {
return "", nil, "", false
}
if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
if apiKey.Group.Platform != service.PlatformAnthropic && apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
return "", nil, "", false
}
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformOpenAI {
effectivePlatform := effectiveAPIKeyPlatform(c, apiKey)
if effectivePlatform != service.PlatformAnthropic && effectivePlatform != service.PlatformOpenAI {
return "", nil, "", false
}
return apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings, apiKey.Group.MaxReasoningEffortOverLimit, true
maxEffort, mappings := apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings
if effectivePlatform == service.PlatformAnthropic {
maxEffort, mappings = anthropicCompatibleReasoningEffortPolicy(maxEffort, mappings)
}
return maxEffort, mappings, apiKey.Group.MaxReasoningEffortOverLimit, true
}
func anthropicReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, string, bool) {
if apiKey == nil || apiKey.Group == nil {
return "", nil, "", false
}
if apiKey.Group.Platform != service.PlatformAnthropic && apiKey.Group.Platform != service.PlatformComposite {
return "", nil, "", false
}
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformAnthropic {
return "", nil, "", false
}
maxEffort, mappings := anthropicCompatibleReasoningEffortPolicy(apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings)
return maxEffort, mappings, apiKey.Group.MaxReasoningEffortOverLimit, true
}
func anthropicCompatibleReasoningEffortPolicy(maxEffort string, mappings []service.ReasoningEffortMapping) (string, []service.ReasoningEffortMapping) {
if service.NormalizeMaxReasoningEffort(maxEffort) == "minimal" {
maxEffort = "low"
}
normalizedMappings := append([]service.ReasoningEffortMapping(nil), mappings...)
for i := range normalizedMappings {
if service.NormalizeMaxReasoningEffort(normalizedMappings[i].To) == "minimal" {
normalizedMappings[i].To = "low"
}
}
return maxEffort, normalizedMappings
}
func bindRequestedReasoningEffort(c *gin.Context, body []byte, model string) {
@@ -118,6 +150,14 @@ func respondOpenAIReasoningEffortPolicyError(c *gin.Context, err error, write fu
write(c, http.StatusForbidden, "permission_error", err.Error())
}
func applyAnthropicReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool, error) {
maxEffort, mappings, overLimit, ok := anthropicReasoningEffortPolicyForRequest(c, apiKey)
if !ok {
return body, false, nil
}
return service.ApplyReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
}
func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *service.APIKey, body []byte) {
if c == nil || c.Request == nil {
return
@@ -8,6 +8,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestCompositeTargetPlatformAllowedResolvesKnownAllowedModel(t *testing.T) {
@@ -160,6 +161,75 @@ func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) {
require.Equal(t, body, got)
}
func TestOpenAIShapedReasoningEffortPolicyForAnthropicGroup(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest("POST", "/v1/responses", nil)
apiKey := &service.APIKey{Group: &service.Group{
Platform: service.PlatformAnthropic,
MaxReasoningEffort: "xhigh",
}}
got, changed, err := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, []byte(`{"reasoning":{"effort":"max"}}`))
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, "xhigh", gjson.GetBytes(got, "reasoning.effort").String())
}
func TestAnthropicReasoningEffortPolicyForConcreteAndCompositeTargets(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"claude-fable-5-1","output_config":{"effort":"max"}}`)
policy := func(platform string) *service.APIKey {
return &service.APIKey{Group: &service.Group{
Platform: platform,
MaxReasoningEffort: "xhigh",
ReasoningEffortMappings: []service.ReasoningEffortMapping{
{From: "max", To: "xhigh"},
},
}}
}
concrete, _ := gin.CreateTestContext(httptest.NewRecorder())
concrete.Request = httptest.NewRequest("POST", "/v1/messages", nil)
got, changed, err := applyAnthropicReasoningEffortPolicyForRequest(concrete, policy(service.PlatformAnthropic), body)
require.NoError(t, err)
require.True(t, changed)
require.JSONEq(t, `{"model":"claude-fable-5-1","output_config":{"effort":"xhigh"}}`, string(got))
denyMax := policy(service.PlatformAnthropic)
denyMax.Group.ReasoningEffortMappings = nil
denyMax.Group.MaxReasoningEffortOverLimit = service.ReasoningEffortOverLimitDeny
_, changed, err = applyAnthropicReasoningEffortPolicyForRequest(concrete, denyMax, body)
require.Error(t, err)
require.False(t, changed)
var overLimit *service.ReasoningEffortOverLimitError
require.ErrorAs(t, err, &overLimit)
require.Equal(t, "max", overLimit.Requested)
require.Equal(t, "xhigh", overLimit.Max)
composite, _ := gin.CreateTestContext(httptest.NewRecorder())
composite.Request = httptest.NewRequest("POST", "/v1/messages", nil)
composite.Request = composite.Request.WithContext(service.WithResolvedTargetPlatform(composite.Request.Context(), service.PlatformAnthropic))
got, changed, err = applyAnthropicReasoningEffortPolicyForRequest(composite, policy(service.PlatformComposite), body)
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, "xhigh", gjson.GetBytes(got, "output_config.effort").String())
openAI, _ := gin.CreateTestContext(httptest.NewRecorder())
openAI.Request = httptest.NewRequest("POST", "/v1/messages", nil)
openAI.Request = openAI.Request.WithContext(service.WithResolvedTargetPlatform(openAI.Request.Context(), service.PlatformOpenAI))
got, changed, err = applyAnthropicReasoningEffortPolicyForRequest(openAI, policy(service.PlatformComposite), body)
require.NoError(t, err)
require.False(t, changed)
require.Equal(t, body, got)
}
func TestAnthropicCompositePolicyClampsUnsupportedMinimalToLow(t *testing.T) {
maxEffort, mappings := anthropicCompatibleReasoningEffortPolicy("minimal", []service.ReasoningEffortMapping{{From: "max", To: "minimal"}})
require.Equal(t, "low", maxEffort)
require.Equal(t, []service.ReasoningEffortMapping{{From: "max", To: "low"}}, mappings)
}
func TestClientRequestedModelUsesCompositePublicModel(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
+2 -2
View File
@@ -151,11 +151,11 @@ type Group struct {
// RPMLimit 分组级每分钟请求数上限(0 = 不限制),设置后覆盖用户级 rpm_limit。
RPMLimit int `json:"rpm_limit"`
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
// MaxReasoningEffort Anthropic/OpenAI 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort"`
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit"`
// ReasoningEffortMappings OpenAI/Codex 推理强度映射,可按模型精确名、前缀或后缀限定。
// ReasoningEffortMappings Anthropic/OpenAI 推理强度映射,可按模型精确名、前缀或后缀限定。
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
CreatedAt time.Time `json:"created_at"`
@@ -171,6 +171,19 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
reqStream := parsedReq.Stream
bindRequestedReasoningEffort(c, body, reqModel)
ensureCompositeTargetPlatform(c, apiKey, reqModel)
if policyBody, changed, err := applyAnthropicReasoningEffortPolicyForRequest(c, apiKey, body); err != nil {
respondOpenAIReasoningEffortPolicyError(c, err, h.errorResponse)
return
} else if changed {
if err := parsedReq.ReplaceBody(policyBody); err != nil {
reqLog.Warn("gateway.reasoning_effort_policy_parse_failed", zap.Error(err))
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to apply reasoning effort policy")
return
}
body = parsedReq.Body.Bytes()
reqModel = parsedReq.Model
reqStream = parsedReq.Stream
}
reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
// 解析渠道级模型映射
@@ -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, 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, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at
FROM channel_model_pricing WHERE channel_id = $1 ORDER BY id`, channelID,
)
if err != nil {
@@ -61,10 +61,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, image_input_price = $10, image_output_price = $11, per_request_price = $12, time_pricing = $13, platform = $14, updated_at = NOW()
WHERE id = $15`,
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()
WHERE id = $16`,
modelsJSON, billingMode, pricing.InputPrice, pricing.OutputPrice, pricing.CacheWritePrice, pricing.CacheWrite1hPrice, pricing.CacheReadPrice,
pricing.FastMultiplier, pricing.FlexMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice,
pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice, pricing.PerRequestPrice,
timePricingJSON, pricing.Platform, pricing.ID,
)
if err != nil {
@@ -96,7 +96,7 @@ func (r *channelRepository) ReplaceModelPricing(ctx context.Context, channelID i
// batchLoadModelPricing 批量加载多个渠道的模型定价(含区间)
func (r *channelRepository) batchLoadModelPricing(ctx context.Context, channelIDs []int64) (map[int64][]service.ChannelModelPricing, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT id, channel_id, platform, models, billing_mode, input_price, output_price, cache_write_price, cache_write_1h_price, cache_read_price, fast_multiplier, flex_multiplier, image_input_price, image_output_price, per_request_price, time_pricing, created_at, updated_at
`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
FROM channel_model_pricing WHERE channel_id = ANY($1) ORDER BY channel_id, id`,
pq.Array(channelIDs),
)
@@ -180,7 +180,7 @@ func scanModelPricingRows(rows *sql.Rows) ([]service.ChannelModelPricing, []int6
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.FastMultiplier, &p.FlexMultiplier, &p.MaxReasoningEffortMultiplier,
&p.ImageInputPrice, &p.ImageOutputPrice, &p.PerRequestPrice, &timePricingJSON, &p.CreatedAt, &p.UpdatedAt,
); err != nil {
return nil, nil, fmt.Errorf("scan model pricing: %w", err)
@@ -247,11 +247,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, image_input_price, image_output_price, per_request_price, time_pricing)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at`,
`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)
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.ImageInputPrice, pricing.ImageOutputPrice,
pricing.FastMultiplier, pricing.FlexMultiplier, pricing.MaxReasoningEffortMultiplier, pricing.ImageInputPrice, pricing.ImageOutputPrice,
pricing.PerRequestPrice, timePricingJSON,
).Scan(&pricing.ID, &pricing.CreatedAt, &pricing.UpdatedAt)
if err != nil {
@@ -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", "image_input_price", "image_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",
}
@@ -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, timePricing,
nil, 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, 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, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")).
WithArgs(
int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, channelModelPricingTimePricingJSON,
).
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{}))
@@ -118,10 +118,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateRoundTrip(t *testing.T) {
t.Run("update writes JSON and entry ID", func(t *testing.T) {
repo, mock := newChannelModelPricingTimePricingRepo(t)
mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$12, time_pricing = \$13, platform = \$14.*WHERE id = \$15`).
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, channelModelPricingTimePricingJSON, "openai", int64(11),
nil, 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, 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, max_reasoning_effort_multiplier, image_input_price, image_output_price, per_request_price, time_pricing)")).
WithArgs(
int64(7), "openai", []byte(`["gpt-5"]`), service.BillingModeToken,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
).
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(int64(11), time.Time{}, time.Time{}))
@@ -166,10 +166,10 @@ func TestChannelModelPricingTimePricingCreateAndUpdateWriteNullWhenDisabled(t *t
t.Run("update writes SQL NULL", func(t *testing.T) {
repo, mock := newChannelModelPricingTimePricingRepo(t)
mock.ExpectExec(`(?s)UPDATE channel_model_pricing.*per_request_price = \$12, time_pricing = \$13, platform = \$14.*WHERE id = \$15`).
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, "openai", int64(11),
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "openai", int64(11),
).
WillReturnResult(sqlmock.NewResult(0, 1))
@@ -17,6 +17,7 @@ import (
// upstreamModel 是最终发往上游的模型 ID。
// totalCost 是本次请求的客户计费(倍率前),用于优先级 2。
// serviceTier 是最终参与用户计费的 OpenAI 服务层级,用于优先级 3。
// reasoningEffort 是最终转发等级;Fable 5.1 max 默认按 3 倍额度消耗。
func resolveAccountStatsCost(
ctx context.Context,
channelService *ChannelService,
@@ -28,7 +29,12 @@ func resolveAccountStatsCost(
requestCount int,
totalCost float64,
serviceTier string,
reasoningEfforts ...string,
) *float64 {
reasoningEffort := ""
if len(reasoningEfforts) > 0 {
reasoningEffort = reasoningEfforts[0]
}
if channelService == nil || upstreamModel == "" {
return nil
}
@@ -40,7 +46,7 @@ func resolveAccountStatsCost(
platform := channelService.GetGroupPlatform(ctx, groupID)
// 优先级 1:自定义规则(始终尝试)
if cost := tryCustomRules(channel, accountID, groupID, platform, upstreamModel, tokens, requestCount); cost != nil {
if cost := tryCustomRules(channel, accountID, groupID, platform, upstreamModel, tokens, requestCount, reasoningEffort); cost != nil {
return cost
}
@@ -55,7 +61,7 @@ func resolveAccountStatsCost(
// 优先级 3:模型定价文件(LiteLLM)默认价格
if billingService != nil {
return tryModelFilePricing(billingService, upstreamModel, tokens, serviceTier)
return tryModelFilePricing(billingService, upstreamModel, tokens, serviceTier, reasoningEffort)
}
return nil
@@ -65,13 +71,18 @@ func resolveAccountStatsCost(
// 与用户计费共用同一条定价管线,避免这里维护第二份"单价 × token 数"实现后,
// 每加一个定价特性都要手工镜像一次。channelPricing 为 nil,保持优先级 3 的
// 语义:只取模型定价文件,不引入渠道自定义定价。
func tryModelFilePricing(billingService *BillingService, model string, tokens UsageTokens, serviceTier string) *float64 {
func tryModelFilePricing(billingService *BillingService, model string, tokens UsageTokens, serviceTier string, reasoningEfforts ...string) *float64 {
reasoningEffort := ""
if len(reasoningEfforts) > 0 {
reasoningEffort = reasoningEfforts[0]
}
breakdown, err := billingService.CalculateCostWithServiceTier(
model, tokens, 1, normalizeBillingServiceTier(serviceTier),
)
if err != nil || breakdown == nil || breakdown.TotalCost <= 0 {
return nil
}
applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(model, reasoningEffort, nil))
return &breakdown.TotalCost
}
@@ -79,7 +90,12 @@ func tryModelFilePricing(billingService *BillingService, model string, tokens Us
func tryCustomRules(
channel *Channel, accountID, groupID int64,
platform, model string, tokens UsageTokens, requestCount int,
reasoningEfforts ...string,
) *float64 {
reasoningEffort := ""
if len(reasoningEfforts) > 0 {
reasoningEffort = reasoningEfforts[0]
}
modelLower := strings.ToLower(model)
for _, rule := range channel.AccountStatsPricingRules {
if !matchAccountStatsRule(&rule, accountID, groupID) {
@@ -89,7 +105,11 @@ func tryCustomRules(
if pricing == nil {
continue // 规则匹配但模型不在规则定价中,继续下一条
}
return calculateStatsCost(pricing, tokens, requestCount)
cost := calculateStatsCost(pricing, tokens, requestCount)
if cost != nil {
*cost *= maxReasoningEffortBillingMultiplier(model, reasoningEffort, nil)
}
return cost
}
return nil
}
@@ -243,10 +263,14 @@ func applyAccountStatsCost(
requestCount = usageLog.ImageCount
}
serviceTier := ""
reasoningEffort := ""
if usageLog != nil && usageLog.ServiceTier != nil {
serviceTier = *usageLog.ServiceTier
}
if usageLog != nil && usageLog.ReasoningEffort != nil {
reasoningEffort = *usageLog.ReasoningEffort
}
usageLog.AccountStatsCost = resolveAccountStatsCost(
ctx, cs, bs, accountID, groupID, model, tokens, requestCount, totalCost, serviceTier,
ctx, cs, bs, accountID, groupID, model, tokens, requestCount, totalCost, serviceTier, reasoningEffort,
)
}
@@ -478,6 +478,18 @@ func TestTryModelFilePricing_Success(t *testing.T) {
require.InDelta(t, 0.2, *result, 1e-12)
}
func TestTryModelFilePricing_Fable51MaxEffortUsesTripleQuota(t *testing.T) {
bs := newTestBillingServiceWithPrices(map[string]*ModelPricing{
"claude-fable-5-1": {InputPricePerToken: 0.001},
})
tokens := UsageTokens{InputTokens: 100}
standard := tryModelFilePricing(bs, "claude-fable-5-1", tokens, "", "xhigh")
max := tryModelFilePricing(bs, "claude-fable-5-1", tokens, "", "max")
require.NotNil(t, standard)
require.NotNil(t, max)
require.InDelta(t, *standard*3, *max, 1e-12)
}
func TestTryModelFilePricing_AppliesLongContextPricing(t *testing.T) {
bs := newTestBillingServiceWithPrices(map[string]*ModelPricing{
"gpt-5.6-sol": {
+2 -2
View File
@@ -277,11 +277,11 @@ type CreateGroupInput struct {
CodexModelsManifestConfig GroupCodexModelsManifestConfig
// RPMLimit 分组 RPM 上限(0 = 不限制)
RPMLimit int
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
// MaxReasoningEffort Anthropic/OpenAI 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string
// ReasoningEffortMappings OpenAI/Codex 推理强度精确映射。
// ReasoningEffortMappings Anthropic/OpenAI 推理强度映射,可按模型精确名、前缀或后缀限定。
ReasoningEffortMappings []ReasoningEffortMapping
// 分组利润控制(五个 token 平台分组可启用;margin/buffer 为小数,nil 按 0 处理)
ProfitControlEnabled bool
@@ -960,7 +960,7 @@ func TestAdminService_UpdateGroup_ClearsReasoningPolicyForUnsupportedPlatform(t
repo := &groupRepoStubForAdmin{getByID: existing}
svc := &adminServiceImpl{groupRepo: repo}
_, err := svc.UpdateGroup(context.Background(), existing.ID, &UpdateGroupInput{Platform: PlatformAnthropic})
_, err := svc.UpdateGroup(context.Background(), existing.ID, &UpdateGroupInput{Platform: PlatformGemini})
require.NoError(t, err)
require.Empty(t, repo.updated.MaxReasoningEffort)
@@ -115,7 +115,7 @@ type APIKeyAuthGroupSnapshot struct {
// RPMLimit 分组级每分钟请求数上限(0 = 不限制);用于 billing_cache_service.checkRPM 级联判断。
RPMLimit int `json:"rpm_limit"`
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
// MaxReasoningEffort Anthropic/OpenAI 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit,omitempty"`
+53 -4
View File
@@ -105,6 +105,7 @@ type ModelPricing struct {
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 // 是否支持详细的缓存分类
@@ -214,6 +215,42 @@ 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"} {
if at := strings.Index(model, marker); at >= 0 {
after := at + len(marker)
if after == len(model) || model[after] < '0' || model[after] > '9' {
return true
}
}
}
return false
}
func defaultMaxReasoningEffortMultiplier(model string) *float64 {
if !isClaudeFable51Model(model) {
return nil
}
multiplier := claudeFable51MaxReasoningEffortMultiplier
return &multiplier
}
func maxReasoningEffortBillingMultiplier(model, effort string, pricing *ModelPricing) float64 {
if NormalizeMaxReasoningEffort(effort) != "max" {
return 1
}
if pricing != nil && pricing.MaxReasoningEffortMultiplier != nil && *pricing.MaxReasoningEffortMultiplier > 0 {
return *pricing.MaxReasoningEffortMultiplier
}
if multiplier := defaultMaxReasoningEffortMultiplier(model); multiplier != nil {
return *multiplier
}
return 1
}
func resolvedChannelTimeMultiplier(resolved *ResolvedPricing, at time.Time) float64 {
if resolved == nil || resolved.Source != PricingSourceChannel || resolved.channelPricing == nil {
return 1
@@ -803,8 +840,7 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
modelLower := strings.ToLower(model)
// 按模型系列匹配
if strings.Contains(modelLower, "fable-5-1") || strings.Contains(modelLower, "fable-5.1") ||
strings.Contains(modelLower, "fable5.1") || strings.Contains(modelLower, "fable51") {
if isClaudeFable51Model(modelLower) {
return s.fallbackPrices["claude-fable-5-1"]
}
if strings.Contains(modelLower, "fable-5") || strings.Contains(modelLower, "fable5") {
@@ -1169,6 +1205,9 @@ 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
}
if channelPricing.ImageOutputPrice != nil {
pricing.ImageOutputPricePerToken = *channelPricing.ImageOutputPrice
} else {
@@ -1241,6 +1280,7 @@ type CostInput struct {
RateMultiplier float64
PricingAt time.Time // 渠道分时定价使用的计费时刻
ServiceTier string // "priority","flex","" 等
ReasoningEffort string // 最终转发的推理等级;max 可触发模型/渠道倍率
Resolver *ModelPricingResolver // 定价解析器
Resolved *ResolvedPricing // 可选:预解析的定价结果(避免重复 Resolve 调用)
LongContextBillingEnabled *bool
@@ -1255,7 +1295,7 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown,
if input.LongContextBillingEnabled != nil {
applyLongContextBilling = *input.LongContextBillingEnabled
}
return s.calculateCostInternalWithPolicy(
breakdown, err := s.calculateCostInternalWithPolicy(
input.Model,
input.Tokens,
input.RateMultiplier,
@@ -1263,6 +1303,10 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown,
nil,
applyLongContextBilling,
)
if err == nil {
applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(input.Model, input.ReasoningEffort, nil))
}
return breakdown, err
}
// 优先使用预解析结果,避免重复 Resolve 调用
@@ -1346,6 +1390,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))
return breakdown, nil
}
@@ -1627,13 +1672,17 @@ 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 {
if !needsCacheCreationPolicy && fastRatio <= 0 && !needsMaxReasoningEffortMultiplier {
return pricing
}
cloned := *pricing
if needsMaxReasoningEffortMultiplier {
cloned.MaxReasoningEffortMultiplier = defaultMaxReasoningEffortMultiplier(model)
}
if isGPT56 && !cloned.CacheCreationPriceExplicit {
if cloned.CacheCreationPricePerToken <= 0 {
cloned.CacheCreationPricePerToken = cloned.InputPricePerToken * 1.25
@@ -1771,6 +1771,8 @@ 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)
}
func TestGetModelPricingWithChannel_CacheReadPriceAffectsPriority(t *testing.T) {
@@ -7,14 +7,15 @@ import (
// TokenCostRequest 通用网关 token 计费请求。
type TokenCostRequest struct {
Ctx context.Context
Model string
Group *Group
Tokens UsageTokens
RateMultiplier float64
PricingAt time.Time
ServiceTier string
Resolver *ModelPricingResolver
Ctx context.Context
Model string
Group *Group
Tokens UsageTokens
RateMultiplier float64
PricingAt time.Time
ServiceTier string
ReasoningEffort string
Resolver *ModelPricingResolver
// Resolved 为调用方预先解析的定价(Resolver.Resolve 的结果),nil 表示未解析。
Resolved *ResolvedPricing
}
@@ -33,21 +34,25 @@ func (s *BillingService) CalculateTokenCostForRequest(req TokenCostRequest) (*Co
if req.Resolver != nil && req.Group != nil {
return s.CalculateCostUnified(s.tokenCostInput(req, resolved))
}
if req.ReasoningEffort != "" {
return s.CalculateCostUnified(s.tokenCostInput(req, resolved))
}
return s.CalculateCost(req.Model, req.Tokens, req.RateMultiplier)
}
func (s *BillingService) tokenCostInput(req TokenCostRequest, resolved *ResolvedPricing) CostInput {
input := CostInput{
Ctx: req.Ctx,
Model: req.Model,
Group: req.Group,
Tokens: req.Tokens,
RequestCount: 1,
RateMultiplier: req.RateMultiplier,
PricingAt: req.PricingAt,
ServiceTier: req.ServiceTier,
Resolver: req.Resolver,
Resolved: resolved,
Ctx: req.Ctx,
Model: req.Model,
Group: req.Group,
Tokens: req.Tokens,
RequestCount: 1,
RateMultiplier: req.RateMultiplier,
PricingAt: req.PricingAt,
ServiceTier: req.ServiceTier,
ReasoningEffort: req.ReasoningEffort,
Resolver: req.Resolver,
Resolved: resolved,
}
if req.Group != nil {
gid := req.Group.ID
@@ -216,3 +216,41 @@ func TestCalculateTokenCostForRequest_NoResolverFallsBackToCatalog(t *testing.T)
require.NoError(t, err)
require.Equal(t, want, got)
}
func TestCalculateTokenCostForRequest_Fable51MaxEffortUsesDefaultMultiplier(t *testing.T) {
bs := NewBillingService(&config.Config{}, nil)
tokens := UsageTokens{InputTokens: 1000, OutputTokens: 10}
standard, err := bs.CalculateTokenCostForRequest(TokenCostRequest{
Model: "claude-fable-5-1", Tokens: tokens, RateMultiplier: 1, ReasoningEffort: "xhigh",
})
require.NoError(t, err)
max, err := bs.CalculateTokenCostForRequest(TokenCostRequest{
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)
}
func TestCalculateTokenCostForRequest_ChannelOverridesFable51MaxEffortMultiplier(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,
}}, nil)
group := &Group{ID: 100, Platform: PlatformAnthropic}
gid := group.ID
resolved := resolver.Resolve(context.Background(), PricingInput{Model: "claude-fable-5-1", GroupID: &gid, Group: group})
got, err := bs.CalculateTokenCostForRequest(TokenCostRequest{
Ctx: context.Background(), Model: "claude-fable-5-1", Group: group,
Tokens: UsageTokens{InputTokens: 1000}, RateMultiplier: 1, ReasoningEffort: "max",
Resolver: resolver, Resolved: resolved,
})
require.NoError(t, err)
require.InDelta(t, 1000*10e-6*configured, got.TotalCost, 1e-12)
require.InDelta(t, got.TotalCost, got.ActualCost, 1e-12)
}
+20 -19
View File
@@ -87,25 +87,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"`
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"`
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"`
}
// ChannelTimePricing 渠道模型定价的分时倍率配置。
+28 -23
View File
@@ -117,20 +117,16 @@ 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) {
if pricingService == nil {
return
}
for i := range models {
if !pricingNeedsFallback(models[i].Pricing) {
continue
if pricingService != nil && pricingNeedsFallback(models[i].Pricing) {
if lp := pricingService.GetModelPricing(models[i].Name); lp != nil {
models[i].Pricing = synthesizePricingFromLiteLLM(lp, models[i].Pricing)
}
}
lp := pricingService.GetModelPricing(models[i].Name)
if lp == nil {
continue
}
models[i].Pricing = synthesizePricingFromLiteLLM(lp, models[i].Pricing)
models[i].Pricing = withDefaultMaxReasoningEffortMultiplier(models[i].Pricing, models[i].Name)
}
}
@@ -180,24 +176,33 @@ 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),
BillingMode: mode,
PerRequestPrice: nonZeroPtr(lp.OutputCostPerImage),
ImageOutputPrice: nonZeroPtr(lp.OutputCostPerImageToken),
InputPrice: nonZeroPtr(lp.InputCostPerToken),
OutputPrice: nonZeroPtr(lp.OutputCostPerToken),
MaxReasoningEffortMultiplier: maxReasoningEffortMultiplierFromPricing(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),
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),
}
}
func maxReasoningEffortMultiplierFromPricing(pricing *ChannelModelPricing) *float64 {
if pricing == nil {
return nil
}
return pricing.MaxReasoningEffortMultiplier
}
func nonZeroPtr(v float64) *float64 {
if v == 0 {
return nil
@@ -1092,15 +1092,16 @@ func (s *GatewayService) calculateTokenCost(
}
cost, err := s.billingService.CalculateTokenCostForRequest(TokenCostRequest{
Ctx: ctx,
Model: billingModel,
Group: apiKey.Group,
Tokens: tokens,
RateMultiplier: multiplier,
PricingAt: pricingAt,
ServiceTier: optionalStringValue(result.ServiceTier),
Resolver: s.resolver,
Resolved: resolved,
Ctx: ctx,
Model: billingModel,
Group: apiKey.Group,
Tokens: tokens,
RateMultiplier: multiplier,
PricingAt: pricingAt,
ServiceTier: optionalStringValue(result.ServiceTier),
ReasoningEffort: optionalStringValue(result.ReasoningEffort),
Resolver: s.resolver,
Resolved: resolved,
})
if err != nil {
logger.LegacyPrintf("service.gateway", "Calculate cost failed: %v", err)
+1 -1
View File
@@ -116,7 +116,7 @@ type Group struct {
// 一旦设置即接管该分组用户的限流(覆盖用户级 rpm_limit),可被 user-group rpm_override 进一步覆盖。
RPMLimit int
// MaxReasoningEffort limits the effective OpenAI/Codex reasoning effort.
// MaxReasoningEffort limits the effective Anthropic/OpenAI reasoning effort.
// Empty means unlimited; supported values are minimal/low/medium/high/xhigh/max.
MaxReasoningEffort string
// MaxReasoningEffortOverLimit is the access control when an explicit effort
@@ -231,7 +231,7 @@ func (s *ModelPlazaService) fillDisplayPricing(ctx context.Context, m *PlazaMode
Platform: m.Platform,
})
if err == nil && sched != nil && len(sched.Tiers) > 0 {
m.Pricing = plazaPricingFromSchedule(m.Pricing, sched)
m.Pricing = withDefaultMaxReasoningEffortMultiplier(plazaPricingFromSchedule(m.Pricing, sched), m.Name)
if len(sched.Tiers) > 1 {
m.LongContextBasis = sched.Basis
}
@@ -239,7 +239,20 @@ func (s *ModelPlazaService) fillDisplayPricing(ctx context.Context, m *PlazaMode
return
}
}
m.Pricing = plazaImageDisplayPricing(m.Pricing, g)
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
}
// plazaPricingFromSchedule 把阶梯表压成展示用的 ChannelModelPricing:
@@ -250,6 +263,7 @@ func plazaPricingFromSchedule(raw *ChannelModelPricing, sched *ContextPricingSch
out.ImageInputPrice = raw.ImageInputPrice
out.ImageOutputPrice = raw.ImageOutputPrice
out.PerRequestPrice = raw.PerRequestPrice
out.MaxReasoningEffortMultiplier = raw.MaxReasoningEffortMultiplier
}
first := sched.Tiers[0]
out.InputPrice = first.Input
@@ -57,6 +57,19 @@ 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_DedupFirstWinsWithPricingUpgrade(t *testing.T) {
// 同名模型:先见者胜;仅当已存条目无定价而新条目有定价时升级替换。
unpriced := Channel{
@@ -247,6 +247,9 @@ 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
}
// 渠道定价覆盖一切:显式配置则用配置值,未配置则归零(不回退到 LiteLLM)
if chPricing.ImageOutputPrice != nil {
resolved.BasePricing.ImageOutputPricePerToken = *chPricing.ImageOutputPrice
@@ -611,6 +611,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost(
pricingAt,
tokens,
serviceTier,
optionalStringValue(result.ReasoningEffort),
longContextBillingGate,
)
if err == nil {
@@ -704,6 +705,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost(
pricingAt time.Time,
tokens UsageTokens,
serviceTier string,
reasoningEffort string,
longContextBillingGate *bool,
) (*CostBreakdown, error) {
if s.resolver != nil && apiKey.Group != nil {
@@ -711,17 +713,21 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost(
return s.billingService.CalculateCostUnified(CostInput{
Ctx: ctx, Model: billingModel, GroupID: &gid, Group: apiKey.Group,
Tokens: tokens, RequestCount: 1, RateMultiplier: multiplier, PricingAt: pricingAt,
ServiceTier: serviceTier, Resolver: s.resolver,
ServiceTier: serviceTier, ReasoningEffort: reasoningEffort, Resolver: s.resolver,
LongContextBillingEnabled: longContextBillingGate,
})
}
return s.billingService.calculateCostWithServiceTierPolicy(
breakdown, err := 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(
@@ -22,6 +22,7 @@ const (
)
var openAIReasoningEffortValues = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
var anthropicReasoningEffortValues = []string{"low", "medium", "high", "xhigh", "max"}
func normalizeReasoningEffortMappingSource(raw string) string {
if strings.EqualFold(strings.TrimSpace(raw), "none") {
@@ -90,10 +91,14 @@ func NormalizeMaxReasoningEffort(raw string) string {
}
func reasoningEffortValuesForPlatform(platform string) []string {
if platform != PlatformOpenAI && platform != PlatformComposite {
switch platform {
case PlatformOpenAI, PlatformComposite:
return openAIReasoningEffortValues
case PlatformAnthropic:
return anthropicReasoningEffortValues
default:
return nil
}
return openAIReasoningEffortValues
}
func normalizeMaxReasoningEffortForPlatform(platform, raw string) (string, error) {
@@ -104,7 +109,8 @@ func normalizeMaxReasoningEffortForPlatform(platform, raw string) (string, error
allowedValues := reasoningEffortValuesForPlatform(platform)
if len(allowedValues) == 0 {
return "", fmt.Errorf(
"reasoning effort policy is only supported for platforms %q and %q",
"reasoning effort policy is only supported for platforms %q, %q, and %q",
PlatformAnthropic,
PlatformOpenAI,
PlatformComposite,
)
@@ -152,7 +158,8 @@ func normalizeMaxReasoningEffortOverLimitForPlatform(platform, raw string) (stri
}
if len(reasoningEffortValuesForPlatform(platform)) == 0 {
return "", fmt.Errorf(
"reasoning effort policy is only supported for platforms %q and %q",
"reasoning effort policy is only supported for platforms %q, %q, and %q",
PlatformAnthropic,
PlatformOpenAI,
PlatformComposite,
)
@@ -279,7 +286,7 @@ func selectReasoningEffortMapping(mappings []ReasoningEffortMapping, from, reque
}
// NormalizeReasoningEffortMappings validates group mapping rules against the
// fixed effort values supported by OpenAI routes. Optional model scopes are
// fixed effort values supported by the selected platform. Optional model scopes are
// canonicalized to exact/prefix/suffix match; empty type and model keep the
// global all-models rule.
func NormalizeReasoningEffortMappings(platform string, raw []ReasoningEffortMapping) ([]ReasoningEffortMapping, error) {
@@ -421,7 +428,7 @@ func sanitizeGroupReasoningEffortPolicy(group *Group) {
if mappingsErr != nil {
mappings = []ReasoningEffortMapping{}
}
if overLimit == "" || (group.Platform != PlatformOpenAI && group.Platform != PlatformComposite) {
if overLimit == "" || (group.Platform != PlatformAnthropic && group.Platform != PlatformOpenAI && group.Platform != PlatformComposite) {
overLimit = ReasoningEffortOverLimitDowngrade
}
group.MaxReasoningEffort = maxEffort
@@ -429,12 +436,13 @@ func sanitizeGroupReasoningEffortPolicy(group *Group) {
group.ReasoningEffortMappings = mappings
}
// ApplyOpenAIReasoningEffortPolicy applies one mapping (optionally scoped to
// ApplyReasoningEffortPolicy applies one mapping (optionally scoped to
// the request model by exact name, prefix, or suffix) and then either caps
// known effort levels or rejects the request when the group is configured to
// deny values above the ceiling. Omitted values remain untouched so upstream
// defaults stay in control.
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping, overLimit string) ([]byte, bool, error) {
// defaults stay in control. It understands both OpenAI and Anthropic request
// field shapes.
func ApplyReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping, overLimit string) ([]byte, bool, error) {
maxRank, hasMax := reasoningEffortRank(maxEffort)
if len(body) == 0 || (!hasMax && len(mappings) == 0) {
return body, false, nil
@@ -445,7 +453,7 @@ func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []
requestModel := strings.TrimSpace(gjson.GetBytes(body, "model").String())
result := body
changed := false
for _, path := range []string{"reasoning.effort", "reasoning_effort"} {
for _, path := range []string{"reasoning.effort", "reasoning_effort", "output_config.effort"} {
field := gjson.GetBytes(result, path)
if !field.Exists() || field.Type != gjson.String {
continue
@@ -492,3 +500,8 @@ func applyOpenAIWSReasoningEffortPolicy(payload []byte, hooks *OpenAIWSIngressHo
}
return payload, nil
}
// ApplyOpenAIReasoningEffortPolicy is retained for OpenAI forwarding callers.
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping, overLimit string) ([]byte, bool, error) {
return ApplyReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
}
@@ -147,9 +147,9 @@ func TestNormalizeReasoningEffortMappings(t *testing.T) {
})
t.Run("rejects mappings for non OpenAI platforms", func(t *testing.T) {
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok} {
for _, platform := range []string{PlatformGemini, PlatformAntigravity, PlatformGrok} {
_, err := NormalizeReasoningEffortMappings(platform, []ReasoningEffortMapping{{From: "low", To: "high"}})
require.ErrorContains(t, err, "only supported for platforms \"openai\" and \"composite\"")
require.ErrorContains(t, err, "only supported for platforms")
}
_, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{{From: "ultra", To: "high"}})
@@ -166,6 +166,15 @@ func TestNormalizeReasoningEffortMappings(t *testing.T) {
_, err := NormalizeReasoningEffortMappings(PlatformOpenAI, []ReasoningEffortMapping{{From: "low", To: "none"}})
require.ErrorContains(t, err, "empty or unknown")
})
t.Run("supports Anthropic values except minimal", func(t *testing.T) {
got, err := NormalizeReasoningEffortMappings(PlatformAnthropic, []ReasoningEffortMapping{{From: " MAX ", To: " x-high "}})
require.NoError(t, err)
require.Equal(t, []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, got)
_, err = NormalizeReasoningEffortMappings(PlatformAnthropic, []ReasoningEffortMapping{{From: "minimal", To: "low"}})
require.ErrorContains(t, err, "not supported for platform \"anthropic\"")
})
}
func TestNormalizeMaxReasoningEffortForPlatform(t *testing.T) {
@@ -176,9 +185,15 @@ func TestNormalizeMaxReasoningEffortForPlatform(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "max", value)
for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformAntigravity, PlatformGrok} {
value, err = normalizeMaxReasoningEffortForPlatform(PlatformAnthropic, "xhigh")
require.NoError(t, err)
require.Equal(t, "xhigh", value)
_, err = normalizeMaxReasoningEffortForPlatform(PlatformAnthropic, "minimal")
require.ErrorContains(t, err, "not supported")
for _, platform := range []string{PlatformGemini, PlatformAntigravity, PlatformGrok} {
_, err = normalizeMaxReasoningEffortForPlatform(platform, "low")
require.ErrorContains(t, err, "only supported for platforms \"openai\" and \"composite\"")
require.ErrorContains(t, err, "only supported for platforms")
}
_, err = normalizeMaxReasoningEffortForPlatform(PlatformOpenAI, "none")
@@ -205,8 +220,9 @@ func TestNormalizeMaxReasoningEffortOverLimitForPlatform(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ReasoningEffortOverLimitDowngrade, value)
_, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformAnthropic, "deny")
require.ErrorContains(t, err, "only supported for platforms \"openai\" and \"composite\"")
value, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformAnthropic, "deny")
require.NoError(t, err)
require.Equal(t, ReasoningEffortOverLimitDeny, value)
_, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformOpenAI, "block")
require.ErrorContains(t, err, "not supported")
@@ -251,6 +267,8 @@ func TestApplyOpenAIReasoningEffortPolicy(t *testing.T) {
}{
{name: "nested caps high", body: `{"reasoning":{"effort":"xhigh"}}`, max: "medium", path: "reasoning.effort", want: "medium", changed: true},
{name: "flat caps high", body: `{"reasoning_effort":"high"}`, max: "low", path: "reasoning_effort", want: "low", changed: true},
{name: "Anthropic output config caps max", body: `{"output_config":{"effort":"max"}}`, max: "xhigh", path: "output_config.effort", want: "xhigh", changed: true},
{name: "Anthropic output config maps before cap", body: `{"output_config":{"effort":"max"}}`, max: "high", mappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, path: "output_config.effort", want: "high", changed: true},
{name: "does not raise omitted", body: `{"model":"gpt-5"}`, max: "low", path: "reasoning_effort", want: "", changed: false},
{name: "keeps lower value", body: `{"reasoning_effort":"low"}`, max: "high", path: "reasoning_effort", want: "low", changed: false},
{name: "normalizes request alias", body: `{"reasoning_effort":"x-high"}`, max: "xhigh", path: "reasoning_effort", want: "xhigh", changed: true},
@@ -0,0 +1,11 @@
ALTER TABLE channel_model_pricing
ADD COLUMN IF NOT EXISTS max_reasoning_effort_multiplier NUMERIC(10,4);
COMMENT ON COLUMN channel_model_pricing.max_reasoning_effort_multiplier IS
'Billing/quota multiplier applied when the forwarded reasoning effort is max';
ALTER TABLE channel_model_pricing
DROP CONSTRAINT IF EXISTS chk_channel_model_pricing_max_reasoning_effort_multiplier_positive;
ALTER TABLE channel_model_pricing
ADD CONSTRAINT chk_channel_model_pricing_max_reasoning_effort_multiplier_positive
CHECK (max_reasoning_effort_multiplier IS NULL OR max_reasoning_effort_multiplier > 0);
+2
View File
@@ -50,6 +50,7 @@ export interface ChannelModelPricing {
cache_read_price: number | null
fast_multiplier?: number | null
flex_multiplier?: number | null
max_reasoning_effort_multiplier?: number | null
image_input_price: number | null
image_output_price: number | null
per_request_price: number | null
@@ -179,6 +180,7 @@ export interface ModelDefaultPricing {
cache_read_price?: number
image_input_price?: number
image_output_price?: number
max_reasoning_effort_multiplier?: number | null
}
export async function getModelDefaultPricing(model: string): Promise<ModelDefaultPricing> {
+1
View File
@@ -41,6 +41,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
image_input_price: number | null
image_output_price: number | null
per_request_price: number | null
@@ -144,7 +144,7 @@
</div>
</div>
<div v-if="enableTierMultipliers" class="mt-3 grid max-w-md grid-cols-2 gap-2">
<div v-if="enableTierMultipliers" class="mt-3 grid max-w-2xl grid-cols-1 gap-2 sm:grid-cols-3">
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.fastMultiplier') }}</label>
<input :value="entry.fast_multiplier" @input="emitField('fast_multiplier', ($event.target as HTMLInputElement).value)"
@@ -155,6 +155,11 @@
<input :value="entry.flex_multiplier" @input="emitField('flex_multiplier', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0.000001" class="input mt-0.5 text-sm" :placeholder="t('admin.channels.form.multiplierPlaceholder')" />
</div>
<div>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.maxReasoningEffortMultiplier') }}</label>
<input :value="entry.max_reasoning_effort_multiplier" @input="emitField('max_reasoning_effort_multiplier', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0.000001" class="input mt-0.5 text-sm" :placeholder="maxReasoningEffortMultiplierPlaceholder" />
</div>
</div>
<!-- Channel token intervals; the group long-context toggle controls whether tiers apply. -->
@@ -308,6 +313,12 @@ 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')
)
function emitField(field: keyof PricingFormEntry, value: string) {
emit('update', { ...props.entry, [field]: value === '' ? null : value })
}
@@ -383,6 +394,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,
})
}
} catch {
@@ -18,6 +18,7 @@ function createEntry(billingMode: PricingFormEntry['billing_mode'] = 'token'): P
cache_read_price: null,
fast_multiplier: null,
flex_multiplier: null,
max_reasoning_effort_multiplier: null,
image_input_price: null,
image_output_price: null,
per_request_price: null,
@@ -72,8 +73,8 @@ describe('PricingEntryCard time pricing visibility', () => {
})
})
describe('PricingEntryCard service tier multipliers', () => {
it('shows Fast and Flex controls only when explicitly enabled', () => {
describe('PricingEntryCard request multipliers', () => {
it('shows Fast, Flex, and Max effort controls only when explicitly enabled', () => {
const hidden = shallowMount(PricingEntryCard, { props: { entry: createEntry() } })
expect(hidden.text()).not.toContain('admin.channels.form.fastMultiplier')
@@ -82,5 +83,6 @@ describe('PricingEntryCard service tier 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')
})
})
@@ -29,6 +29,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
image_input_price: number | string | null
image_output_price: number | string | null
per_request_price: number | string | null
@@ -99,6 +99,13 @@
>
{{ t('modelPlaza.table.marginalBadge') }}
</span>
<span
v-if="m.pricing?.max_reasoning_effort_multiplier"
class="rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
:title="t('modelPlaza.table.maxReasoningMultiplierHint', { multiplier: m.pricing.max_reasoning_effort_multiplier })"
>
{{ t('modelPlaza.table.maxReasoningMultiplierBadge', { multiplier: m.pricing.max_reasoning_effort_multiplier }) }}
</span>
</div>
</td>
@@ -68,6 +68,15 @@ describe('PlazaModelPricingTable', () => {
expect(text).toContain('1x')
})
it('shows the Max reasoning billing multiplier', () => {
const model = tokenModel()
model.pricing!.max_reasoning_effort_multiplier = 3
const wrapper = mountTable([model], 1)
expect(wrapper.text()).toContain('modelPlaza.table.maxReasoningMultiplierBadge')
expect(wrapper.find('[title="modelPlaza.table.maxReasoningMultiplierHint"]').exists()).toBe(true)
})
it('倍率 ≠ 1 时价格列为折后实付价,官方价列保持原价', () => {
const wrapper = mountTable([tokenModel()], 0.5)
const text = wrapper.text()
@@ -138,8 +138,10 @@ export default {
pricePlaceholder: 'Default',
fastMultiplier: 'Fast Multiplier',
flexMultiplier: 'Flex Multiplier',
maxReasoningEffortMultiplier: 'Max Effort Multiplier',
fable51DefaultMaxReasoningMultiplier: 'Default: 3',
multiplierPlaceholder: 'Not set',
multiplierPositive: 'Fast/Flex multipliers must be greater than 0',
multiplierPositive: 'Fast/Flex/Max effort multipliers must be greater than 0',
inputMultiplier: 'Input Mult.',
outputMultiplier: 'Output Mult.',
cacheWriteMultiplier: 'Cache Write Mult.',
@@ -859,7 +859,7 @@ export default {
rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).',
maxReasoningEffort: 'Max reasoning effort',
maxReasoningEffortUnlimited: 'Unlimited (follow request)',
maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortHint: 'Limits explicit Anthropic and OpenAI reasoning effort requests. For Composite groups, it applies to the resolved target platform. Omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortOverLimit: 'Over-limit access control',
maxReasoningEffortOverLimitDowngrade: 'Automatically downgrade when over limit',
maxReasoningEffortOverLimitDeny: 'Deny access',
@@ -636,6 +636,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}',
marginalBadge: 'excess-only tiers',
timePricingRowHint: 'Requests made within this period ({timezone} time) are billed at the prices in this row',
timePricingRowHintWeekdays:
@@ -138,8 +138,10 @@ export default {
pricePlaceholder: '默认',
fastMultiplier: 'Fast 倍率',
flexMultiplier: 'Flex 倍率',
maxReasoningEffortMultiplier: 'Max 推理倍率',
fable51DefaultMaxReasoningMultiplier: '默认 3',
multiplierPlaceholder: '未配置',
multiplierPositive: 'Fast/Flex 倍率必须大于 0',
multiplierPositive: 'Fast/Flex/Max 推理倍率必须大于 0',
inputMultiplier: '输入倍率',
outputMultiplier: '输出倍率',
cacheWriteMultiplier: '缓存写倍率',
@@ -841,7 +841,7 @@ export default {
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
maxReasoningEffort: '推理强度上限',
maxReasoningEffortUnlimited: '不限制(跟随请求)',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortHint: '仅限制客户端主动请求的 Anthropic/OpenAI 推理强度;Composite 分组按实际目标平台生效。不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortOverLimit: '超限访问控制',
maxReasoningEffortOverLimitDowngrade: '超过上限时自动降档',
maxReasoningEffortOverLimitDeny: '拒绝访问',
@@ -641,6 +641,8 @@ export default {
cacheReadShort: '读',
tierHint: '按单次请求的总上下文(输入 + 缓存写入 + 缓存读取)所在档位对整单计价',
tierHintMarginal: '仅超过阈值的部分按该档计价,输出不加价',
maxReasoningMultiplierBadge: 'Max ×{multiplier}',
maxReasoningMultiplierHint: '最终转发的推理强度为 max 时,整次请求的计费与额度消耗乘以 {multiplier}',
marginalBadge: '超出部分计价',
timePricingRowHint: '按 {timezone} 时间,在该时段内发起的请求按本行价格计费',
timePricingRowHintWeekdays:
+1 -1
View File
@@ -560,7 +560,7 @@ export interface Group {
platform: GroupPlatform
rate_multiplier: number
rpm_limit?: number // Group-level RPM cap (0 = unlimited); overrides user-level rpm_limit when set
max_reasoning_effort?: string // OpenAI/Codex reasoning ceiling; empty means unlimited
max_reasoning_effort?: string // Anthropic/OpenAI reasoning ceiling; empty means unlimited
max_reasoning_effort_over_limit?: string // downgrade (default) or deny when over the ceiling
reasoning_effort_mappings?: ReasoningEffortMapping[]
is_exclusive: boolean
+6 -1
View File
@@ -865,6 +865,7 @@ function addPricingEntry(sectionIdx: number) {
cache_read_price: null,
fast_multiplier: null,
flex_multiplier: null,
max_reasoning_effort_multiplier: null,
image_input_price: null,
image_output_price: null,
per_request_price: null,
@@ -902,6 +903,7 @@ async function syncLatestModels(sectionIdx: number) {
cache_read_price: null,
fast_multiplier: null,
flex_multiplier: null,
max_reasoning_effort_multiplier: null,
image_input_price: null,
image_output_price: null,
per_request_price: null,
@@ -1132,6 +1134,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,
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,
@@ -1234,6 +1237,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,
image_input_price: perTokenToMTok(p.image_input_price),
image_output_price: perTokenToMTok(p.image_output_price),
per_request_price: p.per_request_price,
@@ -1541,7 +1545,8 @@ async function handleSubmit() {
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.flex_multiplier) ||
!isValidPositiveMultiplier(entry.max_reasoning_effort_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')}`)
@@ -40,12 +40,14 @@ describe("groupsReasoningEffort", () => {
).toEqual(["none", ...expected]);
expect(supportsReasoningEffortPolicyPlatform(platform)).toBe(true);
}
for (const platform of [
"anthropic",
"gemini",
"antigravity",
"grok",
] as const) {
expect(
reasoningEffortOptionsForPlatform("anthropic").map(
(option) => option.value,
),
).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(supportsReasoningEffortPolicyPlatform("anthropic")).toBe(true);
for (const platform of ["gemini", "antigravity", "grok"] as const) {
expect(reasoningEffortOptionsForPlatform(platform)).toEqual([]);
expect(reasoningEffortSourceOptionsForPlatform(platform)).toEqual([]);
expect(supportsReasoningEffortPolicyPlatform(platform)).toBe(false);
@@ -158,6 +160,12 @@ describe("groupsReasoningEffort", () => {
expect(normalizeReasoningEffortForPlatform("composite", " MAX ")).toBe(
"max",
);
expect(normalizeReasoningEffortForPlatform("anthropic", "xhigh")).toBe(
"xhigh",
);
expect(normalizeReasoningEffortForPlatform("anthropic", "minimal")).toBe(
"",
);
expect(normalizeReasoningEffortForPlatform("grok", "max")).toBe("");
expect(normalizeReasoningEffortForPlatform("openai", "none")).toBe("");
});
@@ -17,6 +17,14 @@ const openAIReasoningEffortSourceValues = [
...openAIReasoningEffortValues,
] as const;
const anthropicReasoningEffortValues = [
"low",
"medium",
"high",
"xhigh",
"max",
] as const;
const reasoningEffortMatchTypes: readonly ReasoningEffortMatchType[] = [
"exact",
"prefix",
@@ -29,14 +37,16 @@ export const reasoningEffortOverLimitDeny = "deny";
const reasoningEffortValuesForPlatform = (
platform: GroupPlatform,
): readonly string[] =>
supportsReasoningEffortPolicyPlatform(platform)
? openAIReasoningEffortValues
: [];
platform === "anthropic"
? anthropicReasoningEffortValues
: supportsReasoningEffortPolicyPlatform(platform)
? openAIReasoningEffortValues
: [];
export function supportsReasoningEffortPolicyPlatform(
platform: GroupPlatform,
): boolean {
return platform === "openai" || platform === "composite";
return platform === "anthropic" || platform === "openai" || platform === "composite";
}
export function reasoningEffortOptionsForPlatform(platform: GroupPlatform) {