mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3898 from Arron196/feat/gpt-5.6-cache-billing-stats
feat(openai): 支持 GPT-5.6 系列缓存写入计费与统计
This commit is contained in:
@@ -609,9 +609,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.
|
||||
@@ -546,6 +562,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"`
|
||||
|
||||
@@ -53,11 +53,11 @@ func (s *UserMsgQueueCacheSuite) TestReconcileExpiredLockCandidatesRemovesNatura
|
||||
require.NoError(s.T(), err)
|
||||
require.True(s.T(), acquired)
|
||||
|
||||
score, err := s.rdb.ZScore(s.ctx, umqLockIndexKey, "702").Result()
|
||||
_, err = s.rdb.ZScore(s.ctx, umqLockIndexKey, "702").Result()
|
||||
require.NoError(s.T(), err)
|
||||
require.Eventually(s.T(), func() bool {
|
||||
nowMs, err := s.cache.GetCurrentTimeMs(s.ctx)
|
||||
return err == nil && nowMs >= int64(score)
|
||||
_, err := s.rdb.Get(s.ctx, umqLockKey(accountID)).Result()
|
||||
return errors.Is(err, redis.Nil)
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
cleaned, err := s.cache.ReconcileExpiredLockCandidates(s.ctx, 1000)
|
||||
|
||||
@@ -90,22 +90,24 @@ 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)
|
||||
CacheCreationPriceExplicit bool // 是否由渠道/区间定价显式设定(为 true 时即使 == 0 也不回退)
|
||||
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 +124,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 {
|
||||
@@ -280,10 +283,37 @@ func (s *BillingService) initFallbackPricing() {
|
||||
s.fallbackPrices["gpt-5.5"] = s.fallbackPrices["gpt-5.4"]
|
||||
s.fallbackPrices["gpt-5.5-pro"] = s.fallbackPrices["gpt-5.4"]
|
||||
|
||||
// GPT-5.6(sol / terra / luna)暂无独立定价,回退到 GPT-5.4。
|
||||
s.fallbackPrices["gpt-5.6-sol"] = s.fallbackPrices["gpt-5.4"]
|
||||
s.fallbackPrices["gpt-5.6-terra"] = s.fallbackPrices["gpt-5.4"]
|
||||
s.fallbackPrices["gpt-5.6-luna"] = s.fallbackPrices["gpt-5.4"]
|
||||
// OpenAI GPT-5.6 官方价格(USD/token)。缓存写入为输入价的 1.25 倍。
|
||||
s.fallbackPrices["gpt-5.6-sol"] = &ModelPricing{
|
||||
InputPricePerToken: 5e-6,
|
||||
InputPricePerTokenPriority: 10e-6,
|
||||
OutputPricePerToken: 30e-6,
|
||||
OutputPricePerTokenPriority: 60e-6,
|
||||
CacheCreationPricePerToken: 6.25e-6,
|
||||
CacheCreationPricePerTokenPriority: 12.5e-6,
|
||||
CacheReadPricePerToken: 0.5e-6,
|
||||
CacheReadPricePerTokenPriority: 1e-6,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.6-terra"] = &ModelPricing{
|
||||
InputPricePerToken: 2.5e-6,
|
||||
InputPricePerTokenPriority: 5e-6,
|
||||
OutputPricePerToken: 15e-6,
|
||||
OutputPricePerTokenPriority: 30e-6,
|
||||
CacheCreationPricePerToken: 3.125e-6,
|
||||
CacheCreationPricePerTokenPriority: 6.25e-6,
|
||||
CacheReadPricePerToken: 0.25e-6,
|
||||
CacheReadPricePerTokenPriority: 0.5e-6,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.6-luna"] = &ModelPricing{
|
||||
InputPricePerToken: 1e-6,
|
||||
InputPricePerTokenPriority: 2e-6,
|
||||
OutputPricePerToken: 6e-6,
|
||||
OutputPricePerTokenPriority: 12e-6,
|
||||
CacheCreationPricePerToken: 1.25e-6,
|
||||
CacheCreationPricePerTokenPriority: 2.5e-6,
|
||||
CacheReadPricePerToken: 0.1e-6,
|
||||
CacheReadPricePerTokenPriority: 0.2e-6,
|
||||
}
|
||||
|
||||
s.fallbackPrices["gpt-5.4-mini"] = &ModelPricing{
|
||||
InputPricePerToken: 7.5e-7,
|
||||
@@ -739,20 +769,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 +825,8 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
|
||||
}
|
||||
if channelPricing.CacheWritePrice != nil {
|
||||
pricing.CacheCreationPricePerToken = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreationPricePerTokenPriority = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreationPriceExplicit = true
|
||||
pricing.CacheCreation5mPrice = *channelPricing.CacheWritePrice
|
||||
pricing.CacheCreation1hPrice = *channelPricing.CacheWritePrice
|
||||
}
|
||||
@@ -867,7 +900,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 +930,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 +944,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 +1000,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 +1021,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 +1030,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 +1047,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,21 +1094,38 @@ func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *
|
||||
if pricing == nil {
|
||||
return nil
|
||||
}
|
||||
if !isOpenAIGPT54Model(model) {
|
||||
normalized := normalizeKnownOpenAICodexModel(model)
|
||||
isGPT56 := isOpenAIGPT56Model(normalized)
|
||||
usesLegacyLongContextPricing := usesOpenAILegacyLongContextPricing(normalized)
|
||||
if !isGPT56 && !usesLegacyLongContextPricing {
|
||||
return pricing
|
||||
}
|
||||
if pricing.LongContextInputThreshold > 0 && pricing.LongContextInputMultiplier > 0 && pricing.LongContextOutputMultiplier > 0 {
|
||||
needsLongContextPolicy := usesLegacyLongContextPricing &&
|
||||
(pricing.LongContextInputThreshold <= 0 || pricing.LongContextInputMultiplier <= 0 || pricing.LongContextOutputMultiplier <= 0)
|
||||
needsCacheCreationPolicy := isGPT56 && !pricing.CacheCreationPriceExplicit && (pricing.CacheCreationPricePerToken <= 0 ||
|
||||
(pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0))
|
||||
if !needsLongContextPolicy && !needsCacheCreationPolicy {
|
||||
return pricing
|
||||
}
|
||||
cloned := *pricing
|
||||
if cloned.LongContextInputThreshold <= 0 {
|
||||
cloned.LongContextInputThreshold = openAIGPT54LongContextInputThreshold
|
||||
if isGPT56 {
|
||||
if cloned.CacheCreationPricePerToken <= 0 {
|
||||
cloned.CacheCreationPricePerToken = cloned.InputPricePerToken * 1.25
|
||||
}
|
||||
if cloned.CacheCreationPricePerTokenPriority <= 0 {
|
||||
cloned.CacheCreationPricePerTokenPriority = cloned.InputPricePerTokenPriority * 1.25
|
||||
}
|
||||
}
|
||||
if cloned.LongContextInputMultiplier <= 0 {
|
||||
cloned.LongContextInputMultiplier = openAIGPT54LongContextInputMultiplier
|
||||
}
|
||||
if cloned.LongContextOutputMultiplier <= 0 {
|
||||
cloned.LongContextOutputMultiplier = openAIGPT54LongContextOutputMultiplier
|
||||
if usesLegacyLongContextPricing {
|
||||
if cloned.LongContextInputThreshold <= 0 {
|
||||
cloned.LongContextInputThreshold = openAIGPT54LongContextInputThreshold
|
||||
}
|
||||
if cloned.LongContextInputMultiplier <= 0 {
|
||||
cloned.LongContextInputMultiplier = openAIGPT54LongContextInputMultiplier
|
||||
}
|
||||
if cloned.LongContextOutputMultiplier <= 0 {
|
||||
cloned.LongContextOutputMultiplier = openAIGPT54LongContextOutputMultiplier
|
||||
}
|
||||
}
|
||||
return &cloned
|
||||
}
|
||||
@@ -1083,17 +1137,16 @@ 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
|
||||
}
|
||||
|
||||
func isOpenAIGPT54Model(model string) bool {
|
||||
// 仅当模型字符串实际属于已知 GPT-5/Codex 族时才做归一判定,避免
|
||||
// normalizeCodexModel 的默认兜底把非 OpenAI 模型(claude-*、gemini-*、gpt-4o)
|
||||
// 误识别为 gpt-5.4。
|
||||
normalized := normalizeKnownOpenAICodexModel(model)
|
||||
return normalized == "gpt-5.4" || normalized == "gpt-5.5" || normalized == "gpt-5.5-pro" ||
|
||||
normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" || normalized == "gpt-5.6-luna"
|
||||
func isOpenAIGPT56Model(normalized string) bool {
|
||||
return normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" || normalized == "gpt-5.6-luna"
|
||||
}
|
||||
|
||||
func usesOpenAILegacyLongContextPricing(normalized string) bool {
|
||||
return normalized == "gpt-5.4" || normalized == "gpt-5.5" || normalized == "gpt-5.5-pro"
|
||||
}
|
||||
|
||||
// CalculateCostWithConfig 使用配置中的默认倍率计算费用
|
||||
|
||||
@@ -183,6 +183,8 @@ func (r *ModelPricingResolver) applyTokenOverrides(chPricing *ChannelModelPricin
|
||||
}
|
||||
if chPricing.CacheWritePrice != nil {
|
||||
resolved.BasePricing.CacheCreationPricePerToken = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreationPricePerTokenPriority = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreationPriceExplicit = true
|
||||
resolved.BasePricing.CacheCreation5mPrice = *chPricing.CacheWritePrice
|
||||
resolved.BasePricing.CacheCreation1hPrice = *chPricing.CacheWritePrice
|
||||
}
|
||||
@@ -251,6 +253,8 @@ func intervalToModelPricing(iv *PricingInterval, supportsCacheBreakdown bool, ch
|
||||
}
|
||||
if iv.CacheWritePrice != nil {
|
||||
pricing.CacheCreationPricePerToken = *iv.CacheWritePrice
|
||||
pricing.CacheCreationPricePerTokenPriority = *iv.CacheWritePrice
|
||||
pricing.CacheCreationPriceExplicit = true
|
||||
pricing.CacheCreation5mPrice = *iv.CacheWritePrice
|
||||
pricing.CacheCreation1hPrice = *iv.CacheWritePrice
|
||||
}
|
||||
|
||||
@@ -114,6 +114,52 @@ func TestGetIntervalPricing_NoMatch_FallsBackToBase(t *testing.T) {
|
||||
require.Equal(t, basePricing, result)
|
||||
}
|
||||
|
||||
func TestGPT56ExplicitZeroCacheWritePriceIsPreserved(t *testing.T) {
|
||||
bs := &BillingService{}
|
||||
resolver := NewModelPricingResolver(nil, bs)
|
||||
zero := 0.0
|
||||
|
||||
t.Run("flat channel price", func(t *testing.T) {
|
||||
resolved := &ResolvedPricing{
|
||||
Mode: BillingModeToken,
|
||||
BasePricing: &ModelPricing{
|
||||
InputPricePerToken: 5e-6,
|
||||
OutputPricePerToken: 30e-6,
|
||||
},
|
||||
}
|
||||
resolver.applyTokenOverrides(&ChannelModelPricing{CacheWritePrice: &zero}, resolved)
|
||||
|
||||
require.True(t, resolved.BasePricing.CacheCreationPriceExplicit)
|
||||
cost, err := bs.CalculateCostUnified(CostInput{
|
||||
Model: "gpt-5.6-sol",
|
||||
Tokens: UsageTokens{CacheCreationTokens: 100},
|
||||
RateMultiplier: 1,
|
||||
Resolver: resolver,
|
||||
Resolved: resolved,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cost.CacheCreationCost)
|
||||
})
|
||||
|
||||
t.Run("interval price", func(t *testing.T) {
|
||||
pricing := intervalToModelPricing(&PricingInterval{CacheWritePrice: &zero}, false, nil)
|
||||
require.True(t, pricing.CacheCreationPriceExplicit)
|
||||
|
||||
cost, err := bs.CalculateCostUnified(CostInput{
|
||||
Model: "gpt-5.6-sol",
|
||||
Tokens: UsageTokens{CacheCreationTokens: 100},
|
||||
RateMultiplier: 1,
|
||||
Resolver: resolver,
|
||||
Resolved: &ResolvedPricing{
|
||||
Mode: BillingModeToken,
|
||||
BasePricing: pricing,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cost.CacheCreationCost)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRequestTierPrice(t *testing.T) {
|
||||
bs := newTestBillingServiceForResolver()
|
||||
r := NewModelPricingResolver(&ChannelService{}, bs)
|
||||
|
||||
@@ -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*6.25e-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("input_tokens_details.cache_write_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_write_tokens"),
|
||||
value.Get("input_tokens_details.cache_creation_tokens"),
|
||||
value.Get("prompt_tokens_details.cache_creation_tokens"),
|
||||
value.Get("cache_write_tokens"),
|
||||
value.Get("cache_creation_input_tokens"),
|
||||
value.Get("cache_write_input_tokens"),
|
||||
value.Get("cache_creation_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,27 @@ 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)
|
||||
|
||||
usage, ok = extractOpenAIUsageFromJSONBytes([]byte(`{"usage":{"input_tokens":20,"output_tokens":2,"cache_creation_input_tokens":19,"input_tokens_details":{"cache_write_tokens":7}}}`))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 7, 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
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func TestIsOpenAIWSClientDisconnectError(t *testing.T) {
|
||||
{name: "ws_policy_violation", err: coderws.CloseError{Code: coderws.StatusPolicyViolation}, want: false},
|
||||
{name: "wrapped_eof_message", err: errors.New("failed to get reader: failed to read frame header: EOF"), want: true},
|
||||
{name: "connection_reset_by_peer", err: errors.New("failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: read: connection reset by peer"), want: true},
|
||||
{name: "windows_connection_reset", err: errors.New("failed to get reader: failed to read frame header: read tcp 127.0.0.1:1234->127.0.0.1:5678: wsarecv: An existing connection was forcibly closed by the remote host."), want: true},
|
||||
{name: "broken_pipe", err: errors.New("write tcp 127.0.0.1:1234->127.0.0.1:5678: write: broken pipe"), want: true},
|
||||
}
|
||||
|
||||
|
||||
@@ -660,6 +660,7 @@ func isOpenAIWSClientDisconnectError(err error) bool {
|
||||
strings.Contains(message, "use of closed network connection") ||
|
||||
strings.Contains(message, "connection reset by peer") ||
|
||||
strings.Contains(message, "broken pipe") ||
|
||||
strings.Contains(message, "an existing connection was forcibly closed by the remote host") ||
|
||||
strings.Contains(message, "an established connection was aborted")
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
"input_tokens_details.cache_write_tokens",
|
||||
"prompt_tokens_details.cache_write_tokens",
|
||||
"input_tokens_details.cache_creation_tokens",
|
||||
"prompt_tokens_details.cache_creation_tokens",
|
||||
"cache_write_tokens",
|
||||
"cache_creation_input_tokens",
|
||||
"cache_write_input_tokens",
|
||||
"cache_creation_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)
|
||||
|
||||
@@ -35,6 +35,48 @@ var (
|
||||
Mode: "chat",
|
||||
SupportsPromptCaching: true,
|
||||
}
|
||||
openAIGPT56SolFallbackPricing = &LiteLLMModelPricing{
|
||||
InputCostPerToken: 5e-06,
|
||||
InputCostPerTokenPriority: 1e-05,
|
||||
OutputCostPerToken: 3e-05,
|
||||
OutputCostPerTokenPriority: 6e-05,
|
||||
CacheCreationInputTokenCost: 6.25e-06,
|
||||
CacheCreationInputTokenCostPriority: 1.25e-05,
|
||||
CacheReadInputTokenCost: 5e-07,
|
||||
CacheReadInputTokenCostPriority: 1e-06,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
SupportsPromptCaching: true,
|
||||
}
|
||||
openAIGPT56TerraFallbackPricing = &LiteLLMModelPricing{
|
||||
InputCostPerToken: 2.5e-06,
|
||||
InputCostPerTokenPriority: 5e-06,
|
||||
OutputCostPerToken: 1.5e-05,
|
||||
OutputCostPerTokenPriority: 3e-05,
|
||||
CacheCreationInputTokenCost: 3.125e-06,
|
||||
CacheCreationInputTokenCostPriority: 6.25e-06,
|
||||
CacheReadInputTokenCost: 2.5e-07,
|
||||
CacheReadInputTokenCostPriority: 5e-07,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
SupportsPromptCaching: true,
|
||||
}
|
||||
openAIGPT56LunaFallbackPricing = &LiteLLMModelPricing{
|
||||
InputCostPerToken: 1e-06,
|
||||
InputCostPerTokenPriority: 2e-06,
|
||||
OutputCostPerToken: 6e-06,
|
||||
OutputCostPerTokenPriority: 1.2e-05,
|
||||
CacheCreationInputTokenCost: 1.25e-06,
|
||||
CacheCreationInputTokenCostPriority: 2.5e-06,
|
||||
CacheReadInputTokenCost: 1e-07,
|
||||
CacheReadInputTokenCostPriority: 2e-07,
|
||||
SupportsServiceTier: true,
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
SupportsPromptCaching: true,
|
||||
}
|
||||
openAIGPT54MiniFallbackPricing = &LiteLLMModelPricing{
|
||||
InputCostPerToken: 7.5e-07,
|
||||
OutputCostPerToken: 4.5e-06,
|
||||
@@ -61,6 +103,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 +136,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 +450,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
|
||||
}
|
||||
@@ -837,11 +884,20 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
||||
}
|
||||
}
|
||||
|
||||
// GPT-5.6(sol / terra / luna)回退到 GPT-5.4 定价
|
||||
if strings.HasPrefix(model, "gpt-5.6") {
|
||||
if strings.HasPrefix(model, "gpt-5.6-sol") {
|
||||
logger.With(zap.String("component", "service.pricing")).
|
||||
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4(static)"))
|
||||
return openAIGPT54FallbackPricing
|
||||
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.6-sol(static)"))
|
||||
return openAIGPT56SolFallbackPricing
|
||||
}
|
||||
if strings.HasPrefix(model, "gpt-5.6-terra") {
|
||||
logger.With(zap.String("component", "service.pricing")).
|
||||
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.6-terra(static)"))
|
||||
return openAIGPT56TerraFallbackPricing
|
||||
}
|
||||
if strings.HasPrefix(model, "gpt-5.6-luna") {
|
||||
logger.With(zap.String("component", "service.pricing")).
|
||||
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.6-luna(static)"))
|
||||
return openAIGPT56LunaFallbackPricing
|
||||
}
|
||||
|
||||
// GPT-5.5 回退到 GPT-5.4 定价
|
||||
|
||||
@@ -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,155 @@ 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_GPT56CacheWritePricingUsesOfficialMultiplier(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
input float64
|
||||
inputPriority float64
|
||||
output float64
|
||||
outputPriority float64
|
||||
cacheRead float64
|
||||
cacheReadPriority float64
|
||||
}{
|
||||
{model: "gpt-5.6-sol", input: 5e-6, inputPriority: 10e-6, output: 30e-6, outputPriority: 60e-6, cacheRead: 0.5e-6, cacheReadPriority: 1e-6},
|
||||
{model: "gpt-5.6-terra", input: 2.5e-6, inputPriority: 5e-6, output: 15e-6, outputPriority: 30e-6, cacheRead: 0.25e-6, cacheReadPriority: 0.5e-6},
|
||||
{model: "gpt-5.6-luna", input: 1e-6, inputPriority: 2e-6, output: 6e-6, outputPriority: 12e-6, cacheRead: 0.1e-6, cacheReadPriority: 0.2e-6},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
tt.model: {
|
||||
InputCostPerToken: tt.input,
|
||||
InputCostPerTokenPriority: tt.inputPriority,
|
||||
OutputCostPerToken: tt.output,
|
||||
OutputCostPerTokenPriority: tt.outputPriority,
|
||||
CacheReadInputTokenCost: tt.cacheRead,
|
||||
CacheReadInputTokenCostPriority: tt.cacheReadPriority,
|
||||
},
|
||||
}}
|
||||
svc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
|
||||
pricing, err := svc.GetModelPricing(tt.model)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, tt.input*1.25, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.inputPriority*1.25, pricing.CacheCreationPricePerTokenPriority, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
|
||||
tokens := UsageTokens{InputTokens: 700, OutputTokens: 50, CacheCreationTokens: 200, CacheReadTokens: 100}
|
||||
standard, err := svc.CalculateCostWithServiceTier(tt.model, tokens, 1, "")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*tt.input*1.25, standard.CacheCreationCost, 1e-12)
|
||||
|
||||
priority, err := svc.CalculateCostWithServiceTier(tt.model, tokens, 1, "priority")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*tt.inputPriority*1.25, priority.CacheCreationCost, 1e-12)
|
||||
|
||||
flex, err := svc.CalculateCostWithServiceTier(tt.model, tokens, 1, "flex")
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 200*tt.input*1.25*0.5, flex.CacheCreationCost, 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingService_GPT56DoesNotUseLegacyLongContextMultiplier(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*5e-6, cost.InputCost, 1e-12)
|
||||
require.InDelta(t, 173000*6.25e-6, cost.CacheCreationCost, 1e-12)
|
||||
require.InDelta(t, 10*30e-6, cost.OutputCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestDefaultPricingIncludesOfficialGPT56Rates(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
pricingSvc := &PricingService{}
|
||||
pricingData, err := pricingSvc.parsePricingData(data)
|
||||
require.NoError(t, err)
|
||||
pricingSvc.pricingData = pricingData
|
||||
billingSvc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
|
||||
tests := []struct {
|
||||
model string
|
||||
input, cached, cacheWrite, output float64
|
||||
inputPriority, cachedPriority, cacheWritePriority, outputPriority float64
|
||||
}{
|
||||
{model: "gpt-5.6-sol", input: 5e-6, cached: 0.5e-6, cacheWrite: 6.25e-6, output: 30e-6, inputPriority: 10e-6, cachedPriority: 1e-6, cacheWritePriority: 12.5e-6, outputPriority: 60e-6},
|
||||
{model: "gpt-5.6-terra", input: 2.5e-6, cached: 0.25e-6, cacheWrite: 3.125e-6, output: 15e-6, inputPriority: 5e-6, cachedPriority: 0.5e-6, cacheWritePriority: 6.25e-6, outputPriority: 30e-6},
|
||||
{model: "gpt-5.6-luna", input: 1e-6, cached: 0.1e-6, cacheWrite: 1.25e-6, output: 6e-6, inputPriority: 2e-6, cachedPriority: 0.2e-6, cacheWritePriority: 2.5e-6, outputPriority: 12e-6},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
pricing, err := billingSvc.GetModelPricing(tt.model)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, tt.input, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.cached, pricing.CacheReadPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.cacheWrite, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.output, pricing.OutputPricePerToken, 1e-12)
|
||||
require.InDelta(t, tt.inputPriority, pricing.InputPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, tt.cachedPriority, pricing.CacheReadPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, tt.cacheWritePriority, pricing.CacheCreationPricePerTokenPriority, 1e-12)
|
||||
require.InDelta(t, tt.outputPriority, pricing.OutputPricePerTokenPriority, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPT56DedicatedFallbacksUseOfficialRates(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
input, cached, cacheWrite, output float64
|
||||
}{
|
||||
{model: "gpt-5.6-sol", input: 5e-6, cached: 0.5e-6, cacheWrite: 6.25e-6, output: 30e-6},
|
||||
{model: "gpt-5.6-terra", input: 2.5e-6, cached: 0.25e-6, cacheWrite: 3.125e-6, output: 15e-6},
|
||||
{model: "gpt-5.6-luna", input: 1e-6, cached: 0.1e-6, cacheWrite: 1.25e-6, output: 6e-6},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model+"/pricing_service", func(t *testing.T) {
|
||||
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
|
||||
"gpt-5.1-codex": {InputCostPerToken: 1.25e-6},
|
||||
}}
|
||||
svc := NewBillingService(&config.Config{}, pricingSvc)
|
||||
pricing, err := svc.GetModelPricing(tt.model + "-preview")
|
||||
require.NoError(t, err)
|
||||
assertGPT56FallbackPricing(t, pricing, tt.input, tt.cached, tt.cacheWrite, tt.output)
|
||||
})
|
||||
|
||||
t.Run(tt.model+"/billing_service", func(t *testing.T) {
|
||||
svc := NewBillingService(&config.Config{}, nil)
|
||||
pricing, err := svc.GetModelPricing(tt.model)
|
||||
require.NoError(t, err)
|
||||
assertGPT56FallbackPricing(t, pricing, tt.input, tt.cached, tt.cacheWrite, tt.output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertGPT56FallbackPricing(t *testing.T, pricing *ModelPricing, input, cached, cacheWrite, output float64) {
|
||||
t.Helper()
|
||||
require.InDelta(t, input, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, cached, pricing.CacheReadPricePerToken, 1e-12)
|
||||
require.InDelta(t, cacheWrite, pricing.CacheCreationPricePerToken, 1e-12)
|
||||
require.InDelta(t, output, pricing.OutputPricePerToken, 1e-12)
|
||||
require.Zero(t, pricing.LongContextInputThreshold)
|
||||
}
|
||||
|
||||
func TestParsePricingData_KeepsImageOnlyPricing(t *testing.T) {
|
||||
svc := &PricingService{}
|
||||
body := []byte(`{
|
||||
|
||||
@@ -4961,12 +4961,14 @@
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
"gpt-5.6-sol": {
|
||||
"cache_creation_input_token_cost": 6.25e-06,
|
||||
"cache_creation_input_token_cost_batches": 3.125e-06,
|
||||
"cache_creation_input_token_cost_flex": 3.125e-06,
|
||||
"cache_creation_input_token_cost_priority": 1.25e-05,
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 1e-06,
|
||||
"cache_read_input_token_cost_flex": 2.5e-07,
|
||||
"cache_read_input_token_cost_priority": 1e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 1e-05,
|
||||
"input_cost_per_token_batches": 2.5e-06,
|
||||
"input_cost_per_token_flex": 2.5e-06,
|
||||
"input_cost_per_token_priority": 1e-05,
|
||||
@@ -4976,7 +4978,6 @@
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 4.5e-05,
|
||||
"output_cost_per_token_batches": 1.5e-05,
|
||||
"output_cost_per_token_flex": 1.5e-05,
|
||||
"output_cost_per_token_priority": 6e-05,
|
||||
@@ -5009,25 +5010,26 @@
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
"gpt-5.6-terra": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 1e-06,
|
||||
"cache_read_input_token_cost_flex": 2.5e-07,
|
||||
"cache_read_input_token_cost_priority": 1e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 1e-05,
|
||||
"input_cost_per_token_batches": 2.5e-06,
|
||||
"input_cost_per_token_flex": 2.5e-06,
|
||||
"input_cost_per_token_priority": 1e-05,
|
||||
"cache_creation_input_token_cost": 3.125e-06,
|
||||
"cache_creation_input_token_cost_batches": 1.5625e-06,
|
||||
"cache_creation_input_token_cost_flex": 1.5625e-06,
|
||||
"cache_creation_input_token_cost_priority": 6.25e-06,
|
||||
"cache_read_input_token_cost": 2.5e-07,
|
||||
"cache_read_input_token_cost_flex": 1.25e-07,
|
||||
"cache_read_input_token_cost_priority": 5e-07,
|
||||
"input_cost_per_token": 2.5e-06,
|
||||
"input_cost_per_token_batches": 1.25e-06,
|
||||
"input_cost_per_token_flex": 1.25e-06,
|
||||
"input_cost_per_token_priority": 5e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 4.5e-05,
|
||||
"output_cost_per_token_batches": 1.5e-05,
|
||||
"output_cost_per_token_flex": 1.5e-05,
|
||||
"output_cost_per_token_priority": 6e-05,
|
||||
"output_cost_per_token": 1.5e-05,
|
||||
"output_cost_per_token_batches": 7.5e-06,
|
||||
"output_cost_per_token_flex": 7.5e-06,
|
||||
"output_cost_per_token_priority": 3e-05,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/batch",
|
||||
@@ -5057,25 +5059,26 @@
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
"gpt-5.6-luna": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 1e-06,
|
||||
"cache_read_input_token_cost_flex": 2.5e-07,
|
||||
"cache_read_input_token_cost_priority": 1e-06,
|
||||
"input_cost_per_token": 5e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 1e-05,
|
||||
"input_cost_per_token_batches": 2.5e-06,
|
||||
"input_cost_per_token_flex": 2.5e-06,
|
||||
"input_cost_per_token_priority": 1e-05,
|
||||
"cache_creation_input_token_cost": 1.25e-06,
|
||||
"cache_creation_input_token_cost_batches": 6.25e-07,
|
||||
"cache_creation_input_token_cost_flex": 6.25e-07,
|
||||
"cache_creation_input_token_cost_priority": 2.5e-06,
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
"cache_read_input_token_cost_flex": 5e-08,
|
||||
"cache_read_input_token_cost_priority": 2e-07,
|
||||
"input_cost_per_token": 1e-06,
|
||||
"input_cost_per_token_batches": 5e-07,
|
||||
"input_cost_per_token_flex": 5e-07,
|
||||
"input_cost_per_token_priority": 2e-06,
|
||||
"litellm_provider": "openai",
|
||||
"max_input_tokens": 1050000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 4.5e-05,
|
||||
"output_cost_per_token_batches": 1.5e-05,
|
||||
"output_cost_per_token_flex": 1.5e-05,
|
||||
"output_cost_per_token_priority": 6e-05,
|
||||
"output_cost_per_token": 6e-06,
|
||||
"output_cost_per_token_batches": 3e-06,
|
||||
"output_cost_per_token_flex": 3e-06,
|
||||
"output_cost_per_token_priority": 1.2e-05,
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/batch",
|
||||
|
||||
Reference in New Issue
Block a user