847 lines
26 KiB
Go
847 lines
26 KiB
Go
package catalog
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"gpt-load/internal/pricing"
|
|
)
|
|
|
|
const maxModelIDBytes = 255
|
|
|
|
var (
|
|
providerCanonicalFields = []string{"id", "name", "api", "npm", "models"}
|
|
modelCanonicalFields = []string{
|
|
"id", "name", "description", "family", "attachment", "reasoning",
|
|
"tool_call", "structured_output", "temperature", "knowledge",
|
|
"release_date", "last_updated", "modalities", "open_weights", "limit",
|
|
"status", "cost", "experimental",
|
|
}
|
|
modalityCanonicalFields = []string{"input", "output"}
|
|
limitCanonicalFields = []string{"context", "input", "output"}
|
|
costCanonicalFields = []string{"input", "output", "cache_read", "cache_write", "tiers"}
|
|
experimentalCanonicalFields = []string{"modes"}
|
|
modeCanonicalFields = []string{"cost", "provider"}
|
|
tierCanonicalFields = []string{"tier", "input", "output", "cache_read", "cache_write"}
|
|
tierRuleCanonicalFields = []string{"type", "size"}
|
|
)
|
|
|
|
// Parse decodes, validates, and normalizes the retained Models.dev-compatible subset.
|
|
func Parse(reader io.Reader) (*Snapshot, error) {
|
|
if reader == nil {
|
|
return nil, fmt.Errorf("catalog reader is required")
|
|
}
|
|
decoder := json.NewDecoder(reader)
|
|
decoder.UseNumber()
|
|
providerValues, err := decodeUniqueObject(decoder, "provider")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireJSONEOF(decoder); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snapshot := &Snapshot{Providers: make(map[string]Provider, len(providerValues))}
|
|
for providerKey, rawProvider := range providerValues {
|
|
provider, err := parseProvider(providerKey, rawProvider)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("provider %q: %w", providerKey, err)
|
|
}
|
|
snapshot.Providers[providerKey] = provider
|
|
}
|
|
if err := validateSnapshot(snapshot); err != nil {
|
|
return nil, err
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func parseProvider(key string, raw json.RawMessage) (Provider, error) {
|
|
fields, err := decodeCanonicalObject(raw, "provider", providerCanonicalFields)
|
|
if err != nil {
|
|
return Provider{}, err
|
|
}
|
|
id, err := decodeStringField(fields, "id")
|
|
if err != nil {
|
|
return Provider{}, err
|
|
}
|
|
if id != key {
|
|
return Provider{}, fmt.Errorf("map key and provider id must match")
|
|
}
|
|
if _, err := pricing.ProviderScopeKey(id); err != nil {
|
|
return Provider{}, err
|
|
}
|
|
nameValue, err := decodeStringField(fields, "name")
|
|
if err != nil {
|
|
return Provider{}, err
|
|
}
|
|
name := strings.TrimSpace(nameValue)
|
|
if name == "" {
|
|
return Provider{}, fmt.Errorf("provider name is required")
|
|
}
|
|
modelsRaw, hasModels := fields["models"]
|
|
if !hasModels || bytes.Equal(bytes.TrimSpace(modelsRaw), []byte("null")) {
|
|
return Provider{}, fmt.Errorf("provider models object is required")
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(modelsRaw))
|
|
decoder.UseNumber()
|
|
modelValues, err := decodeUniqueObject(decoder, "model")
|
|
if err != nil {
|
|
return Provider{}, err
|
|
}
|
|
if err := requireJSONEOF(decoder); err != nil {
|
|
return Provider{}, err
|
|
}
|
|
|
|
if _, err := decodeStringField(fields, "api"); err != nil {
|
|
return Provider{}, err
|
|
}
|
|
if _, err := decodeStringField(fields, "npm"); err != nil {
|
|
return Provider{}, err
|
|
}
|
|
provider := Provider{
|
|
ID: id, Name: name, Models: make(map[string]Model, len(modelValues)),
|
|
}
|
|
for modelKey, rawModel := range modelValues {
|
|
model, err := parseModel(modelKey, rawModel)
|
|
if err != nil {
|
|
return Provider{}, fmt.Errorf("model %q: %w", modelKey, err)
|
|
}
|
|
provider.Models[modelKey] = model
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
func parseModel(key string, raw json.RawMessage) (Model, error) {
|
|
fields, err := decodeCanonicalObject(raw, "model", modelCanonicalFields)
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
id, err := decodeStringField(fields, "id")
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
if id != key {
|
|
return Model{}, fmt.Errorf("map key and model id must match")
|
|
}
|
|
if err := validateModelID(id); err != nil {
|
|
return Model{}, err
|
|
}
|
|
nameValue, err := decodeStringField(fields, "name")
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
name := strings.TrimSpace(nameValue)
|
|
if name == "" {
|
|
return Model{}, fmt.Errorf("model name is required")
|
|
}
|
|
metadata, err := parseModelMetadata(fields)
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
model := Model{ID: id, Name: name, Metadata: metadata}
|
|
var cost *ModelCost
|
|
costRaw, hasCost := fields["cost"]
|
|
if hasCost && !bytes.Equal(bytes.TrimSpace(costRaw), []byte("null")) {
|
|
cost, err = parseCost(costRaw)
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
}
|
|
modePrices, err := parseExperimentalModePrices(fields["experimental"])
|
|
if err != nil {
|
|
return Model{}, err
|
|
}
|
|
if len(modePrices) > 0 {
|
|
if cost == nil {
|
|
cost = &ModelCost{}
|
|
}
|
|
cost.ModePrices = modePrices
|
|
}
|
|
model.Cost = cost
|
|
return model, nil
|
|
}
|
|
|
|
func parseExperimentalModePrices(raw json.RawMessage) (map[pricing.Mode]pricing.Prices, error) {
|
|
if len(raw) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
fields, err := decodeCanonicalObject(raw, "model experimental", experimentalCanonicalFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modesRaw, exists := fields["modes"]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(modesRaw), []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(modesRaw))
|
|
decoder.UseNumber()
|
|
modes, err := decodeUniqueObject(decoder, "experimental modes")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireJSONEOF(decoder); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[pricing.Mode]pricing.Prices)
|
|
for key, rawMode := range modes {
|
|
mode := pricing.Mode(key)
|
|
if !mode.Valid() || mode == pricing.ModeStandard {
|
|
return nil, fmt.Errorf("invalid experimental pricing mode %q", key)
|
|
}
|
|
modeFields, err := decodeCanonicalObject(rawMode, "experimental mode", modeCanonicalFields)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mode %q: %w", key, err)
|
|
}
|
|
rawCost, exists := modeFields["cost"]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(rawCost), []byte("null")) {
|
|
continue
|
|
}
|
|
prices, err := parseModePrices(rawCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mode %q: %w", key, err)
|
|
}
|
|
if hasSetPrice(prices) {
|
|
result[mode] = prices
|
|
}
|
|
}
|
|
if len(result) == 0 {
|
|
return nil, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseModePrices(raw json.RawMessage) (pricing.Prices, error) {
|
|
fields, err := decodeCanonicalObject(raw, "mode cost", costCanonicalFields)
|
|
if err != nil {
|
|
return pricing.Prices{}, err
|
|
}
|
|
input, output, cacheRead, cacheWrite, err := decodePriceFields(fields)
|
|
if err != nil {
|
|
return pricing.Prices{}, err
|
|
}
|
|
return parsePrices(input, output, cacheRead, cacheWrite)
|
|
}
|
|
|
|
func parseModelMetadata(fields map[string]json.RawMessage) (ModelMetadata, error) {
|
|
stringFields := [...]struct {
|
|
name string
|
|
set func(*ModelMetadata, string)
|
|
}{
|
|
{name: "description", set: func(metadata *ModelMetadata, value string) { metadata.Description = value }},
|
|
{name: "family", set: func(metadata *ModelMetadata, value string) { metadata.Family = value }},
|
|
{name: "knowledge", set: func(metadata *ModelMetadata, value string) { metadata.Knowledge = value }},
|
|
{name: "release_date", set: func(metadata *ModelMetadata, value string) { metadata.ReleaseDate = value }},
|
|
{name: "last_updated", set: func(metadata *ModelMetadata, value string) { metadata.LastUpdated = value }},
|
|
{name: "status", set: func(metadata *ModelMetadata, value string) { metadata.Status = value }},
|
|
}
|
|
var metadata ModelMetadata
|
|
for _, field := range stringFields {
|
|
value, err := decodeStringField(fields, field.name)
|
|
if err != nil {
|
|
return ModelMetadata{}, err
|
|
}
|
|
field.set(&metadata, strings.TrimSpace(value))
|
|
}
|
|
|
|
boolFields := [...]struct {
|
|
name string
|
|
set func(*ModelMetadata, *bool)
|
|
}{
|
|
{name: "attachment", set: func(metadata *ModelMetadata, value *bool) { metadata.Capabilities.Attachment = value }},
|
|
{name: "reasoning", set: func(metadata *ModelMetadata, value *bool) { metadata.Capabilities.Reasoning = value }},
|
|
{name: "tool_call", set: func(metadata *ModelMetadata, value *bool) { metadata.Capabilities.ToolCall = value }},
|
|
{name: "structured_output", set: func(metadata *ModelMetadata, value *bool) { metadata.Capabilities.StructuredOutput = value }},
|
|
{name: "temperature", set: func(metadata *ModelMetadata, value *bool) { metadata.Capabilities.Temperature = value }},
|
|
{name: "open_weights", set: func(metadata *ModelMetadata, value *bool) { metadata.OpenWeights = value }},
|
|
}
|
|
for _, field := range boolFields {
|
|
value, err := decodeBoolField(fields, field.name)
|
|
if err != nil {
|
|
return ModelMetadata{}, err
|
|
}
|
|
field.set(&metadata, value)
|
|
}
|
|
|
|
modalities, err := parseModelModalities(fields["modalities"])
|
|
if err != nil {
|
|
return ModelMetadata{}, err
|
|
}
|
|
limits, err := parseModelLimits(fields["limit"])
|
|
if err != nil {
|
|
return ModelMetadata{}, err
|
|
}
|
|
metadata.Modalities = modalities
|
|
metadata.Limits = limits
|
|
return metadata, nil
|
|
}
|
|
|
|
func parseModelModalities(raw json.RawMessage) (ModelModalities, error) {
|
|
if len(raw) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return ModelModalities{}, nil
|
|
}
|
|
fields, err := decodeCanonicalObject(raw, "model modalities", modalityCanonicalFields)
|
|
if err != nil {
|
|
return ModelModalities{}, err
|
|
}
|
|
input, err := decodeStringSliceField(fields, "input")
|
|
if err != nil {
|
|
return ModelModalities{}, err
|
|
}
|
|
output, err := decodeStringSliceField(fields, "output")
|
|
if err != nil {
|
|
return ModelModalities{}, err
|
|
}
|
|
return ModelModalities{Input: input, Output: output}, nil
|
|
}
|
|
|
|
func parseModelLimits(raw json.RawMessage) (ModelLimits, error) {
|
|
if len(raw) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return ModelLimits{}, nil
|
|
}
|
|
fields, err := decodeCanonicalObject(raw, "model limits", limitCanonicalFields)
|
|
if err != nil {
|
|
return ModelLimits{}, err
|
|
}
|
|
contextLimit, err := decodeNonNegativeInt64Field(fields, "context")
|
|
if err != nil {
|
|
return ModelLimits{}, err
|
|
}
|
|
inputLimit, err := decodeNonNegativeInt64Field(fields, "input")
|
|
if err != nil {
|
|
return ModelLimits{}, err
|
|
}
|
|
outputLimit, err := decodeNonNegativeInt64Field(fields, "output")
|
|
if err != nil {
|
|
return ModelLimits{}, err
|
|
}
|
|
return ModelLimits{Context: contextLimit, Input: inputLimit, Output: outputLimit}, nil
|
|
}
|
|
|
|
func parseCost(raw json.RawMessage) (*ModelCost, error) {
|
|
fields, err := decodeCanonicalObject(raw, "cost", costCanonicalFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input, output, cacheRead, cacheWrite, err := decodePriceFields(fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prices, err := parsePrices(input, output, cacheRead, cacheWrite)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rawTiers []json.RawMessage
|
|
if raw, exists := fields["tiers"]; exists && !bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
if err := decodeOneJSON(raw, &rawTiers); err != nil {
|
|
return nil, fmt.Errorf("decode cost tiers: %w", err)
|
|
}
|
|
}
|
|
tiers := make([]pricing.ContextTier, 0, len(rawTiers))
|
|
previousThreshold := int64(-1)
|
|
for index, rawTier := range rawTiers {
|
|
threshold, tierPrices, err := parseContextTier(rawTier)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tier %d: %w", index, err)
|
|
}
|
|
if threshold <= previousThreshold {
|
|
return nil, fmt.Errorf("context tier thresholds must be strictly increasing")
|
|
}
|
|
if !hasSetPrice(tierPrices) {
|
|
return nil, fmt.Errorf("tier %d must set at least one supported price", index)
|
|
}
|
|
tiers = append(tiers, pricing.ContextTier{InputThresholdTokens: threshold, Prices: tierPrices})
|
|
previousThreshold = threshold
|
|
}
|
|
if !hasSetPrice(prices) && len(tiers) == 0 {
|
|
return nil, nil
|
|
}
|
|
return &ModelCost{Prices: prices, ContextTiers: tiers}, nil
|
|
}
|
|
|
|
func parseContextTier(raw json.RawMessage) (int64, pricing.Prices, error) {
|
|
fields, err := decodeCanonicalObject(raw, "context tier", tierCanonicalFields)
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
tierRaw, exists := fields["tier"]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(tierRaw), []byte("null")) {
|
|
return 0, pricing.Prices{}, fmt.Errorf("tier rule is required")
|
|
}
|
|
ruleFields, err := decodeCanonicalObject(tierRaw, "context tier rule", tierRuleCanonicalFields)
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
tierType, err := decodeStringField(ruleFields, "type")
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
if tierType != "context" {
|
|
return 0, pricing.Prices{}, fmt.Errorf("unsupported type %q", tierType)
|
|
}
|
|
size, err := decodeNumberField(ruleFields, "size")
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
if size == nil {
|
|
return 0, pricing.Prices{}, fmt.Errorf("context size is required")
|
|
}
|
|
threshold, err := strconv.ParseInt(size.String(), 10, 64)
|
|
if err != nil || threshold < 0 {
|
|
return 0, pricing.Prices{}, fmt.Errorf("context size must be a non-negative integer")
|
|
}
|
|
input, output, cacheRead, cacheWrite, err := decodePriceFields(fields)
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
prices, err := parsePrices(input, output, cacheRead, cacheWrite)
|
|
if err != nil {
|
|
return 0, pricing.Prices{}, err
|
|
}
|
|
return threshold, prices, nil
|
|
}
|
|
|
|
func decodePriceFields(fields map[string]json.RawMessage) (*json.Number, *json.Number, *json.Number, *json.Number, error) {
|
|
input, err := decodeNumberField(fields, "input")
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
output, err := decodeNumberField(fields, "output")
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
cacheRead, err := decodeNumberField(fields, "cache_read")
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
cacheWrite, err := decodeNumberField(fields, "cache_write")
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
return input, output, cacheRead, cacheWrite, nil
|
|
}
|
|
|
|
func parsePrices(input, output, cacheRead, cacheWrite *json.Number) (pricing.Prices, error) {
|
|
values := [...]struct {
|
|
name string
|
|
number *json.Number
|
|
set func(*pricing.Prices, pricing.Price)
|
|
}{
|
|
{name: "input", number: input, set: func(prices *pricing.Prices, price pricing.Price) { prices.Input = price }},
|
|
{name: "output", number: output, set: func(prices *pricing.Prices, price pricing.Price) { prices.Output = price }},
|
|
{name: "cache_read", number: cacheRead, set: func(prices *pricing.Prices, price pricing.Price) { prices.CacheRead = price }},
|
|
{name: "cache_write", number: cacheWrite, set: func(prices *pricing.Prices, price pricing.Price) { prices.CacheWrite = price }},
|
|
}
|
|
var prices pricing.Prices
|
|
for _, value := range values {
|
|
if value.number == nil {
|
|
continue
|
|
}
|
|
amount, err := parseJSONUSD(value.number.String())
|
|
if err != nil {
|
|
return pricing.Prices{}, fmt.Errorf("%s price: %w", value.name, err)
|
|
}
|
|
value.set(&prices, pricing.Price{NanoUSDPerMillion: amount, Set: true})
|
|
}
|
|
return prices, nil
|
|
}
|
|
|
|
// parseJSONUSD expands JSON exponent notation with string arithmetic before
|
|
// handing the plain fixed-point decimal to pricing.ParseUSD. No binary float is
|
|
// created. Long fixed-point values emitted by Models.dev are rounded to the
|
|
// nearest nano-USD only when they remain above zero after normalization.
|
|
func parseJSONUSD(value string) (pricing.NanoUSD, error) {
|
|
if strings.HasPrefix(value, "-") {
|
|
return 0, fmt.Errorf("USD price must be non-negative")
|
|
}
|
|
if normalized, ok := parseModelsDevFloatArtifact(value); ok {
|
|
return normalized, nil
|
|
}
|
|
mantissa, exponentText, hasExponent := strings.Cut(value, "e")
|
|
if !hasExponent {
|
|
mantissa, exponentText, hasExponent = strings.Cut(value, "E")
|
|
}
|
|
if !hasExponent {
|
|
return pricing.ParseUSD(value)
|
|
}
|
|
exponent, err := strconv.ParseInt(exponentText, 10, 32)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid USD exponent")
|
|
}
|
|
integer, fraction, hasFraction := strings.Cut(mantissa, ".")
|
|
if integer == "" || (hasFraction && fraction == "") {
|
|
return 0, fmt.Errorf("invalid USD decimal")
|
|
}
|
|
digits := integer + fraction
|
|
leadingZeros := len(digits) - len(strings.TrimLeft(digits, "0"))
|
|
if leadingZeros == len(digits) {
|
|
return pricing.ParseUSD("0")
|
|
}
|
|
digits = digits[leadingZeros:]
|
|
decimalPosition := int64(len(integer)-leadingZeros) + exponent
|
|
digits = strings.TrimRight(digits, "0")
|
|
if digits == "" {
|
|
return pricing.ParseUSD("0")
|
|
}
|
|
|
|
var plain string
|
|
switch {
|
|
case decimalPosition <= 0:
|
|
fractionalDigits := -decimalPosition + int64(len(digits))
|
|
if fractionalDigits > 9 {
|
|
return 0, fmt.Errorf("USD price has more than nine decimal places")
|
|
}
|
|
plain = "0." + strings.Repeat("0", int(-decimalPosition)) + digits
|
|
case decimalPosition >= int64(len(digits)):
|
|
integerDigits := decimalPosition
|
|
if integerDigits > 32 {
|
|
return 0, fmt.Errorf("USD price exceeds int64 nano USD")
|
|
}
|
|
plain = digits + strings.Repeat("0", int(decimalPosition)-len(digits))
|
|
default:
|
|
fractionalDigits := int64(len(digits)) - decimalPosition
|
|
if fractionalDigits > 9 {
|
|
return 0, fmt.Errorf("USD price has more than nine decimal places")
|
|
}
|
|
plain = digits[:decimalPosition] + "." + digits[decimalPosition:]
|
|
}
|
|
return pricing.ParseUSD(plain)
|
|
}
|
|
|
|
const modelsDevArtifactMinFractionDigits = 15
|
|
|
|
func parseModelsDevFloatArtifact(value string) (pricing.NanoUSD, bool) {
|
|
_, fraction, hasFraction := strings.Cut(value, ".")
|
|
if !hasFraction || strings.ContainsAny(fraction, "eE") ||
|
|
len(fraction) < modelsDevArtifactMinFractionDigits {
|
|
return 0, false
|
|
}
|
|
|
|
exact, ok := new(big.Rat).SetString(value)
|
|
if !ok || exact.Sign() < 0 {
|
|
return 0, false
|
|
}
|
|
scaled := new(big.Rat).Mul(exact, big.NewRat(1_000_000_000, 1))
|
|
quotient, remainder := new(big.Int), new(big.Int)
|
|
quotient.QuoRem(scaled.Num(), scaled.Denom(), remainder)
|
|
if new(big.Int).Lsh(remainder, 1).Cmp(scaled.Denom()) >= 0 {
|
|
quotient.Add(quotient, big.NewInt(1))
|
|
}
|
|
if quotient.Sign() == 0 && exact.Sign() != 0 || !quotient.IsInt64() {
|
|
return 0, false
|
|
}
|
|
return pricing.NanoUSD(quotient.Int64()), true
|
|
}
|
|
|
|
func decodeUniqueObject(decoder *json.Decoder, kind string) (map[string]json.RawMessage, error) {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode %s object: %w", kind, err)
|
|
}
|
|
delimiter, ok := token.(json.Delim)
|
|
if !ok || delimiter != '{' {
|
|
return nil, fmt.Errorf("%s catalog value must be an object", kind)
|
|
}
|
|
values := make(map[string]json.RawMessage)
|
|
for decoder.More() {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode %s key: %w", kind, err)
|
|
}
|
|
key, ok := token.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s object key must be a string", kind)
|
|
}
|
|
if _, exists := values[key]; exists {
|
|
return nil, fmt.Errorf("duplicate %s key %q", kind, key)
|
|
}
|
|
var raw json.RawMessage
|
|
if err := decoder.Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("decode %s %q: %w", kind, key, err)
|
|
}
|
|
values[key] = append(json.RawMessage(nil), raw...)
|
|
}
|
|
if _, err := decoder.Token(); err != nil {
|
|
return nil, fmt.Errorf("close %s object: %w", kind, err)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func decodeCanonicalObject(raw []byte, kind string, canonicalFields []string) (map[string]json.RawMessage, error) {
|
|
if err := rejectDuplicateObjectFields(raw); err != nil {
|
|
return nil, err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
fields, err := decodeUniqueObject(decoder, kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := requireJSONEOF(decoder); err != nil {
|
|
return nil, err
|
|
}
|
|
for field := range fields {
|
|
for _, canonical := range canonicalFields {
|
|
if field != canonical && strings.EqualFold(field, canonical) {
|
|
return nil, fmt.Errorf("%s field %q is a non-canonical alias of %q", kind, field, canonical)
|
|
}
|
|
}
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func decodeStringField(fields map[string]json.RawMessage, name string) (string, error) {
|
|
raw, exists := fields[name]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return "", nil
|
|
}
|
|
var value string
|
|
if err := decodeOneJSON(raw, &value); err != nil {
|
|
return "", fmt.Errorf("decode %s field: %w", name, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func decodeBoolField(fields map[string]json.RawMessage, name string) (*bool, error) {
|
|
raw, exists := fields[name]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
var value bool
|
|
if err := decodeOneJSON(raw, &value); err != nil {
|
|
return nil, fmt.Errorf("decode %s field: %w", name, err)
|
|
}
|
|
return &value, nil
|
|
}
|
|
|
|
func decodeStringSliceField(fields map[string]json.RawMessage, name string) ([]string, error) {
|
|
raw, exists := fields[name]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
var values []string
|
|
if err := decodeOneJSON(raw, &values); err != nil {
|
|
return nil, fmt.Errorf("decode %s field: %w", name, err)
|
|
}
|
|
result := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
normalized := strings.TrimSpace(value)
|
|
if normalized == "" {
|
|
return nil, fmt.Errorf("%s modality must not be empty", name)
|
|
}
|
|
if _, duplicate := seen[normalized]; duplicate {
|
|
return nil, fmt.Errorf("duplicate %s modality %q", name, normalized)
|
|
}
|
|
seen[normalized] = struct{}{}
|
|
result = append(result, normalized)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func decodeNonNegativeInt64Field(fields map[string]json.RawMessage, name string) (*int64, error) {
|
|
value, err := decodeNumberField(fields, name)
|
|
if err != nil || value == nil {
|
|
return nil, err
|
|
}
|
|
parsed, err := strconv.ParseInt(value.String(), 10, 64)
|
|
if err != nil || parsed < 0 {
|
|
return nil, fmt.Errorf("%s limit must be a non-negative integer", name)
|
|
}
|
|
return &parsed, nil
|
|
}
|
|
|
|
func decodeNumberField(fields map[string]json.RawMessage, name string) (*json.Number, error) {
|
|
raw, exists := fields[name]
|
|
if !exists || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return nil, nil
|
|
}
|
|
var decoded any
|
|
if err := decodeOneJSON(raw, &decoded); err != nil {
|
|
return nil, fmt.Errorf("decode %s field: %w", name, err)
|
|
}
|
|
value, ok := decoded.(json.Number)
|
|
if !ok {
|
|
return nil, fmt.Errorf("decode %s field: JSON number is required", name)
|
|
}
|
|
return &value, nil
|
|
}
|
|
|
|
func decodeOneJSON(raw []byte, destination any) error {
|
|
if err := rejectDuplicateObjectFields(raw); err != nil {
|
|
return err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(destination); err != nil {
|
|
return err
|
|
}
|
|
return requireJSONEOF(decoder)
|
|
}
|
|
|
|
func rejectDuplicateObjectFields(raw []byte) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber()
|
|
if err := scanUniqueJSONValue(decoder); err != nil {
|
|
return err
|
|
}
|
|
return requireJSONEOF(decoder)
|
|
}
|
|
|
|
func scanUniqueJSONValue(decoder *json.Decoder) error {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delimiter, isDelimiter := token.(json.Delim)
|
|
if !isDelimiter {
|
|
return nil
|
|
}
|
|
switch delimiter {
|
|
case '{':
|
|
seen := make(map[string]struct{})
|
|
for decoder.More() {
|
|
keyToken, err := decoder.Token()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key, ok := keyToken.(string)
|
|
if !ok {
|
|
return fmt.Errorf("JSON object key must be a string")
|
|
}
|
|
if _, exists := seen[key]; exists {
|
|
return fmt.Errorf("duplicate JSON object key %q", key)
|
|
}
|
|
seen[key] = struct{}{}
|
|
if err := scanUniqueJSONValue(decoder); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = decoder.Token()
|
|
return err
|
|
case '[':
|
|
for decoder.More() {
|
|
if err := scanUniqueJSONValue(decoder); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = decoder.Token()
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unexpected JSON delimiter %q", delimiter)
|
|
}
|
|
}
|
|
|
|
func requireJSONEOF(decoder *json.Decoder) error {
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
return fmt.Errorf("trailing JSON value is not allowed")
|
|
}
|
|
return fmt.Errorf("decode trailing JSON: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSnapshot(snapshot *Snapshot) error {
|
|
if snapshot == nil {
|
|
return fmt.Errorf("catalog snapshot is required")
|
|
}
|
|
for providerKey, provider := range snapshot.Providers {
|
|
if providerKey != provider.ID {
|
|
return fmt.Errorf("provider %q map key and id must match", providerKey)
|
|
}
|
|
if _, err := pricing.ProviderScopeKey(provider.ID); err != nil {
|
|
return fmt.Errorf("provider %q: %w", providerKey, err)
|
|
}
|
|
if strings.TrimSpace(provider.Name) == "" {
|
|
return fmt.Errorf("provider %q name is required", providerKey)
|
|
}
|
|
if provider.Models == nil {
|
|
return fmt.Errorf("provider %q models map is required", providerKey)
|
|
}
|
|
for modelKey, model := range provider.Models {
|
|
if modelKey != model.ID {
|
|
return fmt.Errorf("provider %q model %q map key and id must match", providerKey, modelKey)
|
|
}
|
|
if err := validateModelID(model.ID); err != nil {
|
|
return fmt.Errorf("provider %q model %q: %w", providerKey, modelKey, err)
|
|
}
|
|
if strings.TrimSpace(model.Name) == "" {
|
|
return fmt.Errorf("provider %q model %q name is required", providerKey, modelKey)
|
|
}
|
|
if err := validateModelCost(model.Cost); err != nil {
|
|
return fmt.Errorf("provider %q model %q: %w", providerKey, modelKey, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateModelID(id string) error {
|
|
if len(id) == 0 || len(id) > maxModelIDBytes {
|
|
return fmt.Errorf("model id must be 1 through %d bytes", maxModelIDBytes)
|
|
}
|
|
if strings.TrimSpace(id) != id {
|
|
return fmt.Errorf("model id must not have surrounding whitespace")
|
|
}
|
|
for _, character := range id {
|
|
if unicode.IsControl(character) {
|
|
return fmt.Errorf("model id must not contain control characters")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateModelCost(cost *ModelCost) error {
|
|
if cost == nil {
|
|
return nil
|
|
}
|
|
for _, price := range [...]pricing.Price{cost.Prices.Input, cost.Prices.Output, cost.Prices.CacheRead, cost.Prices.CacheWrite} {
|
|
if price.NanoUSDPerMillion < 0 {
|
|
return fmt.Errorf("price must be non-negative")
|
|
}
|
|
}
|
|
previousThreshold := int64(-1)
|
|
for _, tier := range cost.ContextTiers {
|
|
if tier.InputThresholdTokens < 0 || tier.InputThresholdTokens <= previousThreshold {
|
|
return fmt.Errorf("context tier thresholds must be non-negative and strictly increasing")
|
|
}
|
|
if !hasSetPrice(tier.Prices) {
|
|
return fmt.Errorf("context tier must set at least one price")
|
|
}
|
|
for _, price := range [...]pricing.Price{tier.Prices.Input, tier.Prices.Output, tier.Prices.CacheRead, tier.Prices.CacheWrite} {
|
|
if price.NanoUSDPerMillion < 0 {
|
|
return fmt.Errorf("context tier price must be non-negative")
|
|
}
|
|
}
|
|
previousThreshold = tier.InputThresholdTokens
|
|
}
|
|
for mode, prices := range cost.ModePrices {
|
|
if !mode.Valid() || mode == pricing.ModeStandard {
|
|
return fmt.Errorf("invalid non-standard pricing mode %q", mode)
|
|
}
|
|
if !hasSetPrice(prices) {
|
|
return fmt.Errorf("pricing mode %q must set at least one price", mode)
|
|
}
|
|
for _, price := range [...]pricing.Price{prices.Input, prices.Output, prices.CacheRead, prices.CacheWrite} {
|
|
if price.NanoUSDPerMillion < 0 {
|
|
return fmt.Errorf("pricing mode %q price must be non-negative", mode)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasSetPrice(prices pricing.Prices) bool {
|
|
return prices.Input.Set || prices.Output.Set || prices.CacheRead.Set || prices.CacheWrite.Set
|
|
}
|