mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
feat(openai): support GPT-5.6 cache write billing
This commit is contained in:
@@ -608,9 +608,17 @@ func ChatUsageToResponsesUsage(usage *ChatUsage) *ResponsesUsage {
|
||||
if out.TotalTokens == 0 {
|
||||
out.TotalTokens = out.InputTokens + out.OutputTokens
|
||||
}
|
||||
if usage.PromptTokensDetails != nil && usage.PromptTokensDetails.CachedTokens > 0 {
|
||||
if usage.PromptTokensDetails != nil && (usage.PromptTokensDetails.CachedTokens > 0 ||
|
||||
usage.PromptTokensDetails.CacheCreationTokens > 0 || usage.PromptTokensDetails.CacheWriteTokens > 0) {
|
||||
out.InputTokensDetails = &ResponsesInputTokensDetails{
|
||||
CachedTokens: usage.PromptTokensDetails.CachedTokens,
|
||||
CachedTokens: usage.PromptTokensDetails.CachedTokens,
|
||||
CacheCreationTokens: usage.PromptTokensDetails.CacheCreationTokens,
|
||||
CacheWriteTokens: usage.PromptTokensDetails.CacheWriteTokens,
|
||||
}
|
||||
if usage.PromptTokensDetails.CacheWriteTokens > 0 {
|
||||
out.CacheCreationInputTokens = usage.PromptTokensDetails.CacheWriteTokens
|
||||
} else {
|
||||
out.CacheCreationInputTokens = usage.PromptTokensDetails.CacheCreationTokens
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -32,6 +32,27 @@ func TestChatCompletionsToResponses_BasicText(t *testing.T) {
|
||||
assert.Equal(t, "user", items[0].Role)
|
||||
}
|
||||
|
||||
func TestUsageConversionsPreserveCacheWriteTokens(t *testing.T) {
|
||||
var responsesUsage ResponsesUsage
|
||||
require.NoError(t, json.Unmarshal([]byte(`{
|
||||
"input_tokens":1000,
|
||||
"output_tokens":50,
|
||||
"input_tokens_details":{"cached_tokens":100,"cache_write_tokens":200}
|
||||
}`), &responsesUsage))
|
||||
require.NotNil(t, responsesUsage.InputTokensDetails)
|
||||
require.Equal(t, 200, responsesUsage.InputTokensDetails.CacheWriteTokens)
|
||||
|
||||
chatUsage := chatUsageFromResponsesUsage(&responsesUsage)
|
||||
require.NotNil(t, chatUsage.PromptTokensDetails)
|
||||
require.Equal(t, 100, chatUsage.PromptTokensDetails.CachedTokens)
|
||||
require.Equal(t, 200, chatUsage.PromptTokensDetails.CacheWriteTokens)
|
||||
|
||||
roundTrip := ChatUsageToResponsesUsage(chatUsage)
|
||||
require.NotNil(t, roundTrip.InputTokensDetails)
|
||||
require.Equal(t, 200, roundTrip.CacheCreationInputTokens)
|
||||
require.Equal(t, 200, roundTrip.InputTokensDetails.CacheWriteTokens)
|
||||
}
|
||||
|
||||
func TestChatCompletionsToResponses_SystemMessage(t *testing.T) {
|
||||
req := &ChatCompletionsRequest{
|
||||
Model: "gpt-4o",
|
||||
|
||||
@@ -338,6 +338,14 @@ func chatUsageFromResponsesUsage(u *ResponsesUsage) *ChatUsage {
|
||||
TotalTokens: u.InputTokens + u.OutputTokens,
|
||||
}
|
||||
usage.PromptTokensDetails = promptDetailsFromResponses(u.InputTokensDetails)
|
||||
if u.CacheCreationInputTokens > 0 {
|
||||
if usage.PromptTokensDetails == nil {
|
||||
usage.PromptTokensDetails = &ChatTokenDetails{}
|
||||
}
|
||||
if usage.PromptTokensDetails.CacheWriteTokens == 0 && usage.PromptTokensDetails.CacheCreationTokens == 0 {
|
||||
usage.PromptTokensDetails.CacheCreationTokens = u.CacheCreationInputTokens
|
||||
}
|
||||
}
|
||||
usage.CompletionTokensDetails = completionDetailsFromResponses(u.OutputTokensDetails)
|
||||
return usage
|
||||
}
|
||||
@@ -349,12 +357,14 @@ func promptDetailsFromResponses(src *ResponsesInputTokensDetails) *ChatTokenDeta
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
if src.CachedTokens == 0 && src.AudioTokens == 0 {
|
||||
if src.CachedTokens == 0 && src.AudioTokens == 0 && src.CacheCreationTokens == 0 && src.CacheWriteTokens == 0 {
|
||||
return nil
|
||||
}
|
||||
return &ChatTokenDetails{
|
||||
CachedTokens: src.CachedTokens,
|
||||
AudioTokens: src.AudioTokens,
|
||||
CachedTokens: src.CachedTokens,
|
||||
AudioTokens: src.AudioTokens,
|
||||
CacheCreationTokens: src.CacheCreationTokens,
|
||||
CacheWriteTokens: src.CacheWriteTokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -320,9 +320,10 @@ type ResponsesSummary struct {
|
||||
|
||||
// ResponsesUsage holds token counts in Responses API format.
|
||||
type ResponsesUsage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"`
|
||||
|
||||
// Optional detailed breakdown
|
||||
InputTokensDetails *ResponsesInputTokensDetails `json:"input_tokens_details,omitempty"`
|
||||
@@ -335,6 +336,9 @@ func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
responsesUsageAlias
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
CacheCreationTokens int `json:"cache_creation_tokens"`
|
||||
CacheWriteInputTokens int `json:"cache_write_input_tokens"`
|
||||
CacheWriteTokens int `json:"cache_write_tokens"`
|
||||
PromptTokensDetails *ResponsesInputTokensDetails `json:"prompt_tokens_details,omitempty"`
|
||||
CompletionTokensDetails *ResponsesOutputTokensDetails `json:"completion_tokens_details,omitempty"`
|
||||
}
|
||||
@@ -348,6 +352,16 @@ func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
if u.OutputTokens == 0 && aux.CompletionTokens != 0 {
|
||||
u.OutputTokens = aux.CompletionTokens
|
||||
}
|
||||
if u.CacheCreationInputTokens == 0 {
|
||||
switch {
|
||||
case aux.CacheWriteInputTokens > 0:
|
||||
u.CacheCreationInputTokens = aux.CacheWriteInputTokens
|
||||
case aux.CacheCreationTokens > 0:
|
||||
u.CacheCreationInputTokens = aux.CacheCreationTokens
|
||||
case aux.CacheWriteTokens > 0:
|
||||
u.CacheCreationInputTokens = aux.CacheWriteTokens
|
||||
}
|
||||
}
|
||||
if u.InputTokensDetails == nil && aux.PromptTokensDetails != nil {
|
||||
u.InputTokensDetails = aux.PromptTokensDetails
|
||||
}
|
||||
@@ -362,8 +376,10 @@ func (u *ResponsesUsage) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// ResponsesInputTokensDetails breaks down input token usage.
|
||||
type ResponsesInputTokensDetails struct {
|
||||
CachedTokens int `json:"cached_tokens,omitempty"`
|
||||
AudioTokens int `json:"audio_tokens,omitempty"`
|
||||
CachedTokens int `json:"cached_tokens,omitempty"`
|
||||
AudioTokens int `json:"audio_tokens,omitempty"`
|
||||
CacheCreationTokens int `json:"cache_creation_tokens,omitempty"`
|
||||
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// ResponsesOutputTokensDetails breaks down output token usage.
|
||||
@@ -545,6 +561,8 @@ type ChatUsage struct {
|
||||
type ChatTokenDetails struct {
|
||||
CachedTokens int `json:"cached_tokens,omitempty"`
|
||||
AudioTokens int `json:"audio_tokens,omitempty"`
|
||||
CacheCreationTokens int `json:"cache_creation_tokens,omitempty"`
|
||||
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
|
||||
ReasoningTokens int `json:"reasoning_tokens,omitempty"`
|
||||
AcceptedPredictionTokens int `json:"accepted_prediction_tokens,omitempty"`
|
||||
RejectedPredictionTokens int `json:"rejected_prediction_tokens,omitempty"`
|
||||
|
||||
@@ -90,22 +90,23 @@ type BillingCache interface {
|
||||
|
||||
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
||||
type ModelPricing struct {
|
||||
InputPricePerToken float64 // 每token输入价格 (USD)
|
||||
InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD)
|
||||
ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken
|
||||
OutputPricePerToken float64 // 每token输出价格 (USD)
|
||||
OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD)
|
||||
CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD)
|
||||
CacheReadPricePerToken float64 // 缓存读取每token价格 (USD)
|
||||
CacheReadPricePerTokenPriority float64 // priority service tier 下缓存读取每token价格 (USD)
|
||||
CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD)
|
||||
CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD)
|
||||
SupportsCacheBreakdown bool // 是否支持详细的缓存分类
|
||||
LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格
|
||||
LongContextInputMultiplier float64 // 长上下文整次会话输入倍率
|
||||
LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率
|
||||
ImageOutputPricePerToken float64 // 图片输出 token 价格 (USD)
|
||||
ImageOutputPriceExplicit bool // 是否由渠道定价显式设定(为 true 时即使 == 0 也不回退)
|
||||
InputPricePerToken float64 // 每token输入价格 (USD)
|
||||
InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD)
|
||||
ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken
|
||||
OutputPricePerToken float64 // 每token输出价格 (USD)
|
||||
OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD)
|
||||
CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD)
|
||||
CacheCreationPricePerTokenPriority float64 // priority service tier 下缓存创建每token价格 (USD)
|
||||
CacheReadPricePerToken float64 // 缓存读取每token价格 (USD)
|
||||
CacheReadPricePerTokenPriority float64 // priority service tier 下缓存读取每token价格 (USD)
|
||||
CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD)
|
||||
CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD)
|
||||
SupportsCacheBreakdown bool // 是否支持详细的缓存分类
|
||||
LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格
|
||||
LongContextInputMultiplier float64 // 长上下文整次会话输入倍率
|
||||
LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率
|
||||
ImageOutputPricePerToken float64 // 图片输出 token 价格 (USD)
|
||||
ImageOutputPriceExplicit bool // 是否由渠道定价显式设定(为 true 时即使 == 0 也不回退)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -122,7 +123,8 @@ func usePriorityServiceTierPricing(serviceTier string, pricing *ModelPricing) bo
|
||||
if pricing == nil || normalizeBillingServiceTier(serviceTier) != "priority" {
|
||||
return false
|
||||
}
|
||||
return pricing.InputPricePerTokenPriority > 0 || pricing.OutputPricePerTokenPriority > 0 || pricing.CacheReadPricePerTokenPriority > 0
|
||||
return pricing.InputPricePerTokenPriority > 0 || pricing.OutputPricePerTokenPriority > 0 ||
|
||||
pricing.CacheCreationPricePerTokenPriority > 0 || pricing.CacheReadPricePerTokenPriority > 0
|
||||
}
|
||||
|
||||
func serviceTierCostMultiplier(serviceTier string) float64 {
|
||||
@@ -739,20 +741,21 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) {
|
||||
price1h := litellmPricing.CacheCreationInputTokenCostAbove1hr
|
||||
enableBreakdown := price1h > 0 && price1h > price5m
|
||||
return s.applyModelSpecificPricingPolicy(model, &ModelPricing{
|
||||
InputPricePerToken: litellmPricing.InputCostPerToken,
|
||||
InputPricePerTokenPriority: litellmPricing.InputCostPerTokenPriority,
|
||||
OutputPricePerToken: litellmPricing.OutputCostPerToken,
|
||||
OutputPricePerTokenPriority: litellmPricing.OutputCostPerTokenPriority,
|
||||
CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
|
||||
CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
|
||||
CacheReadPricePerTokenPriority: litellmPricing.CacheReadInputTokenCostPriority,
|
||||
CacheCreation5mPrice: price5m,
|
||||
CacheCreation1hPrice: price1h,
|
||||
SupportsCacheBreakdown: enableBreakdown,
|
||||
LongContextInputThreshold: litellmPricing.LongContextInputTokenThreshold,
|
||||
LongContextInputMultiplier: litellmPricing.LongContextInputCostMultiplier,
|
||||
LongContextOutputMultiplier: litellmPricing.LongContextOutputCostMultiplier,
|
||||
ImageOutputPricePerToken: litellmPricing.OutputCostPerImageToken,
|
||||
InputPricePerToken: litellmPricing.InputCostPerToken,
|
||||
InputPricePerTokenPriority: litellmPricing.InputCostPerTokenPriority,
|
||||
OutputPricePerToken: litellmPricing.OutputCostPerToken,
|
||||
OutputPricePerTokenPriority: litellmPricing.OutputCostPerTokenPriority,
|
||||
CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
|
||||
CacheCreationPricePerTokenPriority: litellmPricing.CacheCreationInputTokenCostPriority,
|
||||
CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
|
||||
CacheReadPricePerTokenPriority: litellmPricing.CacheReadInputTokenCostPriority,
|
||||
CacheCreation5mPrice: price5m,
|
||||
CacheCreation1hPrice: price1h,
|
||||
SupportsCacheBreakdown: enableBreakdown,
|
||||
LongContextInputThreshold: litellmPricing.LongContextInputTokenThreshold,
|
||||
LongContextInputMultiplier: litellmPricing.LongContextInputCostMultiplier,
|
||||
LongContextOutputMultiplier: litellmPricing.LongContextOutputCostMultiplier,
|
||||
ImageOutputPricePerToken: litellmPricing.OutputCostPerImageToken,
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
@@ -794,6 +797,7 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
|
||||
}
|
||||
if channelPricing.CacheWritePrice != nil {
|
||||
pricing.CacheCreationPricePerToken = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreationPricePerTokenPriority = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreation5mPrice = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreation1hPrice = *channelPricing.CacheWritePrice
|
||||
}
|
||||
@@ -867,7 +871,7 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown,
|
||||
|
||||
// calculateTokenCost 按 token 区间计费
|
||||
func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input CostInput) (*CostBreakdown, error) {
|
||||
totalContext := input.Tokens.InputTokens + input.Tokens.CacheReadTokens
|
||||
totalContext := input.Tokens.InputTokens + input.Tokens.CacheCreationTokens + input.Tokens.CacheReadTokens
|
||||
|
||||
pricing := input.Resolver.GetIntervalPricing(resolved, totalContext)
|
||||
if pricing == nil {
|
||||
@@ -897,6 +901,7 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
inputPrice := pricing.InputPricePerToken
|
||||
outputPrice := pricing.OutputPricePerToken
|
||||
cacheReadPrice := pricing.CacheReadPricePerToken
|
||||
cacheCreationPrice := pricing.CacheCreationPricePerToken
|
||||
cacheCreationMultiplier := 1.0
|
||||
tierMultiplier := 1.0
|
||||
|
||||
@@ -910,6 +915,9 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
if pricing.CacheReadPricePerTokenPriority > 0 {
|
||||
cacheReadPrice = pricing.CacheReadPricePerTokenPriority
|
||||
}
|
||||
if pricing.CacheCreationPricePerTokenPriority > 0 {
|
||||
cacheCreationPrice = pricing.CacheCreationPricePerTokenPriority
|
||||
}
|
||||
} else {
|
||||
tierMultiplier = serviceTierCostMultiplier(serviceTier)
|
||||
}
|
||||
@@ -963,7 +971,7 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
}
|
||||
|
||||
// 缓存创建费用
|
||||
bd.CacheCreationCost = s.computeCacheCreationCost(pricing, tokens, cacheCreationMultiplier)
|
||||
bd.CacheCreationCost = s.computeCacheCreationCost(pricing, tokens, cacheCreationPrice, cacheCreationMultiplier)
|
||||
|
||||
bd.CacheReadCost = float64(tokens.CacheReadTokens) * cacheReadPrice
|
||||
|
||||
@@ -984,7 +992,7 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
|
||||
// computeCacheCreationCost 计算缓存创建费用(支持 5m/1h 分类或标准计费)。
|
||||
// multiplier 用于长上下文等场景下的整体价格缩放(普通调用传 1.0 即可)。
|
||||
func (s *BillingService) computeCacheCreationCost(pricing *ModelPricing, tokens UsageTokens, multiplier float64) float64 {
|
||||
func (s *BillingService) computeCacheCreationCost(pricing *ModelPricing, tokens UsageTokens, price, multiplier float64) float64 {
|
||||
if pricing.SupportsCacheBreakdown && (pricing.CacheCreation5mPrice > 0 || pricing.CacheCreation1hPrice > 0) {
|
||||
if tokens.CacheCreation5mTokens == 0 && tokens.CacheCreation1hTokens == 0 && tokens.CacheCreationTokens > 0 {
|
||||
// API 未返回 ephemeral 明细,回退到全部按 5m 单价计费
|
||||
@@ -993,7 +1001,7 @@ func (s *BillingService) computeCacheCreationCost(pricing *ModelPricing, tokens
|
||||
return float64(tokens.CacheCreation5mTokens)*pricing.CacheCreation5mPrice*multiplier +
|
||||
float64(tokens.CacheCreation1hTokens)*pricing.CacheCreation1hPrice*multiplier
|
||||
}
|
||||
return float64(tokens.CacheCreationTokens) * pricing.CacheCreationPricePerToken * multiplier
|
||||
return float64(tokens.CacheCreationTokens) * price * multiplier
|
||||
}
|
||||
|
||||
// calculatePerRequestCost 按次/图片计费
|
||||
@@ -1010,7 +1018,7 @@ func (s *BillingService) calculatePerRequestCost(resolved *ResolvedPricing, inpu
|
||||
}
|
||||
|
||||
if unitPrice == 0 {
|
||||
totalContext := input.Tokens.InputTokens + input.Tokens.CacheReadTokens
|
||||
totalContext := input.Tokens.InputTokens + input.Tokens.CacheCreationTokens + input.Tokens.CacheReadTokens
|
||||
unitPrice = input.Resolver.GetRequestTierPriceByContext(resolved, totalContext)
|
||||
}
|
||||
|
||||
@@ -1057,13 +1065,26 @@ func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *
|
||||
if pricing == nil {
|
||||
return nil
|
||||
}
|
||||
normalized := normalizeKnownOpenAICodexModel(model)
|
||||
if !isOpenAIGPT54Model(model) {
|
||||
return pricing
|
||||
}
|
||||
if pricing.LongContextInputThreshold > 0 && pricing.LongContextInputMultiplier > 0 && pricing.LongContextOutputMultiplier > 0 {
|
||||
isGPT56 := normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" || normalized == "gpt-5.6-luna"
|
||||
needsLongContextPolicy := pricing.LongContextInputThreshold <= 0 || pricing.LongContextInputMultiplier <= 0 || pricing.LongContextOutputMultiplier <= 0
|
||||
needsCacheCreationPolicy := isGPT56 && (pricing.CacheCreationPricePerToken <= 0 ||
|
||||
(pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0))
|
||||
if !needsLongContextPolicy && !needsCacheCreationPolicy {
|
||||
return pricing
|
||||
}
|
||||
cloned := *pricing
|
||||
if isGPT56 {
|
||||
if cloned.CacheCreationPricePerToken <= 0 {
|
||||
cloned.CacheCreationPricePerToken = cloned.InputPricePerToken
|
||||
}
|
||||
if cloned.CacheCreationPricePerTokenPriority <= 0 {
|
||||
cloned.CacheCreationPricePerTokenPriority = cloned.InputPricePerTokenPriority
|
||||
}
|
||||
}
|
||||
if cloned.LongContextInputThreshold <= 0 {
|
||||
cloned.LongContextInputThreshold = openAIGPT54LongContextInputThreshold
|
||||
}
|
||||
@@ -1083,7 +1104,7 @@ func (s *BillingService) shouldApplySessionLongContextPricing(tokens UsageTokens
|
||||
if pricing.LongContextInputMultiplier <= 1 && pricing.LongContextOutputMultiplier <= 1 {
|
||||
return false
|
||||
}
|
||||
totalInputTokens := tokens.InputTokens + tokens.CacheReadTokens
|
||||
totalInputTokens := tokens.InputTokens + tokens.CacheCreationTokens + tokens.CacheReadTokens
|
||||
return totalInputTokens > pricing.LongContextInputThreshold
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
|
||||
}
|
||||
if chPricing.CacheWritePrice != nil {
|
||||
resolved.BasePricing.CacheCreationPricePerToken = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreationPricePerTokenPriority = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreation5mPrice = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreation1hPrice = *chPricing.CacheWritePrice
|
||||
}
|
||||
@@ -251,6 +252,7 @@ func intervalToModelPricing(iv *PricingInterval, supportsCacheBreakdown bool, ch
|
||||
}
|
||||
if iv.CacheWritePrice != nil {
|
||||
pricing.CacheCreationPricePerToken = *iv.CacheWritePrice
|
||||
pricing.CacheCreationPricePerTokenPriority = *iv.CacheWritePrice
|
||||
pricing.CacheCreation5mPrice = *iv.CacheWritePrice
|
||||
pricing.CacheCreation1hPrice = *iv.CacheWritePrice
|
||||
}
|
||||
|
||||
@@ -206,17 +206,8 @@ func extractOpenAIEmbeddingsUsage(body []byte) OpenAIUsage {
|
||||
usage.Get("completion_tokens"),
|
||||
usage.Get("output_tokens"),
|
||||
)
|
||||
cacheReadTokens := firstPositiveGJSONInt(
|
||||
usage.Get("prompt_tokens_details.cached_tokens"),
|
||||
usage.Get("input_tokens_details.cached_tokens"),
|
||||
usage.Get("cache_read_tokens"),
|
||||
usage.Get("cache_read_input_tokens"),
|
||||
)
|
||||
cacheCreationTokens := firstPositiveGJSONInt(
|
||||
usage.Get("cache_creation_tokens"),
|
||||
usage.Get("cache_creation_input_tokens"),
|
||||
usage.Get("input_tokens_details.cache_creation_tokens"),
|
||||
)
|
||||
cacheReadTokens := openAICacheReadTokensFromUsage(usage)
|
||||
cacheCreationTokens := openAICacheCreationTokensFromUsage(usage)
|
||||
// 多模态 embedding(如 doubao-embedding-vision)回传图文 token 拆分,
|
||||
// 用于图文不同价计费;纯文本 embedding 该字段为 0,行为不变。
|
||||
imageInputTokens := firstPositiveGJSONInt(
|
||||
|
||||
@@ -383,12 +383,9 @@ func extractCCStreamUsage(payload string) *OpenAIUsage {
|
||||
if !usageResult.Exists() || !usageResult.IsObject() {
|
||||
return nil
|
||||
}
|
||||
u := OpenAIUsage{
|
||||
InputTokens: int(gjson.Get(payload, "usage.prompt_tokens").Int()),
|
||||
OutputTokens: int(gjson.Get(payload, "usage.completion_tokens").Int()),
|
||||
}
|
||||
if cached := gjson.Get(payload, "usage.prompt_tokens_details.cached_tokens"); cached.Exists() {
|
||||
u.CacheReadInputTokens = int(cached.Int())
|
||||
u, ok := openAIUsageFromGJSON(usageResult)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
@@ -1114,11 +1114,19 @@ func copyOpenAIUsageFromResponsesUsage(usage *apicompat.ResponsesUsage) OpenAIUs
|
||||
return OpenAIUsage{}
|
||||
}
|
||||
result := OpenAIUsage{
|
||||
InputTokens: usage.InputTokens,
|
||||
OutputTokens: usage.OutputTokens,
|
||||
InputTokens: usage.InputTokens,
|
||||
OutputTokens: usage.OutputTokens,
|
||||
CacheCreationInputTokens: usage.CacheCreationInputTokens,
|
||||
}
|
||||
if usage.InputTokensDetails != nil {
|
||||
result.CacheReadInputTokens = usage.InputTokensDetails.CachedTokens
|
||||
if result.CacheCreationInputTokens == 0 {
|
||||
if usage.InputTokensDetails.CacheWriteTokens > 0 {
|
||||
result.CacheCreationInputTokens = usage.InputTokensDetails.CacheWriteTokens
|
||||
} else {
|
||||
result.CacheCreationInputTokens = usage.InputTokensDetails.CacheCreationTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ func expectedOpenAICost(t *testing.T, svc *OpenAIGatewayService, model string, u
|
||||
t.Helper()
|
||||
|
||||
cost, err := svc.billingService.CalculateCost(model, UsageTokens{
|
||||
InputTokens: max(usage.InputTokens-usage.CacheReadInputTokens, 0),
|
||||
InputTokens: max(usage.InputTokens-usage.CacheReadInputTokens-usage.CacheCreationInputTokens, 0),
|
||||
OutputTokens: usage.OutputTokens,
|
||||
CacheCreationTokens: usage.CacheCreationInputTokens,
|
||||
CacheReadTokens: usage.CacheReadInputTokens,
|
||||
@@ -1002,6 +1002,49 @@ func TestOpenAIGatewayServiceRecordUsage_ClampsActualInputTokensToZero(t *testin
|
||||
require.Equal(t, 0, usageRepo.lastLog.InputTokens)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_GPT56SeparatesCacheWriteForBillingAndStats(t *testing.T) {
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
userRepo := &openAIRecordUsageUserRepoStub{}
|
||||
subRepo := &openAIRecordUsageSubRepoStub{}
|
||||
svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil)
|
||||
svc.billingService = NewBillingService(svc.cfg, &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
"gpt-5.6-sol": {
|
||||
InputCostPerToken: 5e-6,
|
||||
OutputCostPerToken: 30e-6,
|
||||
CacheReadInputTokenCost: 0.5e-6,
|
||||
},
|
||||
}})
|
||||
|
||||
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
|
||||
Result: &OpenAIForwardResult{
|
||||
RequestID: "resp_gpt56_cache_write",
|
||||
Usage: OpenAIUsage{
|
||||
InputTokens: 1000,
|
||||
OutputTokens: 50,
|
||||
CacheCreationInputTokens: 200,
|
||||
CacheReadInputTokens: 100,
|
||||
},
|
||||
Model: "gpt-5.6-sol",
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{ID: 1056},
|
||||
User: &User{ID: 2056},
|
||||
Account: &Account{ID: 3056},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, usageRepo.lastLog)
|
||||
require.Equal(t, 700, usageRepo.lastLog.InputTokens)
|
||||
require.Equal(t, 200, usageRepo.lastLog.CacheCreationTokens)
|
||||
require.Equal(t, 100, usageRepo.lastLog.CacheReadTokens)
|
||||
require.Equal(t, 1050, usageRepo.lastLog.TotalTokens())
|
||||
require.InDelta(t, 700*5e-6, usageRepo.lastLog.InputCost, 1e-12)
|
||||
require.InDelta(t, 200*5e-6, usageRepo.lastLog.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 100*0.5e-6, usageRepo.lastLog.CacheReadCost, 1e-12)
|
||||
require.InDelta(t, 50*30e-6, usageRepo.lastLog.OutputCost, 1e-12)
|
||||
require.InDelta(t, usageRepo.lastLog.TotalCost*1.1, usageRepo.lastLog.ActualCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_Gpt54LongContextBillsWholeSession(t *testing.T) {
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
userRepo := &openAIRecordUsageUserRepoStub{}
|
||||
|
||||
@@ -756,10 +756,8 @@ func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
|
||||
if outputTokens == 0 {
|
||||
outputTokens = value.Get("completion_tokens").Int()
|
||||
}
|
||||
cacheReadTokens := value.Get("input_tokens_details.cached_tokens").Int()
|
||||
if cacheReadTokens == 0 {
|
||||
cacheReadTokens = value.Get("prompt_tokens_details.cached_tokens").Int()
|
||||
}
|
||||
cacheReadTokens := openAICacheReadTokensFromUsage(value)
|
||||
cacheCreationTokens := openAICacheCreationTokensFromUsage(value)
|
||||
imageOutputTokens := value.Get("output_tokens_details.image_tokens").Int()
|
||||
if imageOutputTokens == 0 {
|
||||
imageOutputTokens = value.Get("completion_tokens_details.image_tokens").Int()
|
||||
@@ -767,12 +765,35 @@ func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
|
||||
return OpenAIUsage{
|
||||
InputTokens: int(inputTokens),
|
||||
OutputTokens: int(outputTokens),
|
||||
CacheCreationInputTokens: int(value.Get("cache_creation_input_tokens").Int()),
|
||||
CacheReadInputTokens: int(cacheReadTokens),
|
||||
CacheCreationInputTokens: cacheCreationTokens,
|
||||
CacheReadInputTokens: cacheReadTokens,
|
||||
ImageOutputTokens: int(imageOutputTokens),
|
||||
}, true
|
||||
}
|
||||
|
||||
func openAICacheReadTokensFromUsage(value gjson.Result) int {
|
||||
return firstPositiveGJSONInt(
|
||||
value.Get("input_tokens_details.cached_tokens"),
|
||||
value.Get("prompt_tokens_details.cached_tokens"),
|
||||
value.Get("cache_read_input_tokens"),
|
||||
value.Get("cache_read_tokens"),
|
||||
value.Get("cached_tokens"),
|
||||
)
|
||||
}
|
||||
|
||||
func openAICacheCreationTokensFromUsage(value gjson.Result) int {
|
||||
return firstPositiveGJSONInt(
|
||||
value.Get("cache_creation_input_tokens"),
|
||||
value.Get("cache_write_input_tokens"),
|
||||
value.Get("cache_creation_tokens"),
|
||||
value.Get("cache_write_tokens"),
|
||||
value.Get("input_tokens_details.cache_creation_tokens"),
|
||||
value.Get("input_tokens_details.cache_write_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_creation_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_write_tokens"),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) handleNonStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, originalModel, mappedModel string) (*openaiNonStreamingResult, error) {
|
||||
body, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError)
|
||||
if err != nil {
|
||||
|
||||
@@ -2800,17 +2800,23 @@ func TestParseSSEUsage_SelectiveParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExtractOpenAIUsageFromJSONBytes_AcceptsResponseAndChatUsageShapes(t *testing.T) {
|
||||
usage, ok := extractOpenAIUsageFromJSONBytes([]byte(`{"id":"resp_1","usage":{"input_tokens":3,"output_tokens":5,"input_tokens_details":{"cached_tokens":2}}}`))
|
||||
usage, ok := extractOpenAIUsageFromJSONBytes([]byte(`{"id":"resp_1","usage":{"input_tokens":9,"output_tokens":5,"input_tokens_details":{"cached_tokens":2,"cache_write_tokens":4}}}`))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 3, usage.InputTokens)
|
||||
require.Equal(t, 9, usage.InputTokens)
|
||||
require.Equal(t, 5, usage.OutputTokens)
|
||||
require.Equal(t, 2, usage.CacheReadInputTokens)
|
||||
require.Equal(t, 4, usage.CacheCreationInputTokens)
|
||||
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"type":"response.completed","response":{"usage":{"prompt_tokens":13,"completion_tokens":7,"prompt_tokens_details":{"cached_tokens":4}}}}`))
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"type":"response.completed","response":{"usage":{"prompt_tokens":13,"completion_tokens":7,"prompt_tokens_details":{"cached_tokens":4,"cache_creation_tokens":3}}}}`))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 13, usage.InputTokens)
|
||||
require.Equal(t, 7, usage.OutputTokens)
|
||||
require.Equal(t, 4, usage.CacheReadInputTokens)
|
||||
require.Equal(t, 3, usage.CacheCreationInputTokens)
|
||||
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"usage":{"input_tokens":11,"output_tokens":2,"cache_write_input_tokens":6}}`))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 6, usage.CacheCreationInputTokens)
|
||||
}
|
||||
|
||||
func TestExtractCodexFinalResponse_SampleReplay(t *testing.T) {
|
||||
|
||||
@@ -119,9 +119,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
ApplyOpenAIImageBillingResolution(result)
|
||||
}
|
||||
|
||||
// 计算实际的新输入token(减去缓存读取的token)
|
||||
// 因为 input_tokens 包含了 cache_read_tokens,而缓存读取的token不应按输入价格计费
|
||||
actualInputTokens := result.Usage.InputTokens - result.Usage.CacheReadInputTokens
|
||||
// OpenAI input_tokens 是总输入,包含缓存读取和缓存写入明细。
|
||||
// 将三类 token 拆成互斥桶,避免缓存写入同时按普通输入和 cache_write 重复计费。
|
||||
actualInputTokens := result.Usage.InputTokens - result.Usage.CacheReadInputTokens - result.Usage.CacheCreationInputTokens
|
||||
if actualInputTokens < 0 {
|
||||
actualInputTokens = 0
|
||||
}
|
||||
|
||||
@@ -262,15 +262,9 @@ func populateOpenAIUsageFromResponseJSON(body []byte, usage *OpenAIUsage) {
|
||||
if usage == nil || len(body) == 0 {
|
||||
return
|
||||
}
|
||||
values := gjson.GetManyBytes(
|
||||
body,
|
||||
"usage.input_tokens",
|
||||
"usage.output_tokens",
|
||||
"usage.input_tokens_details.cached_tokens",
|
||||
)
|
||||
usage.InputTokens = int(values[0].Int())
|
||||
usage.OutputTokens = int(values[1].Int())
|
||||
usage.CacheReadInputTokens = int(values[2].Int())
|
||||
if parsed, ok := extractOpenAIUsageFromJSONBytes(body); ok {
|
||||
*usage = parsed
|
||||
}
|
||||
}
|
||||
|
||||
func getOpenAIGroupIDFromContext(c *gin.Context) int64 {
|
||||
|
||||
@@ -787,7 +787,7 @@ func parseUsageAndAccumulate(
|
||||
parsedUsage := Usage{
|
||||
InputTokens: inputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
CacheCreationInputTokens: int(usageResult.Get("cache_creation_input_tokens").Int()),
|
||||
CacheCreationInputTokens: openAICacheCreationTokensFromUsage(usageResult),
|
||||
CacheReadInputTokens: cachedTokens,
|
||||
ImageOutputTokens: int(imageTokens),
|
||||
}
|
||||
@@ -810,6 +810,24 @@ func parseUsageIntField(value gjson.Result, required bool) (int, bool) {
|
||||
return int(value.Int()), true
|
||||
}
|
||||
|
||||
func openAICacheCreationTokensFromUsage(value gjson.Result) int {
|
||||
for _, field := range []string{
|
||||
"cache_creation_input_tokens",
|
||||
"cache_write_input_tokens",
|
||||
"cache_creation_tokens",
|
||||
"cache_write_tokens",
|
||||
"input_tokens_details.cache_creation_tokens",
|
||||
"input_tokens_details.cache_write_tokens",
|
||||
"prompt_tokens_details.cache_creation_tokens",
|
||||
"prompt_tokens_details.cache_write_tokens",
|
||||
} {
|
||||
if tokens := int(value.Get(field).Int()); tokens > 0 {
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func enrichResult(result *RelayResult, state *relayState, duration time.Duration) {
|
||||
if result == nil {
|
||||
return
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestParseUsageAndEnrichCoverage(t *testing.T) {
|
||||
require.Equal(t, 0, state.usage.OutputTokens)
|
||||
require.Equal(t, 0, state.usage.CacheReadInputTokens)
|
||||
|
||||
parseUsageAndAccumulate(state, []byte(`{"type":"response.completed","response":{"usage":{"input_tokens":2,"output_tokens":1,"input_tokens_details":{"cached_tokens":1},"cache_creation_input_tokens":4,"output_tokens_details":{"image_tokens":3}}}}`), "response.completed", nil)
|
||||
parseUsageAndAccumulate(state, []byte(`{"type":"response.completed","response":{"usage":{"input_tokens":2,"output_tokens":1,"input_tokens_details":{"cached_tokens":1,"cache_write_tokens":4},"output_tokens_details":{"image_tokens":3}}}}`), "response.completed", nil)
|
||||
require.Equal(t, 2, state.usage.InputTokens)
|
||||
require.Equal(t, 1, state.usage.OutputTokens)
|
||||
require.Equal(t, 1, state.usage.CacheReadInputTokens)
|
||||
|
||||
@@ -61,6 +61,7 @@ type LiteLLMModelPricing struct {
|
||||
OutputCostPerToken float64 `json:"output_cost_per_token"`
|
||||
OutputCostPerTokenPriority float64 `json:"output_cost_per_token_priority"`
|
||||
CacheCreationInputTokenCost float64 `json:"cache_creation_input_token_cost"`
|
||||
CacheCreationInputTokenCostPriority float64 `json:"cache_creation_input_token_cost_priority"`
|
||||
CacheCreationInputTokenCostAbove1hr float64 `json:"cache_creation_input_token_cost_above_1hr"`
|
||||
CacheReadInputTokenCost float64 `json:"cache_read_input_token_cost"`
|
||||
CacheReadInputTokenCostPriority float64 `json:"cache_read_input_token_cost_priority"`
|
||||
@@ -93,6 +94,7 @@ type LiteLLMRawEntry struct {
|
||||
OutputCostPerToken *float64 `json:"output_cost_per_token"`
|
||||
OutputCostPerTokenPriority *float64 `json:"output_cost_per_token_priority"`
|
||||
CacheCreationInputTokenCost *float64 `json:"cache_creation_input_token_cost"`
|
||||
CacheCreationInputTokenCostPriority *float64 `json:"cache_creation_input_token_cost_priority"`
|
||||
CacheCreationInputTokenCostAbove1hr *float64 `json:"cache_creation_input_token_cost_above_1hr"`
|
||||
CacheReadInputTokenCost *float64 `json:"cache_read_input_token_cost"`
|
||||
CacheReadInputTokenCostPriority *float64 `json:"cache_read_input_token_cost_priority"`
|
||||
@@ -406,6 +408,9 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel
|
||||
if entry.CacheCreationInputTokenCost != nil {
|
||||
pricing.CacheCreationInputTokenCost = *entry.CacheCreationInputTokenCost
|
||||
}
|
||||
if entry.CacheCreationInputTokenCostPriority != nil {
|
||||
pricing.CacheCreationInputTokenCostPriority = *entry.CacheCreationInputTokenCostPriority
|
||||
}
|
||||
if entry.CacheCreationInputTokenCostAbove1hr != nil {
|
||||
pricing.CacheCreationInputTokenCostAbove1hr = *entry.CacheCreationInputTokenCostAbove1hr
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) {
|
||||
"output_cost_per_token": 0.000015,
|
||||
"output_cost_per_token_priority": 0.00003,
|
||||
"cache_creation_input_token_cost": 0.0000025,
|
||||
"cache_creation_input_token_cost_priority": 0.000005,
|
||||
"cache_read_input_token_cost": 0.00000025,
|
||||
"cache_read_input_token_cost_priority": 0.0000005,
|
||||
"supports_service_tier": true,
|
||||
@@ -34,10 +35,66 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) {
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, 5e-6, pricing.InputCostPerTokenPriority, 1e-12)
|
||||
require.InDelta(t, 3e-5, pricing.OutputCostPerTokenPriority, 1e-12)
|
||||
require.InDelta(t, 5e-6, pricing.CacheCreationInputTokenCostPriority, 1e-12)
|
||||
require.InDelta(t, 5e-7, pricing.CacheReadInputTokenCostPriority, 1e-12)
|
||||
require.True(t, pricing.SupportsServiceTier)
|
||||
}
|
||||
|
||||
func TestBillingService_GPT56CacheWritePricingUsesInputTier(t *testing.T) {
|
||||
for _, model := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} {
|
||||
t.Run(model, func(t *testing.T) {
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
model: {
|
||||
InputCostPerToken: 5e-6,
|
||||
InputCostPerTokenPriority: 10e-6,
|
||||
OutputCostPerToken: 30e-6,
|
||||
OutputCostPerTokenPriority: 60e-6,
|
||||
CacheReadInputTokenCost: 0.5e-6,
|
||||
CacheReadInputTokenCostPriority: 1e-6,
|
||||
},
|
||||
}}
|
||||
svc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
|
||||
pricing, err := svc.GetModelPricing(model)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 5e-6, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, 10e-6, pricing.CacheCreationPricePerTokenPriority, 1e-12)
|
||||
|
||||
tokens := UsageTokens{InputTokens: 700, OutputTokens: 50, CacheCreationTokens: 200, CacheReadTokens: 100}
|
||||
standard, err := svc.CalculateCostWithServiceTier(model, tokens, 1, "")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*5e-6, standard.CacheCreationCost, 1e-12)
|
||||
|
||||
priority, err := svc.CalculateCostWithServiceTier(model, tokens, 1, "priority")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*10e-6, priority.CacheCreationCost, 1e-12)
|
||||
|
||||
flex, err := svc.CalculateCostWithServiceTier(model, tokens, 1, "flex")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*2.5e-6, flex.CacheCreationCost, 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingService_GPT56CacheWriteContributesToLongContextThreshold(t *testing.T) {
|
||||
model := "gpt-5.6-sol"
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
model: {
|
||||
InputCostPerToken: 5e-6,
|
||||
OutputCostPerToken: 30e-6,
|
||||
CacheReadInputTokenCost: 0.5e-6,
|
||||
},
|
||||
}}
|
||||
svc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
tokens := UsageTokens{InputTokens: 100000, CacheCreationTokens: 173000, OutputTokens: 10}
|
||||
|
||||
cost, err := svc.CalculateCost(model, tokens, 1)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 100000*10e-6, cost.InputCost, 1e-12)
|
||||
require.InDelta(t, 173000*10e-6, cost.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 10*45e-6, cost.OutputCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestParsePricingData_KeepsImageOnlyPricing(t *testing.T) {
|
||||
svc := &PricingService{}
|
||||
body := []byte(`{
|
||||
|
||||
Reference in New Issue
Block a user