mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 12:57:57 +08:00
计费:识别并记录 Anthropic Fast 请求
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func pricingMultiplier(value float64) *float64 { return &value }
|
||||
|
||||
func TestConfiguredServiceTierMultiplier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serviceTier string
|
||||
pricing *ModelPricing
|
||||
want float64
|
||||
}{
|
||||
{name: "gpt-5.5 fast", serviceTier: "fast", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2.5)}, want: 2.5},
|
||||
{name: "priority alias", serviceTier: "priority", pricing: &ModelPricing{FastMultiplier: pricingMultiplier(2)}, want: 2},
|
||||
{name: "flex configured", serviceTier: "flex", pricing: &ModelPricing{FlexMultiplier: pricingMultiplier(0.4)}, want: 0.4},
|
||||
{name: "legacy fast default", serviceTier: "fast", pricing: &ModelPricing{}, want: 2},
|
||||
{name: "legacy flex default", serviceTier: "flex", pricing: &ModelPricing{}, want: 0.5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.InDelta(t, tt.want, configuredServiceTierMultiplier(tt.serviceTier, tt.pricing), 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredServiceTierMultiplierAppliesToEveryTokenComponent(t *testing.T) {
|
||||
pricing := &ModelPricing{
|
||||
InputPricePerToken: 5e-6,
|
||||
OutputPricePerToken: 30e-6,
|
||||
CacheCreationPricePerToken: 6.25e-6,
|
||||
CacheReadPricePerToken: 0.5e-6,
|
||||
FastMultiplier: pricingMultiplier(2.5),
|
||||
FlexMultiplier: pricingMultiplier(0.5),
|
||||
}
|
||||
tokens := UsageTokens{
|
||||
InputTokens: 1_000_000,
|
||||
OutputTokens: 1_000_000,
|
||||
CacheCreationTokens: 1_000_000,
|
||||
CacheReadTokens: 1_000_000,
|
||||
}
|
||||
service := &BillingService{}
|
||||
|
||||
fast := service.computeTokenBreakdown(pricing, tokens, 1, "fast", false)
|
||||
require.InDelta(t, 12.5, fast.InputCost, 1e-12)
|
||||
require.InDelta(t, 75, fast.OutputCost, 1e-12)
|
||||
require.InDelta(t, 15.625, fast.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 1.25, fast.CacheReadCost, 1e-12)
|
||||
|
||||
flex := service.computeTokenBreakdown(pricing, tokens, 1, "flex", false)
|
||||
require.InDelta(t, 2.5, flex.InputCost, 1e-12)
|
||||
require.InDelta(t, 15, flex.OutputCost, 1e-12)
|
||||
require.InDelta(t, 3.125, flex.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 0.25, flex.CacheReadCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestChannelOverridePreservesCatalogFastRatioByDefault(t *testing.T) {
|
||||
pricing := &ModelPricing{
|
||||
InputPricePerToken: 2,
|
||||
InputPricePerTokenPriority: 4,
|
||||
OutputPricePerToken: 6,
|
||||
OutputPricePerTokenPriority: 12,
|
||||
}
|
||||
applyChannelTokenPriceOverrides(pricing, &ChannelModelPricing{
|
||||
InputPrice: pricingMultiplier(3),
|
||||
OutputPrice: pricingMultiplier(9),
|
||||
})
|
||||
|
||||
require.InDelta(t, 3, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 6, pricing.InputPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, 9, pricing.OutputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 18, pricing.OutputPricePerTokenPriority, 1e-12)
|
||||
}
|
||||
|
||||
func TestAnthropicFastUsesDefaultMultiplierWithoutCatalogTier(t *testing.T) {
|
||||
pricing := &ModelPricing{InputPricePerToken: 5e-6, OutputPricePerToken: 25e-6}
|
||||
cost := (&BillingService{}).computeTokenBreakdown(pricing, UsageTokens{
|
||||
InputTokens: 1_000_000, OutputTokens: 1_000_000,
|
||||
}, 1, "fast", false)
|
||||
|
||||
require.InDelta(t, 10, cost.InputCost, 1e-12)
|
||||
require.InDelta(t, 50, cost.OutputCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestBuiltInModelFastDefaults(t *testing.T) {
|
||||
service := &BillingService{fallbackPrices: make(map[string]*ModelPricing)}
|
||||
service.initFallbackPricing()
|
||||
|
||||
for _, tt := range []struct {
|
||||
model string
|
||||
want float64
|
||||
}{
|
||||
{model: "gpt-5.5", want: 2.5},
|
||||
{model: "claude-opus-4.8", want: 2},
|
||||
{model: "claude-opus-5", want: 2},
|
||||
} {
|
||||
pricing := service.fallbackPrices[tt.model]
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, tt.want, pricing.InputPricePerTokenPriority/pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.want, pricing.OutputPricePerTokenPriority/pricing.OutputPricePerToken, 1e-12)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntervalMultipliersApplyToChannelBase(t *testing.T) {
|
||||
base := &ModelPricing{
|
||||
InputPricePerToken: 5,
|
||||
OutputPricePerToken: 30,
|
||||
CacheCreationPricePerToken: 6.25,
|
||||
CacheCreation5mPrice: 6.25,
|
||||
CacheCreation1hPrice: 6.25,
|
||||
CacheReadPricePerToken: 0.5,
|
||||
FastMultiplier: pricingMultiplier(2),
|
||||
FlexMultiplier: pricingMultiplier(0.5),
|
||||
}
|
||||
resolved := &ResolvedPricing{
|
||||
BasePricing: base,
|
||||
Intervals: []PricingInterval{{
|
||||
MinTokens: 272000,
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
OutputMultiplier: pricingMultiplier(1.5),
|
||||
CacheWriteMultiplier: pricingMultiplier(2),
|
||||
CacheReadMultiplier: pricingMultiplier(2),
|
||||
}},
|
||||
}
|
||||
|
||||
pricing := (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272001)
|
||||
require.InDelta(t, 10, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 45, pricing.OutputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 12.5, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, 1, pricing.CacheReadPricePerToken, 1e-12)
|
||||
require.Same(t, base, (&ModelPricingResolver{}).GetIntervalPricing(resolved, 272000))
|
||||
}
|
||||
|
||||
func TestIntervalExplicitPriceTakesPrecedenceOverMultiplier(t *testing.T) {
|
||||
pricing := intervalToModelPricing(&PricingInterval{
|
||||
InputPrice: pricingMultiplier(7),
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
}, &ModelPricing{InputPricePerToken: 5}, nil)
|
||||
|
||||
require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
|
||||
}
|
||||
|
||||
func TestIntervalPricePreservesDefaultFastRatio(t *testing.T) {
|
||||
pricing := intervalToModelPricing(&PricingInterval{
|
||||
InputPrice: pricingMultiplier(7),
|
||||
}, &ModelPricing{
|
||||
InputPricePerToken: 5,
|
||||
InputPricePerTokenPriority: 10,
|
||||
}, nil)
|
||||
|
||||
require.InDelta(t, 7, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 14, pricing.InputPricePerTokenPriority, 1e-12)
|
||||
}
|
||||
|
||||
func TestAnthropicSpeedServiceTier(t *testing.T) {
|
||||
account := &Account{Platform: PlatformAnthropic}
|
||||
|
||||
for _, model := range []string{"claude-opus-5", "claude-opus-4-8", "claude-opus-4.8"} {
|
||||
tier := anthropicSpeedServiceTier(account, "fast", model)
|
||||
require.NotNil(t, tier, "model %s should bill as fast", model)
|
||||
require.Equal(t, "fast", *tier)
|
||||
}
|
||||
|
||||
require.Nil(t, anthropicSpeedServiceTier(&Account{Platform: PlatformOpenAI}, "fast", "claude-opus-5"))
|
||||
require.Nil(t, anthropicSpeedServiceTier(account, "standard", "claude-opus-5"))
|
||||
}
|
||||
|
||||
// fast mode 不存在于这些模型/承载上,即便客户端传了 speed=fast 也不能计 2x。
|
||||
func TestAnthropicSpeedServiceTierRejectsUnsupportedTargets(t *testing.T) {
|
||||
account := &Account{Platform: PlatformAnthropic}
|
||||
|
||||
for _, model := range []string{
|
||||
"claude-opus-4-7", // fast mode 已被移除
|
||||
"claude-opus-4-6", //
|
||||
"claude-opus-4-5", // 不能被 "opus-5" 规则误判
|
||||
"claude-sonnet-5", // 非 Opus
|
||||
"claude-haiku-4-5", //
|
||||
"", //
|
||||
} {
|
||||
require.Nil(t, anthropicSpeedServiceTier(account, "fast", model),
|
||||
"model %q must not bill as fast", model)
|
||||
}
|
||||
|
||||
bedrock := &Account{Platform: PlatformAnthropic, Type: AccountTypeBedrock}
|
||||
require.Nil(t, anthropicSpeedServiceTier(bedrock, "fast", "claude-opus-5"))
|
||||
}
|
||||
|
||||
func TestAnthropicSpeedModelPrefersMappedUpstreamModel(t *testing.T) {
|
||||
parsed := &ParsedRequest{Model: "claude-opus-5"}
|
||||
require.Equal(t, "claude-opus-4-7", anthropicSpeedModel(parsed, &ForwardResult{
|
||||
UpstreamModel: "claude-opus-4-7",
|
||||
}))
|
||||
require.Equal(t, "claude-opus-5", anthropicSpeedModel(parsed, &ForwardResult{}))
|
||||
}
|
||||
|
||||
func TestMultiplierOnlyIntervalIsValid(t *testing.T) {
|
||||
require.NoError(t, ValidateIntervals([]PricingInterval{{
|
||||
MinTokens: 199999,
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
}}, BillingModeToken))
|
||||
require.NoError(t, checkIntervalsHavePrices(ChannelModelPricing{
|
||||
Models: []string{"grok-4.6"},
|
||||
Intervals: []PricingInterval{{
|
||||
MinTokens: 199999,
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
}},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestChannelMultipliersMustBePositive(t *testing.T) {
|
||||
zero := 0.0
|
||||
require.Error(t, checkPricesNotNegative(ChannelModelPricing{FastMultiplier: &zero}))
|
||||
require.Error(t, checkPricesNotNegative(ChannelModelPricing{FlexMultiplier: &zero}))
|
||||
require.Error(t, ValidateIntervals([]PricingInterval{{
|
||||
MinTokens: 100,
|
||||
InputMultiplier: &zero,
|
||||
}}, BillingModeToken))
|
||||
}
|
||||
|
||||
func TestCalculateTokenCostContextTierEnablement(t *testing.T) {
|
||||
base := &ModelPricing{InputPricePerToken: 1e-6}
|
||||
resolved := &ResolvedPricing{
|
||||
BasePricing: base,
|
||||
Intervals: []PricingInterval{{
|
||||
MinTokens: 100,
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
}},
|
||||
}
|
||||
resolver := &ModelPricingResolver{}
|
||||
service := &BillingService{}
|
||||
tokens := UsageTokens{InputTokens: 200}
|
||||
|
||||
t.Run("group disabled uses base tier", func(t *testing.T) {
|
||||
resolved.longContextPricingEnabled = false
|
||||
cost, err := service.calculateTokenCost(resolved, CostInput{
|
||||
Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200e-6, cost.TotalCost, 1e-12)
|
||||
})
|
||||
|
||||
t.Run("group enabled uses interval", func(t *testing.T) {
|
||||
resolved.longContextPricingEnabled = true
|
||||
accountDisabled := false
|
||||
cost, err := service.calculateTokenCost(resolved, CostInput{
|
||||
Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
|
||||
LongContextBillingEnabled: &accountDisabled,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
|
||||
})
|
||||
|
||||
t.Run("account enabled overrides disabled group", func(t *testing.T) {
|
||||
resolved.longContextPricingEnabled = false
|
||||
accountEnabled := true
|
||||
cost, err := service.calculateTokenCost(resolved, CostInput{
|
||||
Model: "custom", Tokens: tokens, RateMultiplier: 1, Resolver: resolver,
|
||||
LongContextBillingEnabled: &accountEnabled,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 400e-6, cost.TotalCost, 1e-12)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateTokenCostCombinesIntervalAndFastMultiplier(t *testing.T) {
|
||||
resolved := &ResolvedPricing{
|
||||
BasePricing: &ModelPricing{
|
||||
InputPricePerToken: 1e-6,
|
||||
FastMultiplier: pricingMultiplier(2.5),
|
||||
},
|
||||
Intervals: []PricingInterval{{
|
||||
MinTokens: 100,
|
||||
InputMultiplier: pricingMultiplier(2),
|
||||
}},
|
||||
longContextPricingEnabled: true,
|
||||
}
|
||||
cost, err := (&BillingService{}).calculateTokenCost(resolved, CostInput{
|
||||
Model: "custom", Tokens: UsageTokens{InputTokens: 200}, RateMultiplier: 1,
|
||||
ServiceTier: "fast", Resolver: &ModelPricingResolver{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 1e-3, cost.TotalCost, 1e-12)
|
||||
}
|
||||
@@ -88,11 +88,21 @@ func sleepWithContext(ctx context.Context, d time.Duration) error {
|
||||
}
|
||||
|
||||
// Forward 转发请求到Claude API
|
||||
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (*ForwardResult, error) {
|
||||
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (result *ForwardResult, err error) {
|
||||
startTime := time.Now()
|
||||
if parsed == nil {
|
||||
return nil, fmt.Errorf("parse request: empty request")
|
||||
}
|
||||
// Anthropic Fast is requested with speed=fast rather than OpenAI's
|
||||
// service_tier. Attach it at this shared boundary so passthrough, OAuth and
|
||||
// partial-stream results all use the same billing and usage-log path.
|
||||
defer func() {
|
||||
if result != nil {
|
||||
if tier := anthropicSpeedServiceTier(account, parsed.Speed, anthropicSpeedModel(parsed, result)); tier != nil {
|
||||
result.ServiceTier = tier
|
||||
}
|
||||
}
|
||||
}()
|
||||
beginUpstreamResponseModelObservation(c)
|
||||
|
||||
// Web Search 模拟:纯 web_search 请求时,直接调用搜索 API 构造响应
|
||||
@@ -885,6 +895,50 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
}, nil
|
||||
}
|
||||
|
||||
func anthropicSpeedModel(parsed *ParsedRequest, result *ForwardResult) string {
|
||||
if result != nil {
|
||||
if upstreamModel := strings.TrimSpace(result.UpstreamModel); upstreamModel != "" {
|
||||
return upstreamModel
|
||||
}
|
||||
}
|
||||
if parsed == nil {
|
||||
return ""
|
||||
}
|
||||
return parsed.Model
|
||||
}
|
||||
|
||||
// anthropicSpeedServiceTier 把 Anthropic 的 speed=fast 归一成可计费的 "fast" tier。
|
||||
//
|
||||
// Fast mode 目前只在 Claude Opus 5 / Opus 4.8 上存在,且不支持 Bedrock 等第三方
|
||||
// 承载(Opus 4.7 的 fast mode 已被移除,传 speed=fast 会直接报错)。这里按模型和
|
||||
// 平台收紧,避免上游根本没跑 fast 时仍然按 2x 计费——宁可漏收也不能多收。
|
||||
//
|
||||
// 注:判据是请求参数而非响应里的 usage.speed。等 usage 解析链路统一暴露该字段后,
|
||||
// 应改为以响应为准。
|
||||
func anthropicSpeedServiceTier(account *Account, speed, model string) *string {
|
||||
if account == nil || account.Platform != PlatformAnthropic || speed != "fast" {
|
||||
return nil
|
||||
}
|
||||
if account.IsBedrock() || !modelSupportsAnthropicFastMode(model) {
|
||||
return nil
|
||||
}
|
||||
tier := "fast"
|
||||
return &tier
|
||||
}
|
||||
|
||||
// modelSupportsAnthropicFastMode 判断模型是否属于支持 fast mode 的 Opus 5 / Opus 4.8。
|
||||
func modelSupportsAnthropicFastMode(model string) bool {
|
||||
modelLower := strings.ToLower(strings.TrimSpace(model))
|
||||
if !strings.Contains(modelLower, "opus") {
|
||||
return false
|
||||
}
|
||||
// "opus-5" 必须先判:不能用裸 "5" 匹配,否则 claude-opus-4-5 会被误判。
|
||||
if strings.Contains(modelLower, "opus-5") || strings.Contains(modelLower, "opus5") {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(modelLower, "4.8") || strings.Contains(modelLower, "4-8")
|
||||
}
|
||||
|
||||
// ResolveChannelMapping 委托渠道服务解析模型映射
|
||||
func (s *GatewayService) ResolveChannelMapping(ctx context.Context, groupID int64, model string) ChannelMappingResult {
|
||||
if s.channelService == nil {
|
||||
|
||||
@@ -117,6 +117,7 @@ func clearGatewayRequestDerivedState(parsed *ParsedRequest) {
|
||||
parsed.HasSystem = false
|
||||
parsed.ThinkingEnabled = false
|
||||
parsed.OutputEffort = ""
|
||||
parsed.Speed = ""
|
||||
parsed.MaxTokens = 0
|
||||
parsed.systemRange = missingJSONRange()
|
||||
parsed.messagesRange = missingJSONRange()
|
||||
@@ -224,6 +225,9 @@ func parseGatewayRequestCurrentBody(parsed *ParsedRequest, protocol string) erro
|
||||
parsed.ThinkingEnabled = thinkingType == "enabled" || thinkingType == "adaptive"
|
||||
|
||||
parsed.OutputEffort = strings.TrimSpace(gjson.Get(jsonStr, "output_config.effort").String())
|
||||
if protocol == domain.PlatformAnthropic {
|
||||
parsed.Speed = strings.ToLower(strings.TrimSpace(gjson.Get(jsonStr, "speed").String()))
|
||||
}
|
||||
|
||||
maxTokensResult := gjson.Get(jsonStr, "max_tokens")
|
||||
if maxTokensResult.Exists() && maxTokensResult.Type == gjson.Number {
|
||||
@@ -282,6 +286,7 @@ type ParsedRequest struct {
|
||||
HasSystem bool // 是否包含 system 字段(包含 null 也视为显式传入)
|
||||
ThinkingEnabled bool // 是否开启 thinking(部分平台会影响最终模型名)
|
||||
OutputEffort string // output_config.effort(Claude API 的推理强度控制)
|
||||
Speed string // Anthropic speed(当前可计费值为 "fast")
|
||||
MaxTokens int // max_tokens 值(用于探测请求拦截)
|
||||
SessionContext *SessionContext // 可选:请求上下文区分因子(nil 时行为不变)
|
||||
|
||||
|
||||
@@ -42,6 +42,22 @@ func TestParseGatewayRequest_ThinkingAdaptiveEnabled(t *testing.T) {
|
||||
require.True(t, parsed.ThinkingEnabled)
|
||||
}
|
||||
|
||||
func TestParseGatewayRequest_AnthropicFastSpeed(t *testing.T) {
|
||||
parsed, err := ParseGatewayRequest(
|
||||
NewRequestBodyRef([]byte(`{"model":"claude-opus-4-8","speed":" FAST "}`)),
|
||||
domain.PlatformAnthropic,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "fast", parsed.Speed)
|
||||
|
||||
nonAnthropic, err := ParseGatewayRequest(
|
||||
NewRequestBodyRef([]byte(`{"model":"gpt-5.4","speed":"fast"}`)),
|
||||
"responses",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, nonAnthropic.Speed)
|
||||
}
|
||||
|
||||
func TestParseGatewayRequest_MaxTokens(t *testing.T) {
|
||||
body := []byte(`{"model":"claude-haiku-4-5","max_tokens":1}`)
|
||||
parsed, err := ParseGatewayRequest(NewRequestBodyRef(body), "")
|
||||
|
||||
@@ -619,6 +619,9 @@ type ForwardResult struct {
|
||||
FirstTokenMs *int // 首字时间(流式请求)
|
||||
ClientDisconnect bool // 客户端是否在流式传输过程中断开
|
||||
ReasoningEffort *string
|
||||
// ServiceTier records the billable request tier. OpenAI uses service_tier;
|
||||
// Anthropic speed=fast is normalized to "fast".
|
||||
ServiceTier *string
|
||||
|
||||
// 图片生成计费字段(图片生成模型使用)
|
||||
ImageCount int // 生成的图片数量
|
||||
|
||||
@@ -1157,6 +1157,7 @@ func (s *GatewayService) calculateTokenCost(
|
||||
RequestCount: 1,
|
||||
RateMultiplier: multiplier,
|
||||
PricingAt: pricingAt,
|
||||
ServiceTier: optionalStringValue(result.ServiceTier),
|
||||
Resolver: s.resolver,
|
||||
Resolved: resolved,
|
||||
})
|
||||
@@ -1167,7 +1168,8 @@ func (s *GatewayService) calculateTokenCost(
|
||||
gid := apiKey.Group.ID
|
||||
cost, err = s.billingService.CalculateCostUnified(CostInput{
|
||||
Ctx: ctx, Model: billingModel, GroupID: &gid, Group: apiKey.Group,
|
||||
Tokens: tokens, RequestCount: 1, RateMultiplier: multiplier, PricingAt: pricingAt, Resolver: s.resolver,
|
||||
Tokens: tokens, RequestCount: 1, RateMultiplier: multiplier, PricingAt: pricingAt,
|
||||
ServiceTier: optionalStringValue(result.ServiceTier), Resolver: s.resolver,
|
||||
})
|
||||
} else {
|
||||
cost, err = s.billingService.CalculateCost(billingModel, tokens, multiplier)
|
||||
@@ -1219,6 +1221,7 @@ func (s *GatewayService) buildRecordUsageLog(
|
||||
UpstreamModel: optionalTrimmedStringPtr(result.UpstreamModel),
|
||||
UpstreamResponseModel: optionalTrimmedStringPtr(result.UpstreamResponseModel),
|
||||
UpstreamModelMismatch: upstreamModelMismatch(sentModel, result.UpstreamResponseModel),
|
||||
ServiceTier: result.ServiceTier,
|
||||
ReasoningEffort: result.ReasoningEffort,
|
||||
InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint),
|
||||
UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint),
|
||||
|
||||
@@ -128,7 +128,8 @@ type UsageLog struct {
|
||||
BillingTier *string
|
||||
// BillingMode 计费模式:token/image
|
||||
BillingMode *string
|
||||
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
|
||||
// ServiceTier records the billable request tier, e.g. OpenAI "priority" / "flex"
|
||||
// or Anthropic "fast".
|
||||
ServiceTier *string
|
||||
// ReasoningEffort is the request's reasoning effort level.
|
||||
// OpenAI: "low" / "medium" / "high" / "xhigh"; Claude: "low" / "medium" / "high" / "max".
|
||||
|
||||
@@ -10,6 +10,13 @@ func optionalTrimmedStringPtr(raw string) *string {
|
||||
return &trimmed
|
||||
}
|
||||
|
||||
func optionalStringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*value)
|
||||
}
|
||||
|
||||
func forwardResultBillingModel(requestedModel, upstreamModel string) string {
|
||||
if trimmed := strings.TrimSpace(requestedModel); trimmed != "" {
|
||||
return trimmed
|
||||
|
||||
Reference in New Issue
Block a user