diff --git a/core/relay/adaptor/openai/chat_test.go b/core/relay/adaptor/openai/chat_test.go index 3205cb09..23cadd81 100644 --- a/core/relay/adaptor/openai/chat_test.go +++ b/core/relay/adaptor/openai/chat_test.go @@ -201,6 +201,26 @@ func TestIsResponsesOnlyModel(t *testing.T) { model: "gpt-3.5-turbo", expected: false, }, + { + name: "gpt-5.6 should support chat completions", + model: "gpt-5.6", + expected: false, + }, + { + name: "gpt-5.6 sol should support chat completions", + model: "gpt-5.6-sol", + expected: false, + }, + { + name: "gpt-5.6 terra should support chat completions", + model: "gpt-5.6-terra", + expected: false, + }, + { + name: "gpt-5.6 luna should support chat completions", + model: "gpt-5.6-luna", + expected: false, + }, { name: "empty model should not be responses only", model: "", diff --git a/core/relay/adaptor/openai/constants.go b/core/relay/adaptor/openai/constants.go index da56ca93..f89d028b 100644 --- a/core/relay/adaptor/openai/constants.go +++ b/core/relay/adaptor/openai/constants.go @@ -8,6 +8,262 @@ import ( ) var ModelList = []model.ModelConfig{ + { + Model: "gpt-5.6", + Type: mode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + Price: model.Price{ + InputPrice: 0.010, + OutputPrice: 0.045, + CachedPrice: 0.001, + CacheCreationPrice: 0.0125, + ConditionalPrices: []model.ConditionalPrice{ + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.010, + OutputPrice: 0.060, + CachedPrice: 0.001, + CacheCreationPrice: 0.0125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.020, + OutputPrice: 0.090, + CachedPrice: 0.002, + CacheCreationPrice: 0.025, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.0025, + OutputPrice: 0.015, + CachedPrice: 0.00025, + CacheCreationPrice: 0.003125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.0225, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000}, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.030, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + }, + }, + }, + }, + SummaryServiceTier: true, + Config: model.NewModelConfig( + model.WithModelConfigMaxContextTokens(1050000), + model.WithModelConfigMaxOutputTokens(128000), + model.WithModelConfigToolChoice(true), + ), + }, + { + Model: "gpt-5.6-sol", + Type: mode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + Price: model.Price{ + InputPrice: 0.010, + OutputPrice: 0.045, + CachedPrice: 0.001, + CacheCreationPrice: 0.0125, + ConditionalPrices: []model.ConditionalPrice{ + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.010, + OutputPrice: 0.060, + CachedPrice: 0.001, + CacheCreationPrice: 0.0125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.020, + OutputPrice: 0.090, + CachedPrice: 0.002, + CacheCreationPrice: 0.025, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.0025, + OutputPrice: 0.015, + CachedPrice: 0.00025, + CacheCreationPrice: 0.003125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.0225, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000}, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.030, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + }, + }, + }, + }, + SummaryServiceTier: true, + Config: model.NewModelConfig( + model.WithModelConfigMaxContextTokens(1050000), + model.WithModelConfigMaxOutputTokens(128000), + model.WithModelConfigToolChoice(true), + ), + }, + { + Model: "gpt-5.6-terra", + Type: mode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.0225, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + ConditionalPrices: []model.ConditionalPrice{ + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.005, + OutputPrice: 0.030, + CachedPrice: 0.0005, + CacheCreationPrice: 0.00625, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.010, + OutputPrice: 0.045, + CachedPrice: 0.001, + CacheCreationPrice: 0.0125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.00125, + OutputPrice: 0.0075, + CachedPrice: 0.000125, + CacheCreationPrice: 0.0015625, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.0025, + OutputPrice: 0.01125, + CachedPrice: 0.00025, + CacheCreationPrice: 0.003125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000}, + Price: model.Price{ + InputPrice: 0.0025, + OutputPrice: 0.015, + CachedPrice: 0.00025, + CacheCreationPrice: 0.003125, + }, + }, + }, + }, + SummaryServiceTier: true, + Config: model.NewModelConfig( + model.WithModelConfigMaxContextTokens(1050000), + model.WithModelConfigMaxOutputTokens(128000), + model.WithModelConfigToolChoice(true), + ), + }, + { + Model: "gpt-5.6-luna", + Type: mode.ChatCompletions, + Owner: model.ModelOwnerOpenAI, + Price: model.Price{ + InputPrice: 0.002, + OutputPrice: 0.009, + CachedPrice: 0.0002, + CacheCreationPrice: 0.0025, + ConditionalPrices: []model.ConditionalPrice{ + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.002, + OutputPrice: 0.012, + CachedPrice: 0.0002, + CacheCreationPrice: 0.0025, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "priority"}, + Price: model.Price{ + InputPrice: 0.004, + OutputPrice: 0.018, + CachedPrice: 0.0004, + CacheCreationPrice: 0.005, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.0005, + OutputPrice: 0.003, + CachedPrice: 0.00005, + CacheCreationPrice: 0.000625, + }, + }, + { + Condition: model.PriceCondition{InputTokenMin: 272001, ServiceTier: "flex"}, + Price: model.Price{ + InputPrice: 0.001, + OutputPrice: 0.0045, + CachedPrice: 0.0001, + CacheCreationPrice: 0.00125, + }, + }, + { + Condition: model.PriceCondition{InputTokenMax: 272000}, + Price: model.Price{ + InputPrice: 0.001, + OutputPrice: 0.006, + CachedPrice: 0.0001, + CacheCreationPrice: 0.00125, + }, + }, + }, + }, + SummaryServiceTier: true, + Config: model.NewModelConfig( + model.WithModelConfigMaxContextTokens(1050000), + model.WithModelConfigMaxOutputTokens(128000), + model.WithModelConfigToolChoice(true), + ), + }, { Model: "o3", Type: mode.Responses, diff --git a/core/relay/adaptor/openai/constants_test.go b/core/relay/adaptor/openai/constants_test.go new file mode 100644 index 00000000..c1118da7 --- /dev/null +++ b/core/relay/adaptor/openai/constants_test.go @@ -0,0 +1,123 @@ +package openai_test + +import ( + "net/http" + "testing" + + "github.com/labring/aiproxy/core/common/consume" + coremodel "github.com/labring/aiproxy/core/model" + "github.com/labring/aiproxy/core/relay/adaptor/openai" + "github.com/labring/aiproxy/core/relay/mode" + "github.com/stretchr/testify/require" +) + +func TestGPT56ModelConfigs(t *testing.T) { + tests := []struct { + model string + inputPrice float64 + cachedPrice float64 + cacheCreationPrice float64 + outputPrice float64 + }{ + {"gpt-5.6", 0.005, 0.0005, 0.00625, 0.030}, + {"gpt-5.6-sol", 0.005, 0.0005, 0.00625, 0.030}, + {"gpt-5.6-terra", 0.0025, 0.00025, 0.003125, 0.015}, + {"gpt-5.6-luna", 0.001, 0.0001, 0.00125, 0.006}, + } + + for _, tt := range tests { + t.Run(tt.model, func(t *testing.T) { + config := findOpenAIModelConfig(t, tt.model) + require.Equal(t, mode.ChatCompletions, config.Type) + require.False(t, openai.IsResponsesOnlyModel(&config, tt.model)) + require.NoError(t, config.Price.ValidateConditionalPrices()) + + price := config.Price.SelectConditionalPriceWithOptions( + coremodel.Usage{InputTokens: 1000}, + coremodel.UsageContext{}, + coremodel.PriceSelectionOptions{}, + ) + require.InDelta(t, tt.inputPrice, float64(price.InputPrice), 1e-12) + require.InDelta(t, tt.cachedPrice, float64(price.CachedPrice), 1e-12) + require.InDelta( + t, + tt.cacheCreationPrice, + float64(price.CacheCreationPrice), + 1e-12, + ) + require.InDelta(t, tt.outputPrice, float64(price.OutputPrice), 1e-12) + }) + } +} + +func TestGPT56SolConditionalPricing(t *testing.T) { + config := findOpenAIModelConfig(t, "gpt-5.6-sol") + tests := []struct { + name string + inputTokens int64 + serviceTier string + inputPrice float64 + cacheCreationPrice float64 + outputPrice float64 + }{ + {"standard", 272000, "", 0.005, 0.00625, 0.030}, + {"standard long context", 272001, "", 0.010, 0.0125, 0.045}, + {"flex", 272000, "flex", 0.0025, 0.003125, 0.015}, + {"flex long context", 272001, "flex", 0.005, 0.00625, 0.0225}, + {"priority", 272000, "priority", 0.010, 0.0125, 0.060}, + {"priority long context", 272001, "priority", 0.020, 0.025, 0.090}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + price := config.Price.SelectConditionalPriceWithOptions( + coremodel.Usage{InputTokens: coremodel.ZeroNullInt64(tt.inputTokens)}, + coremodel.UsageContext{ServiceTier: tt.serviceTier}, + coremodel.PriceSelectionOptions{}, + ) + require.InDelta(t, tt.inputPrice, float64(price.InputPrice), 1e-12) + require.InDelta( + t, + tt.cacheCreationPrice, + float64(price.CacheCreationPrice), + 1e-12, + ) + require.InDelta(t, tt.outputPrice, float64(price.OutputPrice), 1e-12) + }) + } +} + +func TestGPT56CacheWriteBilling(t *testing.T) { + config := findOpenAIModelConfig(t, "gpt-5.6-sol") + amount := consume.CalculateAmountDetail( + http.StatusOK, + coremodel.Usage{ + InputTokens: 1000, + OutputTokens: 100, + CachedTokens: 300, + CacheCreationTokens: 200, + }, + coremodel.UsageContext{}, + config.Price, + ) + + require.InDelta(t, 0.0025, amount.InputAmount, 1e-12) + require.InDelta(t, 0.00015, amount.CachedAmount, 1e-12) + require.InDelta(t, 0.00125, amount.CacheCreationAmount, 1e-12) + require.InDelta(t, 0.003, amount.OutputAmount, 1e-12) + require.InDelta(t, 0.0069, amount.UsedAmount, 1e-12) +} + +func findOpenAIModelConfig(t *testing.T, modelName string) coremodel.ModelConfig { + t.Helper() + + for _, config := range openai.ModelList { + if config.Model == modelName { + return config + } + } + + t.Fatalf("OpenAI model config %q not found", modelName) + + return coremodel.ModelConfig{} +} diff --git a/core/relay/model/chat.go b/core/relay/model/chat.go index 3a96c76a..868c6952 100644 --- a/core/relay/model/chat.go +++ b/core/relay/model/chat.go @@ -28,7 +28,9 @@ func (u ChatUsage) ToModelUsage() model.Usage { usage.AudioInputTokens = model.ZeroNullInt64(u.PromptTokensDetails.AudioTokens) usage.VideoInputTokens = model.ZeroNullInt64(u.PromptTokensDetails.VideoTokens) usage.CachedTokens = model.ZeroNullInt64(u.PromptTokensDetails.CachedTokens) - usage.CacheCreationTokens = model.ZeroNullInt64(u.PromptTokensDetails.CacheCreationTokens) + usage.CacheCreationTokens = model.ZeroNullInt64( + u.PromptTokensDetails.CacheCreationTokenCount(), + ) } if u.CompletionTokensDetails != nil { @@ -73,7 +75,7 @@ func (u ChatUsage) ToClaudeUsage() ClaudeUsage { } if u.PromptTokensDetails != nil { - cu.CacheCreationInputTokens = u.PromptTokensDetails.CacheCreationTokens + cu.CacheCreationInputTokens = u.PromptTokensDetails.CacheCreationTokenCount() cu.CacheReadInputTokens = u.PromptTokensDetails.CachedTokens } @@ -90,15 +92,16 @@ func (u ChatUsage) ToResponseUsage() ResponseUsage { if u.PromptTokensDetails != nil && (u.PromptTokensDetails.CachedTokens > 0 || - u.PromptTokensDetails.CacheCreationTokens > 0 || + u.PromptTokensDetails.CacheCreationTokenCount() > 0 || u.PromptTokensDetails.ImageTokens > 0 || u.PromptTokensDetails.VideoTokens > 0 || u.PromptTokensDetails.AudioTokens > 0) { usage.InputTokensDetails = &ResponseUsageDetails{ - AudioTokens: u.PromptTokensDetails.AudioTokens, - CachedTokens: u.PromptTokensDetails.CachedTokens, - ImageTokens: u.PromptTokensDetails.ImageTokens, - VideoTokens: u.PromptTokensDetails.VideoTokens, + AudioTokens: u.PromptTokensDetails.AudioTokens, + CachedTokens: u.PromptTokensDetails.CachedTokens, + CacheWriteTokens: u.PromptTokensDetails.CacheCreationTokenCount(), + ImageTokens: u.PromptTokensDetails.ImageTokens, + VideoTokens: u.PromptTokensDetails.VideoTokens, } } @@ -141,6 +144,15 @@ type PromptTokensDetails struct { ImageTokens int64 `json:"image_tokens,omitempty"` VideoTokens int64 `json:"video_tokens,omitempty"` CacheCreationTokens int64 `json:"cache_creation_tokens,omitempty"` + CacheWriteTokens int64 `json:"cache_write_tokens,omitempty"` +} + +func (d *PromptTokensDetails) CacheCreationTokenCount() int64 { + if d == nil { + return 0 + } + + return max(d.CacheCreationTokens, d.CacheWriteTokens) } func (d *PromptTokensDetails) Add(other *PromptTokensDetails) { @@ -153,6 +165,7 @@ func (d *PromptTokensDetails) Add(other *PromptTokensDetails) { d.ImageTokens += other.ImageTokens d.VideoTokens += other.VideoTokens d.CacheCreationTokens += other.CacheCreationTokens + d.CacheWriteTokens += other.CacheWriteTokens } type CompletionTokensDetails struct { diff --git a/core/relay/model/chat_test.go b/core/relay/model/chat_test.go index 540ba9f6..8c910c3c 100644 --- a/core/relay/model/chat_test.go +++ b/core/relay/model/chat_test.go @@ -1,6 +1,7 @@ package model_test import ( + "encoding/json" "errors" "net/http" "testing" @@ -84,6 +85,28 @@ func TestChatUsage(t *testing.T) { }) } +func TestChatUsageCacheWriteTokens(t *testing.T) { + convey.Convey("cache_write_tokens maps to cache creation usage", t, func() { + var usage model.ChatUsage + + err := json.Unmarshal([]byte(`{ + "prompt_tokens": 2006, + "completion_tokens": 300, + "total_tokens": 2306, + "prompt_tokens_details": { + "cached_tokens": 1920, + "cache_creation_tokens": 32, + "cache_write_tokens": 64 + } + }`), &usage) + convey.So(err, convey.ShouldBeNil) + + modelUsage := usage.ToModelUsage() + convey.So(int64(modelUsage.CachedTokens), convey.ShouldEqual, 1920) + convey.So(int64(modelUsage.CacheCreationTokens), convey.ShouldEqual, 64) + }) +} + func TestOpenAIError(t *testing.T) { convey.Convey("OpenAIError", t, func() { convey.Convey("NewOpenAIError", func() { diff --git a/core/relay/model/response.go b/core/relay/model/response.go index 0820d801..65d10f04 100644 --- a/core/relay/model/response.go +++ b/core/relay/model/response.go @@ -255,11 +255,12 @@ type InputItem struct { // ResponseUsageDetails represents detailed token usage information type ResponseUsageDetails struct { - AudioTokens int64 `json:"audio_tokens,omitempty"` - CachedTokens int64 `json:"cached_tokens,omitempty"` - ReasoningTokens int64 `json:"reasoning_tokens,omitempty"` - ImageTokens int64 `json:"image_tokens,omitempty"` - VideoTokens int64 `json:"video_tokens,omitempty"` + AudioTokens int64 `json:"audio_tokens,omitempty"` + CachedTokens int64 `json:"cached_tokens,omitempty"` + CacheWriteTokens int64 `json:"cache_write_tokens,omitempty"` + ReasoningTokens int64 `json:"reasoning_tokens,omitempty"` + ImageTokens int64 `json:"image_tokens,omitempty"` + VideoTokens int64 `json:"video_tokens,omitempty"` } // ResponseUsage represents usage information for a response @@ -435,6 +436,7 @@ func (u *ResponseUsage) ToModelUsage() model.Usage { usage.AudioInputTokens = model.ZeroNullInt64(u.InputTokensDetails.AudioTokens) usage.VideoInputTokens = model.ZeroNullInt64(u.InputTokensDetails.VideoTokens) usage.CachedTokens = model.ZeroNullInt64(u.InputTokensDetails.CachedTokens) + usage.CacheCreationTokens = model.ZeroNullInt64(u.InputTokensDetails.CacheWriteTokens) } if u.OutputTokensDetails != nil { @@ -456,14 +458,16 @@ func (u *ResponseUsage) ToChatUsage() ChatUsage { if u.InputTokensDetails != nil && (u.InputTokensDetails.CachedTokens > 0 || + u.InputTokensDetails.CacheWriteTokens > 0 || u.InputTokensDetails.AudioTokens > 0 || u.InputTokensDetails.ImageTokens > 0 || u.InputTokensDetails.VideoTokens > 0) { usage.PromptTokensDetails = &PromptTokensDetails{ - AudioTokens: u.InputTokensDetails.AudioTokens, - CachedTokens: u.InputTokensDetails.CachedTokens, - ImageTokens: u.InputTokensDetails.ImageTokens, - VideoTokens: u.InputTokensDetails.VideoTokens, + AudioTokens: u.InputTokensDetails.AudioTokens, + CachedTokens: u.InputTokensDetails.CachedTokens, + CacheWriteTokens: u.InputTokensDetails.CacheWriteTokens, + ImageTokens: u.InputTokensDetails.ImageTokens, + VideoTokens: u.InputTokensDetails.VideoTokens, } } @@ -492,6 +496,10 @@ func (u *ResponseUsage) ToClaudeUsage() ClaudeUsage { usage.CacheReadInputTokens = u.InputTokensDetails.CachedTokens } + if u.InputTokensDetails != nil && u.InputTokensDetails.CacheWriteTokens > 0 { + usage.CacheCreationInputTokens = u.InputTokensDetails.CacheWriteTokens + } + return usage } diff --git a/core/relay/model/usage_conversion_test.go b/core/relay/model/usage_conversion_test.go index 52c02364..bbb3f8d8 100644 --- a/core/relay/model/usage_conversion_test.go +++ b/core/relay/model/usage_conversion_test.go @@ -1,6 +1,7 @@ package model_test import ( + "encoding/json" "testing" coremodel "github.com/labring/aiproxy/core/model" @@ -8,6 +9,22 @@ import ( "github.com/stretchr/testify/assert" ) +func TestResponseUsageCacheWriteTokensJSON(t *testing.T) { + var usage model.ResponseUsage + + err := json.Unmarshal([]byte(`{ + "input_tokens": 2006, + "output_tokens": 300, + "total_tokens": 2306, + "input_tokens_details": { + "cached_tokens": 1920, + "cache_write_tokens": 64 + } + }`), &usage) + assert.NoError(t, err) + assert.Equal(t, coremodel.ZeroNullInt64(64), usage.ToModelUsage().CacheCreationTokens) +} + func TestChatUsageConversions(t *testing.T) { chatUsage := model.ChatUsage{ PromptTokens: 100, @@ -37,6 +54,7 @@ func TestChatUsageConversions(t *testing.T) { assert.Equal(t, int64(6), responseUsage.InputTokensDetails.ImageTokens) assert.Equal(t, int64(9), responseUsage.InputTokensDetails.VideoTokens) assert.Equal(t, int64(20), responseUsage.InputTokensDetails.CachedTokens) + assert.Equal(t, int64(10), responseUsage.InputTokensDetails.CacheWriteTokens) assert.NotNil(t, responseUsage.OutputTokensDetails) assert.Equal(t, int64(7), responseUsage.OutputTokensDetails.AudioTokens) assert.Equal(t, int64(8), responseUsage.OutputTokensDetails.ImageTokens) @@ -78,10 +96,11 @@ func TestResponseUsageConversions(t *testing.T) { OutputTokens: 50, TotalTokens: 150, InputTokensDetails: &model.ResponseUsageDetails{ - AudioTokens: 5, - ImageTokens: 6, - VideoTokens: 9, - CachedTokens: 20, + AudioTokens: 5, + ImageTokens: 6, + VideoTokens: 9, + CachedTokens: 20, + CacheWriteTokens: 10, }, OutputTokensDetails: &model.ResponseUsageDetails{ AudioTokens: 7, @@ -100,6 +119,7 @@ func TestResponseUsageConversions(t *testing.T) { assert.Equal(t, int64(6), chatUsage.PromptTokensDetails.ImageTokens) assert.Equal(t, int64(9), chatUsage.PromptTokensDetails.VideoTokens) assert.Equal(t, int64(20), chatUsage.PromptTokensDetails.CachedTokens) + assert.Equal(t, int64(10), chatUsage.PromptTokensDetails.CacheWriteTokens) assert.NotNil(t, chatUsage.CompletionTokensDetails) assert.Equal(t, int64(7), chatUsage.CompletionTokensDetails.AudioTokens) assert.Equal(t, int64(8), chatUsage.CompletionTokensDetails.ImageTokens) @@ -115,6 +135,7 @@ func TestResponseUsageConversions(t *testing.T) { assert.Equal(t, coremodel.ZeroNullInt64(50), modelUsage.OutputTokens) assert.Equal(t, coremodel.ZeroNullInt64(8), modelUsage.ImageOutputTokens) assert.Equal(t, coremodel.ZeroNullInt64(7), modelUsage.AudioOutputTokens) + assert.Equal(t, coremodel.ZeroNullInt64(10), modelUsage.CacheCreationTokens) }) t.Run("ResponseUsage to ClaudeUsage", func(t *testing.T) { @@ -122,6 +143,7 @@ func TestResponseUsageConversions(t *testing.T) { assert.Equal(t, int64(100), claudeUsage.InputTokens) assert.Equal(t, int64(50), claudeUsage.OutputTokens) assert.Equal(t, int64(20), claudeUsage.CacheReadInputTokens) + assert.Equal(t, int64(10), claudeUsage.CacheCreationInputTokens) }) t.Run("ResponseUsage to GeminiUsage", func(t *testing.T) {