a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
package savings
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Price is input-token pricing in USD per 1M tokens. Tokens saved are input
|
|
// tokens that would have been sent to the model, so we always bill them at
|
|
// the input rate.
|
|
type Price struct {
|
|
Model string `json:"model"`
|
|
USDPerMInput float64 `json:"usd_per_m_input"`
|
|
}
|
|
|
|
// defaultPricing is the built-in table used when no override is configured.
|
|
// Input-token list prices in USD per 1M tokens, covering the providers
|
|
// gortex's LLM features talk to: Anthropic, OpenAI (also reachable via
|
|
// Azure), Google Gemini and DeepSeek. Bedrock-hosted Anthropic/OpenAI ids
|
|
// resolve through findPrice's substring match. Tokens saved are billed at
|
|
// the input rate. Users can override the whole table via
|
|
// GORTEX_MODEL_PRICING_JSON='[{"model":"...","usd_per_m_input":N},...]'.
|
|
var defaultPricing = []Price{
|
|
// Anthropic — claude-opus-4-8 is the headline default (see savings.go);
|
|
// claude-mythos-preview is the current preview flagship.
|
|
{"claude-mythos-preview", 10.00},
|
|
{"claude-opus-4-8", 5.00},
|
|
{"claude-opus-4-7", 5.00},
|
|
{"claude-opus-4-6", 5.00},
|
|
{"claude-sonnet-4-6", 3.00},
|
|
{"claude-haiku-4-5", 1.00},
|
|
// OpenAI (also reachable via Azure).
|
|
{"gpt-5.5", 5.00},
|
|
{"gpt-5.4", 2.50},
|
|
{"gpt-5.4-mini", 0.75},
|
|
{"gpt-5.4-nano", 0.20},
|
|
{"gpt-4.1", 2.00},
|
|
{"gpt-4.1-mini", 0.40},
|
|
{"gpt-4.1-nano", 0.10},
|
|
{"gpt-4o", 2.50},
|
|
{"gpt-4o-mini", 0.15},
|
|
{"o3", 2.00},
|
|
{"o4-mini", 1.10},
|
|
// Google Gemini — gemini-2.5-pro is gortex's Gemini default.
|
|
{"gemini-3.1-pro", 2.00},
|
|
{"gemini-3.5-flash", 1.50},
|
|
{"gemini-3.1-flash-lite", 0.25},
|
|
{"gemini-2.5-pro", 1.25},
|
|
{"gemini-2.5-flash", 0.30},
|
|
{"gemini-2.5-flash-lite", 0.10},
|
|
// DeepSeek — deepseek-chat / deepseek-reasoner are gortex's configured ids.
|
|
{"deepseek-chat", 0.27},
|
|
{"deepseek-reasoner", 0.42},
|
|
}
|
|
|
|
// Pricing returns the active pricing table — default unless overridden by
|
|
// GORTEX_MODEL_PRICING_JSON. Falls back to the default on malformed JSON.
|
|
func Pricing() []Price {
|
|
raw := os.Getenv("GORTEX_MODEL_PRICING_JSON")
|
|
if raw == "" {
|
|
return defaultPricing
|
|
}
|
|
var custom []Price
|
|
if err := json.Unmarshal([]byte(raw), &custom); err != nil || len(custom) == 0 {
|
|
return defaultPricing
|
|
}
|
|
return custom
|
|
}
|
|
|
|
// CostAvoided returns the dollar value of tokensSaved evaluated against the
|
|
// pricing row whose model matches `name` (case-insensitive, substring).
|
|
// Returns 0 when no matching entry exists.
|
|
func CostAvoided(tokensSaved int64, name string) float64 {
|
|
if tokensSaved <= 0 {
|
|
return 0
|
|
}
|
|
p := findPrice(name)
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return float64(tokensSaved) * p.USDPerMInput / 1_000_000.0
|
|
}
|
|
|
|
// ModelRate returns the USD-per-1M-token input rate for the named model
|
|
// (case-insensitive, substring — see findPrice), or 0 when no pricing row
|
|
// matches. Callers that need a ProviderPricing-shaped rate card (the
|
|
// review cost block) use this to derive one from the built-in table.
|
|
func ModelRate(name string) float64 {
|
|
p := findPrice(name)
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return p.USDPerMInput
|
|
}
|
|
|
|
// CostAvoidedAll returns the cost across every entry in the pricing table,
|
|
// keyed by model name — useful for the CLI's multi-model summary.
|
|
func CostAvoidedAll(tokensSaved int64) map[string]float64 {
|
|
out := make(map[string]float64, len(defaultPricing))
|
|
for _, p := range Pricing() {
|
|
out[p.Model] = float64(tokensSaved) * p.USDPerMInput / 1_000_000.0
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findPrice locates a pricing entry by case-insensitive substring match on
|
|
// model name so callers can pass "opus" or "claude-opus-4-8" interchangeably.
|
|
func findPrice(name string) *Price {
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
target := strings.ToLower(name)
|
|
prices := Pricing()
|
|
// Exact match first, longest substring second — avoids "claude" matching
|
|
// "claude-opus-4-8" when the user passed "claude-sonnet-4-6".
|
|
for i := range prices {
|
|
if strings.EqualFold(prices[i].Model, name) {
|
|
return &prices[i]
|
|
}
|
|
}
|
|
type hit struct {
|
|
i int
|
|
length int
|
|
}
|
|
var hits []hit
|
|
for i := range prices {
|
|
m := strings.ToLower(prices[i].Model)
|
|
if strings.Contains(m, target) || strings.Contains(target, m) {
|
|
hits = append(hits, hit{i, len(prices[i].Model)})
|
|
}
|
|
}
|
|
if len(hits) == 0 {
|
|
return nil
|
|
}
|
|
sort.Slice(hits, func(a, b int) bool { return hits[a].length > hits[b].length })
|
|
return &prices[hits[0].i]
|
|
}
|