Merge pull request #118 from octo-patch/octo/20260729-model-add-recvqd13uNrAsi
Add MiniMax-M3 to direct MiniMax global and China catalogs
4 files · +103 -19
M packages/provider/anthropic.go +13 -10
| @@ -195,7 +195,7 @@ type anthRequest struct { | |||
| 195 | 195 | // the same family when reached through an Anthropic-Messages- | |
| 196 | 196 | // compatible proxy whose catalog row predates the flag. | |
| 197 | 197 | func usesAdaptiveThinking(m Model) bool { | |
| 198 | - | if m.AdaptiveThinking { | |
| 198 | + | if m.AdaptiveThinking || m.AdaptiveThinkingCompat { | |
| 199 | 199 | return true | |
| 200 | 200 | } | |
| 201 | 201 | id := strings.ToLower(m.ID) | |
| @@ -231,16 +231,17 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 231 | 231 | } | |
| 232 | 232 | ||
| 233 | 233 | adaptive := usesAdaptiveThinking(m) | |
| 234 | + | adaptiveEffort := adaptive && !m.AdaptiveThinkingCompat | |
| 234 | 235 | ||
| 235 | 236 | out := &anthRequest{ | |
| 236 | 237 | Model: req.Model, | |
| 237 | 238 | MaxTokens: maxTok, | |
| 238 | 239 | Stream: true, | |
| 239 | 240 | } | |
| 240 | - | // Adaptive-thinking models reject non-default sampling params | |
| 241 | - | // (temperature/top_p/top_k -> 400). Only forward temperature for | |
| 242 | - | // models that accept it. | |
| 243 | - | if !adaptive { | |
| 241 | + | // Anthropic's adaptive-thinking models reject non-default sampling | |
| 242 | + | // params. Compatible implementations may use the adaptive thinking type | |
| 243 | + | // while continuing to support temperature. | |
| 244 | + | if !adaptiveEffort { | |
| 244 | 245 | out.Temperature = req.Temperature | |
| 245 | 246 | } | |
| 246 | 247 | ||
| @@ -285,12 +286,14 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 285 | 286 | ||
| 286 | 287 | if req.Reasoning != "" && m.Reasoning { | |
| 287 | 288 | if adaptive { | |
| 288 | - | // Adaptive-thinking models (Opus 4.7+): the model decides when | |
| 289 | - | // and how much to think; depth is steered by output_config.effort. | |
| 290 | - | // Explicit budgets are rejected with a 400, so none is sent. | |
| 289 | + | // Adaptive-thinking models decide when and how much to think and | |
| 290 | + | // reject explicit budgets. Anthropic models also accept the | |
| 291 | + | // output_config.effort extension; compatible implementations may not. | |
| 291 | 292 | out.Thinking = &anthThinking{Type: "adaptive"} | |
| 292 | - | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { | |
| 293 | - | out.OutputConfig = &anthOutputConfig{Effort: effort} | |
| 293 | + | if adaptiveEffort { | |
| 294 | + | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { | |
| 295 | + | out.OutputConfig = &anthOutputConfig{Effort: effort} | |
| 296 | + | } | |
| 294 | 297 | } | |
| 295 | 298 | } else { | |
| 296 | 299 | budget := anthropicReasoningBudget(req.Reasoning) | |
| @@ -195,7 +195,7 @@ type anthRequest struct { | |||||
| 195 | // the same family when reached through an Anthropic-Messages- | 195 | // the same family when reached through an Anthropic-Messages- | ||
| 196 | // compatible proxy whose catalog row predates the flag. | 196 | // compatible proxy whose catalog row predates the flag. | ||
| 197 | func usesAdaptiveThinking(m Model) bool { | 197 | func usesAdaptiveThinking(m Model) bool { | ||
| 198 | - | if m.AdaptiveThinking { | 198 | + | if m.AdaptiveThinking || m.AdaptiveThinkingCompat { |
| 199 | return true | 199 | return true | ||
| 200 | } | 200 | } | ||
| 201 | id := strings.ToLower(m.ID) | 201 | id := strings.ToLower(m.ID) | ||
| @@ -231,16 +231,17 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 231 | } | 231 | } | ||
| 232 | 232 | ||||
| 233 | adaptive := usesAdaptiveThinking(m) | 233 | adaptive := usesAdaptiveThinking(m) | ||
| 234 | + | adaptiveEffort := adaptive && !m.AdaptiveThinkingCompat | |||
| 234 | 235 | ||||
| 235 | out := &anthRequest{ | 236 | out := &anthRequest{ | ||
| 236 | Model: req.Model, | 237 | Model: req.Model, | ||
| 237 | MaxTokens: maxTok, | 238 | MaxTokens: maxTok, | ||
| 238 | Stream: true, | 239 | Stream: true, | ||
| 239 | } | 240 | } | ||
| 240 | - | // Adaptive-thinking models reject non-default sampling params | 241 | + | // Anthropic's adaptive-thinking models reject non-default sampling |
| 241 | - | // (temperature/top_p/top_k -> 400). Only forward temperature for | 242 | + | // params. Compatible implementations may use the adaptive thinking type |
| 242 | - | // models that accept it. | 243 | + | // while continuing to support temperature. |
| 243 | - | if !adaptive { | 244 | + | if !adaptiveEffort { |
| 244 | out.Temperature = req.Temperature | 245 | out.Temperature = req.Temperature | ||
| 245 | } | 246 | } | ||
| 246 | 247 | ||||
| @@ -285,12 +286,14 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 285 | 286 | ||||
| 286 | if req.Reasoning != "" && m.Reasoning { | 287 | if req.Reasoning != "" && m.Reasoning { | ||
| 287 | if adaptive { | 288 | if adaptive { | ||
| 288 | - | // Adaptive-thinking models (Opus 4.7+): the model decides when | 289 | + | // Adaptive-thinking models decide when and how much to think and |
| 289 | - | // and how much to think; depth is steered by output_config.effort. | 290 | + | // reject explicit budgets. Anthropic models also accept the |
| 290 | - | // Explicit budgets are rejected with a 400, so none is sent. | 291 | + | // output_config.effort extension; compatible implementations may not. |
| 291 | out.Thinking = &anthThinking{Type: "adaptive"} | 292 | out.Thinking = &anthThinking{Type: "adaptive"} | ||
| 292 | - | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { | 293 | + | if adaptiveEffort { |
| 293 | - | out.OutputConfig = &anthOutputConfig{Effort: effort} | 294 | + | if effort := AnthropicAdaptiveEffort(req.Reasoning); effort != "" { |
| 295 | + | out.OutputConfig = &anthOutputConfig{Effort: effort} | |||
| 296 | + | } | |||
| 294 | } | 297 | } | ||
| 295 | } else { | 298 | } else { | ||
| 296 | budget := anthropicReasoningBudget(req.Reasoning) | 299 | budget := anthropicReasoningBudget(req.Reasoning) | ||
M packages/provider/catalog_builtin.go +2 -0
| @@ -229,9 +229,11 @@ var builtinCatalog = []Model{ | |||
| 229 | 229 | // ----- minimax ----- | |
| 230 | 230 | {Provider: "minimax", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | |
| 231 | 231 | {Provider: "minimax", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | |
| 232 | + | {Provider: "minimax", ID: "MiniMax-M3", DisplayName: "MiniMax-M3", ContextWindow: 1000000, MaxOutput: 131072, Reasoning: true, AdaptiveThinkingCompat: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceTierInputTokens: 512000, PriceInputAbove: 0.6, PriceOutputAbove: 2.4, PriceCacheReadAbove: 0.12, BaseURL: "https://api.minimax.io/anthropic"}, | |
| 232 | 233 | // ----- minimax-cn ----- | |
| 233 | 234 | {Provider: "minimax-cn", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | |
| 234 | 235 | {Provider: "minimax-cn", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | |
| 236 | + | {Provider: "minimax-cn", ID: "MiniMax-M3", DisplayName: "MiniMax-M3", ContextWindow: 1000000, MaxOutput: 131072, Reasoning: true, AdaptiveThinkingCompat: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceTierInputTokens: 512000, PriceInputAbove: 0.6, PriceOutputAbove: 2.4, PriceCacheReadAbove: 0.12, BaseURL: "https://api.minimaxi.com/anthropic"}, | |
| 235 | 237 | // ----- mistral ----- | |
| 236 | 238 | {Provider: "mistral", ID: "codestral-latest", DisplayName: "Codestral (latest)", ContextWindow: 256000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.3, PriceOutput: 0.9, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | |
| 237 | 239 | {Provider: "mistral", ID: "devstral-2512", DisplayName: "Devstral 2", ContextWindow: 262144, MaxOutput: 262144, Reasoning: false, PriceInput: 0.4, PriceOutput: 2, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | |
| @@ -229,9 +229,11 @@ var builtinCatalog = []Model{ | |||||
| 229 | // ----- minimax ----- | 229 | // ----- minimax ----- | ||
| 230 | {Provider: "minimax", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | 230 | {Provider: "minimax", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | ||
| 231 | {Provider: "minimax", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | 231 | {Provider: "minimax", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimax.io/anthropic"}, | ||
| 232 | + | {Provider: "minimax", ID: "MiniMax-M3", DisplayName: "MiniMax-M3", ContextWindow: 1000000, MaxOutput: 131072, Reasoning: true, AdaptiveThinkingCompat: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceTierInputTokens: 512000, PriceInputAbove: 0.6, PriceOutputAbove: 2.4, PriceCacheReadAbove: 0.12, BaseURL: "https://api.minimax.io/anthropic"}, | |||
| 232 | // ----- minimax-cn ----- | 233 | // ----- minimax-cn ----- | ||
| 233 | {Provider: "minimax-cn", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | 234 | {Provider: "minimax-cn", ID: "MiniMax-M2.7", DisplayName: "MiniMax-M2.7", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | ||
| 234 | {Provider: "minimax-cn", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | 235 | {Provider: "minimax-cn", ID: "MiniMax-M2.7-highspeed", DisplayName: "MiniMax-M2.7-highspeed", ContextWindow: 204800, MaxOutput: 131072, Reasoning: true, PriceInput: 0.6, PriceOutput: 2.4, PriceCacheRead: 0.06, PriceCacheWrite: 0.375, BaseURL: "https://api.minimaxi.com/anthropic"}, | ||
| 236 | + | {Provider: "minimax-cn", ID: "MiniMax-M3", DisplayName: "MiniMax-M3", ContextWindow: 1000000, MaxOutput: 131072, Reasoning: true, AdaptiveThinkingCompat: true, PriceInput: 0.3, PriceOutput: 1.2, PriceCacheRead: 0.06, PriceTierInputTokens: 512000, PriceInputAbove: 0.6, PriceOutputAbove: 2.4, PriceCacheReadAbove: 0.12, BaseURL: "https://api.minimaxi.com/anthropic"}, | |||
| 235 | // ----- mistral ----- | 237 | // ----- mistral ----- | ||
| 236 | {Provider: "mistral", ID: "codestral-latest", DisplayName: "Codestral (latest)", ContextWindow: 256000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.3, PriceOutput: 0.9, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | 238 | {Provider: "mistral", ID: "codestral-latest", DisplayName: "Codestral (latest)", ContextWindow: 256000, MaxOutput: 4096, Reasoning: false, PriceInput: 0.3, PriceOutput: 0.9, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | ||
| 237 | {Provider: "mistral", ID: "devstral-2512", DisplayName: "Devstral 2", ContextWindow: 262144, MaxOutput: 262144, Reasoning: false, PriceInput: 0.4, PriceOutput: 2, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | 239 | {Provider: "mistral", ID: "devstral-2512", DisplayName: "Devstral 2", ContextWindow: 262144, MaxOutput: 262144, Reasoning: false, PriceInput: 0.4, PriceOutput: 2, PriceCacheRead: 0, BaseURL: "https://api.mistral.ai"}, | ||
M packages/provider/models.go +32 -9
| @@ -23,11 +23,22 @@ type Model struct { | |||
| 23 | 23 | // output_config.effort and omits temperature for these models. | |
| 24 | 24 | AdaptiveThinking bool | |
| 25 | 25 | ||
| 26 | - | // Prices are USD per 1M tokens. | |
| 27 | - | PriceInput float64 | |
| 28 | - | PriceOutput float64 | |
| 29 | - | PriceCacheRead float64 | |
| 30 | - | PriceCacheWrite float64 | |
| 26 | + | // AdaptiveThinkingCompat marks Anthropic-compatible models that require | |
| 27 | + | // thinking:{type:"adaptive"} but still accept sampling parameters and do | |
| 28 | + | // not support Anthropic's output_config.effort extension. | |
| 29 | + | AdaptiveThinkingCompat bool | |
| 30 | + | ||
| 31 | + | // Prices are USD per 1M tokens. Price*Above fields apply to all token | |
| 32 | + | // classes when the total prompt size exceeds PriceTierInputTokens. | |
| 33 | + | PriceInput float64 | |
| 34 | + | PriceOutput float64 | |
| 35 | + | PriceCacheRead float64 | |
| 36 | + | PriceCacheWrite float64 | |
| 37 | + | PriceTierInputTokens int | |
| 38 | + | PriceInputAbove float64 | |
| 39 | + | PriceOutputAbove float64 | |
| 40 | + | PriceCacheReadAbove float64 | |
| 41 | + | PriceCacheWriteAbove float64 | |
| 31 | 42 | ||
| 32 | 43 | // Speculative marks models whose ids are known from the upstream | |
| 33 | 44 | // vendor's CLI but not yet live on their public API. They'll 404 | |
| @@ -491,9 +502,21 @@ func ModelsForProvider(provider string) []Model { | |||
| 491 | 502 | ||
| 492 | 503 | // ComputeCost returns the USD cost for the given usage on model m. | |
| 493 | 504 | func ComputeCost(m Model, u Usage) float64 { | |
| 505 | + | inputPrice := m.PriceInput | |
| 506 | + | outputPrice := m.PriceOutput | |
| 507 | + | cacheReadPrice := m.PriceCacheRead | |
| 508 | + | cacheWritePrice := m.PriceCacheWrite | |
| 509 | + | promptTokens := u.InputTokens + u.CacheReadTokens + u.CacheWriteTokens | |
| 510 | + | if m.PriceTierInputTokens > 0 && promptTokens > m.PriceTierInputTokens { | |
| 511 | + | inputPrice = m.PriceInputAbove | |
| 512 | + | outputPrice = m.PriceOutputAbove | |
| 513 | + | cacheReadPrice = m.PriceCacheReadAbove | |
| 514 | + | cacheWritePrice = m.PriceCacheWriteAbove | |
| 515 | + | } | |
| 516 | + | ||
| 494 | 517 | const per = 1_000_000.0 | |
| 495 | - | return float64(u.InputTokens)*m.PriceInput/per + | |
| 496 | - | float64(u.OutputTokens)*m.PriceOutput/per + | |
| 497 | - | float64(u.CacheReadTokens)*m.PriceCacheRead/per + | |
| 498 | - | float64(u.CacheWriteTokens)*m.PriceCacheWrite/per | |
| 518 | + | return float64(u.InputTokens)*inputPrice/per + | |
| 519 | + | float64(u.OutputTokens)*outputPrice/per + | |
| 520 | + | float64(u.CacheReadTokens)*cacheReadPrice/per + | |
| 521 | + | float64(u.CacheWriteTokens)*cacheWritePrice/per | |
| 499 | 522 | } | |
| @@ -23,11 +23,22 @@ type Model struct { | |||||
| 23 | // output_config.effort and omits temperature for these models. | 23 | // output_config.effort and omits temperature for these models. | ||
| 24 | AdaptiveThinking bool | 24 | AdaptiveThinking bool | ||
| 25 | 25 | ||||
| 26 | - | // Prices are USD per 1M tokens. | 26 | + | // AdaptiveThinkingCompat marks Anthropic-compatible models that require |
| 27 | - | PriceInput float64 | 27 | + | // thinking:{type:"adaptive"} but still accept sampling parameters and do |
| 28 | - | PriceOutput float64 | 28 | + | // not support Anthropic's output_config.effort extension. |
| 29 | - | PriceCacheRead float64 | 29 | + | AdaptiveThinkingCompat bool |
| 30 | - | PriceCacheWrite float64 | 30 | + | |
| 31 | + | // Prices are USD per 1M tokens. Price*Above fields apply to all token | |||
| 32 | + | // classes when the total prompt size exceeds PriceTierInputTokens. | |||
| 33 | + | PriceInput float64 | |||
| 34 | + | PriceOutput float64 | |||
| 35 | + | PriceCacheRead float64 | |||
| 36 | + | PriceCacheWrite float64 | |||
| 37 | + | PriceTierInputTokens int | |||
| 38 | + | PriceInputAbove float64 | |||
| 39 | + | PriceOutputAbove float64 | |||
| 40 | + | PriceCacheReadAbove float64 | |||
| 41 | + | PriceCacheWriteAbove float64 | |||
| 31 | 42 | ||||
| 32 | // Speculative marks models whose ids are known from the upstream | 43 | // Speculative marks models whose ids are known from the upstream | ||
| 33 | // vendor's CLI but not yet live on their public API. They'll 404 | 44 | // vendor's CLI but not yet live on their public API. They'll 404 | ||
| @@ -491,9 +502,21 @@ func ModelsForProvider(provider string) []Model { | |||||
| 491 | 502 | ||||
| 492 | // ComputeCost returns the USD cost for the given usage on model m. | 503 | // ComputeCost returns the USD cost for the given usage on model m. | ||
| 493 | func ComputeCost(m Model, u Usage) float64 { | 504 | func ComputeCost(m Model, u Usage) float64 { | ||
| 505 | + | inputPrice := m.PriceInput | |||
| 506 | + | outputPrice := m.PriceOutput | |||
| 507 | + | cacheReadPrice := m.PriceCacheRead | |||
| 508 | + | cacheWritePrice := m.PriceCacheWrite | |||
| 509 | + | promptTokens := u.InputTokens + u.CacheReadTokens + u.CacheWriteTokens | |||
| 510 | + | if m.PriceTierInputTokens > 0 && promptTokens > m.PriceTierInputTokens { | |||
| 511 | + | inputPrice = m.PriceInputAbove | |||
| 512 | + | outputPrice = m.PriceOutputAbove | |||
| 513 | + | cacheReadPrice = m.PriceCacheReadAbove | |||
| 514 | + | cacheWritePrice = m.PriceCacheWriteAbove | |||
| 515 | + | } | |||
| 516 | + | ||||
| 494 | const per = 1_000_000.0 | 517 | const per = 1_000_000.0 | ||
| 495 | - | return float64(u.InputTokens)*m.PriceInput/per + | 518 | + | return float64(u.InputTokens)*inputPrice/per + |
| 496 | - | float64(u.OutputTokens)*m.PriceOutput/per + | 519 | + | float64(u.OutputTokens)*outputPrice/per + |
| 497 | - | float64(u.CacheReadTokens)*m.PriceCacheRead/per + | 520 | + | float64(u.CacheReadTokens)*cacheReadPrice/per + |
| 498 | - | float64(u.CacheWriteTokens)*m.PriceCacheWrite/per | 521 | + | float64(u.CacheWriteTokens)*cacheWritePrice/per |
| 499 | } | 522 | } | ||
M packages/provider/provider_test.go +56 -0
| @@ -2,6 +2,7 @@ package provider | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "math" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | "net/http/httptest" | |
| 7 | 8 | "strings" | |
| @@ -62,6 +63,25 @@ func TestComputeCost(t *testing.T) { | |||
| 62 | 63 | } | |
| 63 | 64 | } | |
| 64 | 65 | ||
| 66 | + | func TestComputeCostInputTier(t *testing.T) { | |
| 67 | + | m := Model{ | |
| 68 | + | PriceInput: 1, PriceOutput: 2, PriceCacheRead: 0.1, | |
| 69 | + | PriceTierInputTokens: 100, | |
| 70 | + | PriceInputAbove: 3, PriceOutputAbove: 4, PriceCacheReadAbove: 0.2, | |
| 71 | + | } | |
| 72 | + | below := ComputeCost(m, Usage{InputTokens: 50, OutputTokens: 10, CacheReadTokens: 50}) | |
| 73 | + | wantBelow := (50.0*1 + 10*2 + 50*0.1) / 1_000_000 | |
| 74 | + | if math.Abs(below-wantBelow) > 1e-12 { | |
| 75 | + | t.Fatalf("below-tier cost=%v want=%v", below, wantBelow) | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | above := ComputeCost(m, Usage{InputTokens: 51, OutputTokens: 10, CacheReadTokens: 50}) | |
| 79 | + | wantAbove := (51.0*3 + 10*4 + 50*0.2) / 1_000_000 | |
| 80 | + | if math.Abs(above-wantAbove) > 1e-12 { | |
| 81 | + | t.Fatalf("above-tier cost=%v want=%v", above, wantAbove) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 65 | 85 | func TestAnthropicErrorStatus(t *testing.T) { | |
| 66 | 86 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 67 | 87 | w.WriteHeader(http.StatusUnauthorized) | |
| @@ -90,6 +110,42 @@ func TestOpenAIErrorStatus(t *testing.T) { | |||
| 90 | 110 | } | |
| 91 | 111 | } | |
| 92 | 112 | ||
| 113 | + | func TestMiniMaxM3CatalogAndRequest(t *testing.T) { | |
| 114 | + | for _, provider := range []string{"minimax", "minimax-cn"} { | |
| 115 | + | m, err := FindModel(provider, "MiniMax-M3") | |
| 116 | + | if err != nil { | |
| 117 | + | t.Fatal(err) | |
| 118 | + | } | |
| 119 | + | if m.ContextWindow != 1000000 || m.MaxOutput != 131072 || !m.Reasoning || !m.AdaptiveThinkingCompat { | |
| 120 | + | t.Errorf("%s metadata = %+v", provider, m) | |
| 121 | + | } | |
| 122 | + | if m.PriceInput != 0.3 || m.PriceOutput != 1.2 || m.PriceCacheRead != 0.06 || m.PriceTierInputTokens != 512000 || m.PriceInputAbove != 0.6 || m.PriceOutputAbove != 2.4 || m.PriceCacheReadAbove != 0.12 { | |
| 123 | + | t.Errorf("%s pricing = %+v", provider, m) | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | c := NewAnthropicCompat("minimax", "x", "https://api.minimax.io/anthropic").(*anthropicClient) | |
| 128 | + | temp := float32(0.7) | |
| 129 | + | wire, err := c.buildRequest(Request{ | |
| 130 | + | Model: "MiniMax-M3", | |
| 131 | + | Reasoning: "high", | |
| 132 | + | Temperature: &temp, | |
| 133 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 134 | + | }) | |
| 135 | + | if err != nil { | |
| 136 | + | t.Fatal(err) | |
| 137 | + | } | |
| 138 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" || wire.Thinking.BudgetTokens != 0 { | |
| 139 | + | t.Fatalf("want adaptive thinking without a budget, got %+v", wire.Thinking) | |
| 140 | + | } | |
| 141 | + | if wire.OutputConfig != nil { | |
| 142 | + | t.Fatalf("MiniMax-M3 must not send output_config, got %+v", wire.OutputConfig) | |
| 143 | + | } | |
| 144 | + | if wire.Temperature == nil || *wire.Temperature != temp { | |
| 145 | + | t.Fatalf("MiniMax-M3 should keep temperature, got %v", wire.Temperature) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 93 | 149 | func TestAnthropicAdaptiveThinking(t *testing.T) { | |
| 94 | 150 | c := NewAnthropic("x", "").(*anthropicClient) | |
| 95 | 151 | temp := float32(0.7) | |
| @@ -2,6 +2,7 @@ package provider | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "math" | |||
| 5 | "net/http" | 6 | "net/http" | ||
| 6 | "net/http/httptest" | 7 | "net/http/httptest" | ||
| 7 | "strings" | 8 | "strings" | ||
| @@ -62,6 +63,25 @@ func TestComputeCost(t *testing.T) { | |||||
| 62 | } | 63 | } | ||
| 63 | } | 64 | } | ||
| 64 | 65 | ||||
| 66 | + | func TestComputeCostInputTier(t *testing.T) { | |||
| 67 | + | m := Model{ | |||
| 68 | + | PriceInput: 1, PriceOutput: 2, PriceCacheRead: 0.1, | |||
| 69 | + | PriceTierInputTokens: 100, | |||
| 70 | + | PriceInputAbove: 3, PriceOutputAbove: 4, PriceCacheReadAbove: 0.2, | |||
| 71 | + | } | |||
| 72 | + | below := ComputeCost(m, Usage{InputTokens: 50, OutputTokens: 10, CacheReadTokens: 50}) | |||
| 73 | + | wantBelow := (50.0*1 + 10*2 + 50*0.1) / 1_000_000 | |||
| 74 | + | if math.Abs(below-wantBelow) > 1e-12 { | |||
| 75 | + | t.Fatalf("below-tier cost=%v want=%v", below, wantBelow) | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | above := ComputeCost(m, Usage{InputTokens: 51, OutputTokens: 10, CacheReadTokens: 50}) | |||
| 79 | + | wantAbove := (51.0*3 + 10*4 + 50*0.2) / 1_000_000 | |||
| 80 | + | if math.Abs(above-wantAbove) > 1e-12 { | |||
| 81 | + | t.Fatalf("above-tier cost=%v want=%v", above, wantAbove) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 65 | func TestAnthropicErrorStatus(t *testing.T) { | 85 | func TestAnthropicErrorStatus(t *testing.T) { | ||
| 66 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 86 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 67 | w.WriteHeader(http.StatusUnauthorized) | 87 | w.WriteHeader(http.StatusUnauthorized) | ||
| @@ -90,6 +110,42 @@ func TestOpenAIErrorStatus(t *testing.T) { | |||||
| 90 | } | 110 | } | ||
| 91 | } | 111 | } | ||
| 92 | 112 | ||||
| 113 | + | func TestMiniMaxM3CatalogAndRequest(t *testing.T) { | |||
| 114 | + | for _, provider := range []string{"minimax", "minimax-cn"} { | |||
| 115 | + | m, err := FindModel(provider, "MiniMax-M3") | |||
| 116 | + | if err != nil { | |||
| 117 | + | t.Fatal(err) | |||
| 118 | + | } | |||
| 119 | + | if m.ContextWindow != 1000000 || m.MaxOutput != 131072 || !m.Reasoning || !m.AdaptiveThinkingCompat { | |||
| 120 | + | t.Errorf("%s metadata = %+v", provider, m) | |||
| 121 | + | } | |||
| 122 | + | if m.PriceInput != 0.3 || m.PriceOutput != 1.2 || m.PriceCacheRead != 0.06 || m.PriceTierInputTokens != 512000 || m.PriceInputAbove != 0.6 || m.PriceOutputAbove != 2.4 || m.PriceCacheReadAbove != 0.12 { | |||
| 123 | + | t.Errorf("%s pricing = %+v", provider, m) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | c := NewAnthropicCompat("minimax", "x", "https://api.minimax.io/anthropic").(*anthropicClient) | |||
| 128 | + | temp := float32(0.7) | |||
| 129 | + | wire, err := c.buildRequest(Request{ | |||
| 130 | + | Model: "MiniMax-M3", | |||
| 131 | + | Reasoning: "high", | |||
| 132 | + | Temperature: &temp, | |||
| 133 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 134 | + | }) | |||
| 135 | + | if err != nil { | |||
| 136 | + | t.Fatal(err) | |||
| 137 | + | } | |||
| 138 | + | if wire.Thinking == nil || wire.Thinking.Type != "adaptive" || wire.Thinking.BudgetTokens != 0 { | |||
| 139 | + | t.Fatalf("want adaptive thinking without a budget, got %+v", wire.Thinking) | |||
| 140 | + | } | |||
| 141 | + | if wire.OutputConfig != nil { | |||
| 142 | + | t.Fatalf("MiniMax-M3 must not send output_config, got %+v", wire.OutputConfig) | |||
| 143 | + | } | |||
| 144 | + | if wire.Temperature == nil || *wire.Temperature != temp { | |||
| 145 | + | t.Fatalf("MiniMax-M3 should keep temperature, got %v", wire.Temperature) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 93 | func TestAnthropicAdaptiveThinking(t *testing.T) { | 149 | func TestAnthropicAdaptiveThinking(t *testing.T) { | ||
| 94 | c := NewAnthropic("x", "").(*anthropicClient) | 150 | c := NewAnthropic("x", "").(*anthropicClient) | ||
| 95 | temp := float32(0.7) | 151 | temp := float32(0.7) | ||