mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 02:39:42 +02:00
Follow-up to #5188 addressing the second-round review. - ValidateThinkingLevel now fails an empty codex model closed instead of borrowing the flagged Default (gpt-5.6-sol). An empty model follows config.toml, which can resolve to any installed model; Sol alone advertises `ultra`, so the old borrow green-lit levels Luna / gpt-5.5 don't support and Codex doesn't reject. Checked before ListModels so a discovery error can't fail it open. Frontend pickModelEntry mirrors this (no per-model effort preview for an empty codex model); the persisted-orphan clear path stays. - parseCodexDebugModels drops efforts without a known label so the picker never advertises a level the Create/Update enum gate would 400 on save; the contract test now drives the real parser with an unknown effort instead of comparing two hand-written maps. - gpt-5.6 price aliases anchor to a literal dot (not the [.-] class), so dashed variants like gpt-5-6-luna surface as unmapped on both backend and frontend rather than silently borrowing a tier. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
111 lines
7.8 KiB
Go
111 lines
7.8 KiB
Go
package metrics
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type ModelPrice struct {
|
|
Provider string
|
|
Model string
|
|
InputPerM float64
|
|
CacheReadPerM float64
|
|
CacheWritePerM float64
|
|
OutputPerM float64
|
|
}
|
|
|
|
var modelPrices = map[string]ModelPrice{
|
|
// GPT-5.6 series (Codex `codex` provider). Official rates from OpenAI's
|
|
// GPT-5.6 announcement (openai.com/index/previewing-gpt-5-6-sol). For 5.6+
|
|
// cache read is the 90%-off cached-input rate (0.1x input) and cache write
|
|
// is billed at 1.25x the uncached input rate — unlike earlier OpenAI SKUs,
|
|
// which don't bill cache writes separately. NOTE: Codex's app-server usage
|
|
// stream (0.144.1) does not yet report cache-write tokens separately, so
|
|
// today those tokens fall into plain input and are billed at 1x; the
|
|
// CacheWrite rate below is correct but not yet exercised for Codex.
|
|
"openai:gpt-5.6-sol": {Provider: "openai", Model: "gpt-5.6-sol", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 6.25, OutputPerM: 30.00},
|
|
"openai:gpt-5.6-terra": {Provider: "openai", Model: "gpt-5.6-terra", InputPerM: 2.50, CacheReadPerM: 0.25, CacheWritePerM: 3.125, OutputPerM: 15.00},
|
|
"openai:gpt-5.6-luna": {Provider: "openai", Model: "gpt-5.6-luna", InputPerM: 1.00, CacheReadPerM: 0.10, CacheWritePerM: 1.25, OutputPerM: 6.00},
|
|
"openai:gpt-5.5": {Provider: "openai", Model: "gpt-5.5", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 0.50, OutputPerM: 30.00},
|
|
"openai:gpt-5.4": {Provider: "openai", Model: "gpt-5.4", InputPerM: 2.50, CacheReadPerM: 0.25, CacheWritePerM: 0.25, OutputPerM: 15.00},
|
|
"openai:gpt-5.4-mini": {Provider: "openai", Model: "gpt-5.4-mini", InputPerM: 0.75, CacheReadPerM: 0.075, CacheWritePerM: 0.075, OutputPerM: 4.50},
|
|
"openai:gpt-5.3-codex": {Provider: "openai", Model: "gpt-5.3-codex", InputPerM: 1.75, CacheReadPerM: 0.175, CacheWritePerM: 0.175, OutputPerM: 14.00},
|
|
"openai:gpt-5.2-codex": {Provider: "openai", Model: "gpt-5.2-codex", InputPerM: 1.75, CacheReadPerM: 0.175, CacheWritePerM: 0.175, OutputPerM: 14.00},
|
|
// Anthropic's Sonnet 5 launch price is $2 / $10 through 2026-08-31. This
|
|
// static table cannot schedule the published post-intro $3 / $15 change yet,
|
|
// so keep the intro rate here and update the row when catalog support exists.
|
|
"anthropic:claude-sonnet-5": {Provider: "anthropic", Model: "claude-sonnet-5", InputPerM: 2.00, CacheReadPerM: 0.20, CacheWritePerM: 2.50, OutputPerM: 10.00},
|
|
"anthropic:claude-fable-5": {Provider: "anthropic", Model: "claude-fable-5", InputPerM: 10.00, CacheReadPerM: 1.00, CacheWritePerM: 12.50, OutputPerM: 50.00},
|
|
"anthropic:claude-opus-4.8": {Provider: "anthropic", Model: "claude-opus-4.8", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 6.25, OutputPerM: 25.00},
|
|
"anthropic:claude-opus-4.7": {Provider: "anthropic", Model: "claude-opus-4.7", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 6.25, OutputPerM: 25.00},
|
|
"anthropic:claude-opus-4.6": {Provider: "anthropic", Model: "claude-opus-4.6", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 6.25, OutputPerM: 25.00},
|
|
"anthropic:claude-opus-4.5": {Provider: "anthropic", Model: "claude-opus-4.5", InputPerM: 5.00, CacheReadPerM: 0.50, CacheWritePerM: 6.25, OutputPerM: 25.00},
|
|
"anthropic:claude-sonnet-4.6": {Provider: "anthropic", Model: "claude-sonnet-4.6", InputPerM: 3.00, CacheReadPerM: 0.30, CacheWritePerM: 3.75, OutputPerM: 15.00},
|
|
"anthropic:claude-sonnet-4.5": {Provider: "anthropic", Model: "claude-sonnet-4.5", InputPerM: 3.00, CacheReadPerM: 0.30, CacheWritePerM: 3.75, OutputPerM: 15.00},
|
|
"anthropic:claude-haiku-4.5": {Provider: "anthropic", Model: "claude-haiku-4.5", InputPerM: 1.00, CacheReadPerM: 0.10, CacheWritePerM: 1.25, OutputPerM: 5.00},
|
|
"deepseek:v4-pro": {Provider: "deepseek", Model: "v4-pro", InputPerM: 1.74, CacheReadPerM: 0.0145, CacheWritePerM: 1.74, OutputPerM: 3.48},
|
|
"deepseek:v4-flash": {Provider: "deepseek", Model: "v4-flash", InputPerM: 0.56, CacheReadPerM: 0.0112, CacheWritePerM: 0.56, OutputPerM: 1.12},
|
|
"minimax:m2.7": {Provider: "minimax", Model: "m2.7", InputPerM: 0.30, CacheReadPerM: 0.06, CacheWritePerM: 0.375, OutputPerM: 1.20},
|
|
"minimax:m2.7-highspeed": {Provider: "minimax", Model: "m2.7-highspeed", InputPerM: 0.60, CacheReadPerM: 0.06, CacheWritePerM: 0.375, OutputPerM: 2.40},
|
|
"google:gemini-3-flash": {Provider: "google", Model: "gemini-3-flash", InputPerM: 0.50, CacheReadPerM: 0.05, CacheWritePerM: 0.50, OutputPerM: 3.00},
|
|
"google:gemini-3.1-pro": {Provider: "google", Model: "gemini-3.1-pro", InputPerM: 2.00, CacheReadPerM: 0.20, CacheWritePerM: 2.00, OutputPerM: 12.00},
|
|
"google:gemini-2.5-pro": {Provider: "google", Model: "gemini-2.5-pro", InputPerM: 1.25, CacheReadPerM: 0.31, CacheWritePerM: 1.25, OutputPerM: 10.00},
|
|
"google:gemini-2.5-flash": {Provider: "google", Model: "gemini-2.5-flash", InputPerM: 0.30, CacheReadPerM: 0.03, CacheWritePerM: 0.30, OutputPerM: 2.50},
|
|
}
|
|
|
|
var modelAliasRules = []struct {
|
|
re *regexp.Regexp
|
|
priceKey string
|
|
}{
|
|
// Anchored exact-match: the effort is carried in a separate field, so the
|
|
// model id is the bare slug. Anchoring to `$` keeps unknown variants
|
|
// (`gpt-5.6-luna-pro`, `gpt-5.6-luna/x`) out of these rows. The `.` is a
|
|
// LITERAL dot, not the `[.-]` class the older rows use — the real Codex
|
|
// slug is always dotted (`gpt-5.6-luna`), and the frontend resolver in
|
|
// utils.ts does NOT dash-normalize, so a dashed `gpt-5-6-luna` must surface
|
|
// as unmapped on both sides rather than silently borrowing a tier here.
|
|
{regexp.MustCompile(`(^|/|:)gpt-5\.6-sol$`), "openai:gpt-5.6-sol"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5\.6-terra$`), "openai:gpt-5.6-terra"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5\.6-luna$`), "openai:gpt-5.6-luna"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5[.-]5$|^gpt-5-5$`), "openai:gpt-5.5"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5[.-]4($|-2026-03-05|-xhigh)`), "openai:gpt-5.4"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5[.-]4-mini($|[^a-z0-9])`), "openai:gpt-5.4-mini"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5[.-]3-codex$`), "openai:gpt-5.3-codex"},
|
|
{regexp.MustCompile(`(^|/|:)gpt-5[.-]2-codex$`), "openai:gpt-5.2-codex"},
|
|
{regexp.MustCompile(`claude-sonnet-5|claude-5-sonnet`), "anthropic:claude-sonnet-5"},
|
|
{regexp.MustCompile(`claude-fable-5`), "anthropic:claude-fable-5"},
|
|
{regexp.MustCompile(`claude-opus-4[-.]8`), "anthropic:claude-opus-4.8"},
|
|
{regexp.MustCompile(`claude-opus-4[-.]7`), "anthropic:claude-opus-4.7"},
|
|
{regexp.MustCompile(`claude-opus-4[-.]6`), "anthropic:claude-opus-4.6"},
|
|
{regexp.MustCompile(`claude-opus-4[-.]5`), "anthropic:claude-opus-4.5"},
|
|
{regexp.MustCompile(`claude-sonnet-4[-.]6|claude-4[-.]6-sonnet`), "anthropic:claude-sonnet-4.6"},
|
|
{regexp.MustCompile(`claude-sonnet-4[-.]5|claude-4[-.]5-sonnet`), "anthropic:claude-sonnet-4.5"},
|
|
{regexp.MustCompile(`claude-haiku-4[-.]5`), "anthropic:claude-haiku-4.5"},
|
|
{regexp.MustCompile(`deepseek-v4-pro`), "deepseek:v4-pro"},
|
|
{regexp.MustCompile(`deepseek-v4-flash|^deepseek-chat$|^deepseek-reasoner$`), "deepseek:v4-flash"},
|
|
{regexp.MustCompile(`minimax-m2[.]7.*highspeed|highspeed.*minimax-m2[.]7`), "minimax:m2.7-highspeed"},
|
|
{regexp.MustCompile(`minimax-m2[.]7`), "minimax:m2.7"},
|
|
{regexp.MustCompile(`gemini-3-flash`), "google:gemini-3-flash"},
|
|
{regexp.MustCompile(`gemini-3[.]1-pro`), "google:gemini-3.1-pro"},
|
|
{regexp.MustCompile(`gemini-2[.]5-pro`), "google:gemini-2.5-pro"},
|
|
{regexp.MustCompile(`gemini-2[.]5-flash`), "google:gemini-2.5-flash"},
|
|
}
|
|
|
|
func PriceForModelAlias(model string) (ModelPrice, bool) {
|
|
model = strings.ToLower(strings.TrimSpace(model))
|
|
for _, rule := range modelAliasRules {
|
|
if rule.re.MatchString(model) {
|
|
price, ok := modelPrices[rule.priceKey]
|
|
return price, ok
|
|
}
|
|
}
|
|
return ModelPrice{}, false
|
|
}
|
|
|
|
func tokenCostUSD(tokens int64, pricePerM float64) float64 {
|
|
if tokens <= 0 || pricePerM <= 0 {
|
|
return 0
|
|
}
|
|
return float64(tokens) * pricePerM / 1_000_000
|
|
}
|