feat: generate the known-models catalog and aigateway prices (#27146)

- Regenerates `prices.json` from models.dev. The seeder only upserts, so
existing deployments keep delisted models.
- Generate the frontend known-models catalog instead of hand-writing it.
`make gen/aibridge-prices` fetches models.dev once
- Moved patches to model definitions to separate `overrides.jq` which 
  handles both `claude-sonnet-4-5` 200k context and 'aliasing' Fable 5
  as Mythos 5.
- Editorial choices of selection, order, aliases, and reasoning defaults 
  live in `curation.json`.
- Adds golden join tests with one error case per validation, a
no-network drift test comparing curation to the checked-in artifact, and
pinned invariants for the Anthropic thinking-mode split (the wrong side
returns HTTP 400) and the sonnet-4-5 context pin.

Adding a model is now one `curation.json` entry plus `make
gen/aibridge-prices`, assuming it is present on models.dev.

> This PR was authored by Coder Agents on Cian's behalf.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Cian Johnston
2026-07-14 19:36:27 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 4d884c30e7
commit 8eaf4f507b
17 changed files with 1119 additions and 618 deletions
+150
View File
@@ -0,0 +1,150 @@
package main
import (
"cmp"
_ "embed"
"encoding/json"
"io"
"golang.org/x/xerrors"
)
// curationJSON is the checked-in editorial curation input for the frontend
// known-models catalog. Entry order within each provider controls suggestion
// order in the UI. Everything factual (display name, limits, pricing) is
// joined from models.dev at generation time; the curation file only carries
// editorial choices: which models to suggest, aliases, reasoning defaults,
// and overrides.
//
//go:embed curation.json
var curationJSON []byte
// curatedModel is one entry in curation.json.
type curatedModel struct {
ModelIdentifier string `json:"modelIdentifier"`
Aliases []string `json:"aliases"`
// DisplayName overrides the upstream `name` when set. Needed where
// upstream naming does not match what we want to show (for example
// "Claude Haiku 4.5 (latest)").
DisplayName string `json:"displayName"`
// ReasoningEffort is editorial, not from models.dev. Mutually
// exclusive with ThinkingBudgetTokens.
ReasoningEffort string `json:"reasoningEffort"`
// ThinkingBudgetTokens is Anthropic-only, for models that do not
// support adaptive thinking and use the legacy
// `thinking.budget_tokens` API instead.
ThinkingBudgetTokens int `json:"thinkingBudgetTokens"`
}
// catalogEntry matches the frontend KnownModel shape (knownModels/types.ts).
// Costs are flat USD per million tokens, straight from models.dev; tiered
// pricing such as context_over_200k is intentionally omitted.
type catalogEntry struct {
Provider string `json:"provider"`
ModelIdentifier string `json:"modelIdentifier"`
DisplayName string `json:"displayName"`
Aliases []string `json:"aliases"`
ContextLimit *int64 `json:"contextLimit,omitempty"`
MaxOutputTokens *int64 `json:"maxOutputTokens,omitempty"`
ReasoningEffort string `json:"reasoningEffort,omitempty"`
ThinkingBudgetTokens int `json:"thinkingBudgetTokens,omitempty"`
InputCost *float64 `json:"inputCost,omitempty"`
OutputCost *float64 `json:"outputCost,omitempty"`
CacheReadCost *float64 `json:"cacheReadCost,omitempty"`
CacheWriteCost *float64 `json:"cacheWriteCost,omitempty"`
}
// validReasoningEfforts are the values accepted for curatedModel.ReasoningEffort.
var validReasoningEfforts = map[string]bool{"low": true, "medium": true, "high": true}
// buildCatalog joins the curation file with the upstream models.dev payload
// and returns provider-keyed ordered entry lists.
func buildCatalog(upstream map[string]upstreamProvider, curation map[string][]curatedModel) (map[string][]catalogEntry, error) {
out := make(map[string][]catalogEntry, len(curation))
for providerID, curated := range curation {
provider, ok := upstream[providerID]
if !ok {
return nil, xerrors.Errorf("provider %q missing from upstream", providerID)
}
seenIdentifiers := make(map[string]bool, len(curated))
seenAliases := make(map[string]bool)
entries := make([]catalogEntry, 0, len(curated))
for _, c := range curated {
if c.ModelIdentifier == "" {
return nil, xerrors.Errorf("provider %q: entry with empty modelIdentifier", providerID)
}
if seenIdentifiers[c.ModelIdentifier] {
return nil, xerrors.Errorf("provider %q: duplicate modelIdentifier %q", providerID, c.ModelIdentifier)
}
seenIdentifiers[c.ModelIdentifier] = true
if c.ReasoningEffort != "" && !validReasoningEfforts[c.ReasoningEffort] {
return nil, xerrors.Errorf(`%s/%s: reasoningEffort %q is not one of "low", "medium", "high"`, providerID, c.ModelIdentifier, c.ReasoningEffort)
}
if c.ThinkingBudgetTokens < 0 {
return nil, xerrors.Errorf("%s/%s: thinkingBudgetTokens %d is negative", providerID, c.ModelIdentifier, c.ThinkingBudgetTokens)
}
if c.ReasoningEffort != "" && c.ThinkingBudgetTokens != 0 {
return nil, xerrors.Errorf("%s/%s: reasoningEffort and thinkingBudgetTokens are mutually exclusive", providerID, c.ModelIdentifier)
}
for _, alias := range c.Aliases {
if alias == "" {
return nil, xerrors.Errorf("%s/%s: empty-string alias", providerID, c.ModelIdentifier)
}
if seenAliases[alias] {
return nil, xerrors.Errorf("%s/%s: alias %q declared more than once in provider", providerID, c.ModelIdentifier, alias)
}
seenAliases[alias] = true
}
m, ok := provider.Models[c.ModelIdentifier]
if !ok {
return nil, xerrors.Errorf("%s/%s: model missing from upstream (patch it in via overrides.jq if intentional)", providerID, c.ModelIdentifier)
}
if !m.Cost.hasPricing() {
return nil, xerrors.Errorf("%s/%s: upstream model has no pricing data", providerID, c.ModelIdentifier)
}
if m.Limit.Context == nil || m.Limit.Output == nil {
return nil, xerrors.Errorf("%s/%s: upstream model missing limit.context or limit.output", providerID, c.ModelIdentifier)
}
displayName := cmp.Or(c.DisplayName, m.Name)
if displayName == "" {
return nil, xerrors.Errorf("%s/%s: no displayName override and upstream name is empty", providerID, c.ModelIdentifier)
}
aliases := c.Aliases
if aliases == nil {
aliases = []string{}
}
entries = append(entries, catalogEntry{
Provider: providerID,
ModelIdentifier: c.ModelIdentifier,
DisplayName: displayName,
Aliases: aliases,
ContextLimit: m.Limit.Context,
MaxOutputTokens: m.Limit.Output,
ReasoningEffort: c.ReasoningEffort,
ThinkingBudgetTokens: c.ThinkingBudgetTokens,
InputCost: m.Cost.Input,
OutputCost: m.Cost.Output,
CacheReadCost: m.Cost.CacheRead,
CacheWriteCost: m.Cost.CacheWrite,
})
}
// An alias resolving to a canonical identifier would make exact-alias
// lookup and canonical-id lookup disagree.
for alias := range seenAliases {
if seenIdentifiers[alias] {
return nil, xerrors.Errorf("alias %q duplicates a modelIdentifier in provider %q", alias, providerID)
}
}
out[providerID] = entries
}
return out, nil
}
func writeCatalog(w io.Writer, catalog map[string][]catalogEntry) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(catalog); err != nil {
return xerrors.Errorf("encode: %w", err)
}
return nil
}
+342
View File
@@ -0,0 +1,342 @@
package main
import (
"bytes"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// fixtureUpstream returns a small upstream payload covering the join cases:
// fully priced models with limits and a costless model.
func fixtureUpstream(t *testing.T) map[string]upstreamProvider {
t.Helper()
const upstreamJSON = `{
"anthropic": {
"models": {
"claude-fable-5": {
"name": "Claude Fable 5",
"limit": {"context": 1000000, "output": 128000},
"cost": {"input": 10, "output": 50, "cache_read": 1, "cache_write": 12.5}
},
"claude-mythos-5": {
"name": "Claude Mythos 5",
"limit": {"context": 1000000, "output": 128000},
"cost": {"input": 10, "output": 50, "cache_read": 1, "cache_write": 12.5}
},
"claude-costless": {
"name": "Claude Costless",
"limit": {"context": 200000, "output": 64000}
},
"claude-nameless": {
"name": "",
"limit": {"context": 200000, "output": 64000},
"cost": {"input": 1, "output": 5}
}
}
},
"openai": {
"models": {
"gpt-5.6-sol": {
"name": "GPT-5.6 Sol",
"limit": {"context": 1050000, "output": 128000},
"cost": {"input": 5, "output": 30, "cache_read": 0.5, "cache_write": 6.25}
},
"gpt-partial": {
"name": "GPT Partial",
"limit": {"context": 400000, "output": 128000},
"cost": {"input": 0.2, "output": 1.25}
},
"gpt-limitless": {
"name": "GPT Limitless",
"limit": {"context": 400000},
"cost": {"input": 0.2, "output": 1.25}
}
}
}
}`
var upstream map[string]upstreamProvider
require.NoError(t, json.Unmarshal([]byte(upstreamJSON), &upstream))
return upstream
}
func TestBuildCatalog(t *testing.T) {
t.Parallel()
curation := map[string][]curatedModel{
"openai": {
{ModelIdentifier: "gpt-5.6-sol", Aliases: []string{"gpt-5.6"}, ReasoningEffort: "medium"},
{ModelIdentifier: "gpt-partial"},
},
"anthropic": {
{ModelIdentifier: "claude-fable-5", ReasoningEffort: "high"},
{ModelIdentifier: "claude-mythos-5", DisplayName: "Mythos 5 Override", ThinkingBudgetTokens: 8192},
},
}
catalog, err := buildCatalog(fixtureUpstream(t), curation)
require.NoError(t, err)
var buf bytes.Buffer
require.NoError(t, writeCatalog(&buf, catalog))
const want = `{
"anthropic": [
{
"provider": "anthropic",
"modelIdentifier": "claude-fable-5",
"displayName": "Claude Fable 5",
"aliases": [],
"contextLimit": 1000000,
"maxOutputTokens": 128000,
"reasoningEffort": "high",
"inputCost": 10,
"outputCost": 50,
"cacheReadCost": 1,
"cacheWriteCost": 12.5
},
{
"provider": "anthropic",
"modelIdentifier": "claude-mythos-5",
"displayName": "Mythos 5 Override",
"aliases": [],
"contextLimit": 1000000,
"maxOutputTokens": 128000,
"thinkingBudgetTokens": 8192,
"inputCost": 10,
"outputCost": 50,
"cacheReadCost": 1,
"cacheWriteCost": 12.5
}
],
"openai": [
{
"provider": "openai",
"modelIdentifier": "gpt-5.6-sol",
"displayName": "GPT-5.6 Sol",
"aliases": [
"gpt-5.6"
],
"contextLimit": 1050000,
"maxOutputTokens": 128000,
"reasoningEffort": "medium",
"inputCost": 5,
"outputCost": 30,
"cacheReadCost": 0.5,
"cacheWriteCost": 6.25
},
{
"provider": "openai",
"modelIdentifier": "gpt-partial",
"displayName": "GPT Partial",
"aliases": [],
"contextLimit": 400000,
"maxOutputTokens": 128000,
"inputCost": 0.2,
"outputCost": 1.25
}
]
}
`
require.Equal(t, want, buf.String())
}
func TestBuildCatalogErrors(t *testing.T) {
t.Parallel()
cases := []struct {
name string
curation map[string][]curatedModel
wantErr string
}{
{
name: "MissingUpstreamModel",
curation: map[string][]curatedModel{
"openai": {{ModelIdentifier: "gpt-nonexistent"}},
},
wantErr: "model missing from upstream",
},
{
name: "NoCostBlock",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-costless"}},
},
wantErr: "no pricing data",
},
{
name: "MissingUpstreamLimit",
curation: map[string][]curatedModel{
"openai": {{ModelIdentifier: "gpt-limitless"}},
},
wantErr: "missing limit.context or limit.output",
},
{
name: "EmptyUpstreamName",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-nameless"}},
},
wantErr: "upstream name is empty",
},
{
name: "EffortAndBudgetBothSet",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-fable-5", ReasoningEffort: "high", ThinkingBudgetTokens: 8192}},
},
wantErr: "mutually exclusive",
},
{
name: "InvalidReasoningEffort",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-fable-5", ReasoningEffort: "maximum"}},
},
wantErr: "is not one of",
},
{
name: "NegativeThinkingBudget",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-fable-5", ThinkingBudgetTokens: -1}},
},
wantErr: "is negative",
},
{
name: "DuplicateModelIdentifier",
curation: map[string][]curatedModel{
"anthropic": {
{ModelIdentifier: "claude-fable-5"},
{ModelIdentifier: "claude-fable-5"},
},
},
wantErr: "duplicate modelIdentifier",
},
{
name: "DuplicateAlias",
curation: map[string][]curatedModel{
"anthropic": {
{ModelIdentifier: "claude-fable-5", Aliases: []string{"claude-latest"}},
{ModelIdentifier: "claude-mythos-5", Aliases: []string{"claude-latest"}},
},
},
wantErr: "declared more than once",
},
{
name: "EmptyAlias",
curation: map[string][]curatedModel{
"anthropic": {{ModelIdentifier: "claude-fable-5", Aliases: []string{""}}},
},
wantErr: "empty-string alias",
},
{
name: "AliasShadowsModelIdentifier",
curation: map[string][]curatedModel{
"anthropic": {
{ModelIdentifier: "claude-fable-5", Aliases: []string{"claude-mythos-5"}},
{ModelIdentifier: "claude-mythos-5"},
},
},
wantErr: "duplicates a modelIdentifier",
},
{
name: "MissingProvider",
curation: map[string][]curatedModel{
"google": {{ModelIdentifier: "gemini"}},
},
wantErr: `provider "google" missing`,
},
{
name: "EmptyModelIdentifier",
curation: map[string][]curatedModel{
"openai": {{}},
},
wantErr: "empty modelIdentifier",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := buildCatalog(fixtureUpstream(t), tc.curation)
require.Error(t, err)
require.Contains(t, err.Error(), tc.wantErr)
})
}
}
// TestCurationMatchesGeneratedCatalog is a drift test: the editorial fields
// (per provider, in order) in the embedded curation.json must exactly match
// their projection in the checked-in generated frontend catalog. Fails when
// curation.json changes without running `make gen/aibridge-prices`.
func TestCurationMatchesGeneratedCatalog(t *testing.T) {
t.Parallel()
curation := embeddedCuration(t)
data, err := os.ReadFile("../../site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json")
require.NoError(t, err)
var generated map[string][]catalogEntry
require.NoError(t, json.Unmarshal(data, &generated))
// editorial is the curation-owned projection of an entry. displayName is
// only compared when the curation sets an override; otherwise it comes
// from upstream and is not the curation's to pin.
type editorial struct {
ModelIdentifier string
Aliases []string
DisplayName string
ReasoningEffort string
ThinkingBudgetTokens int
}
curatedProjection := make(map[string][]editorial, len(curation))
for providerID, entries := range curation {
projected := make([]editorial, 0, len(entries))
for _, c := range entries {
aliases := c.Aliases
if aliases == nil {
aliases = []string{}
}
projected = append(projected, editorial{
ModelIdentifier: c.ModelIdentifier,
Aliases: aliases,
DisplayName: c.DisplayName,
ReasoningEffort: c.ReasoningEffort,
ThinkingBudgetTokens: c.ThinkingBudgetTokens,
})
}
curatedProjection[providerID] = projected
}
generatedProjection := make(map[string][]editorial, len(generated))
for providerID, entries := range generated {
curated := map[string]curatedModel{}
for _, c := range curation[providerID] {
curated[c.ModelIdentifier] = c
}
projected := make([]editorial, 0, len(entries))
for _, e := range entries {
displayName := ""
if curated[e.ModelIdentifier].DisplayName != "" {
displayName = e.DisplayName
}
projected = append(projected, editorial{
ModelIdentifier: e.ModelIdentifier,
Aliases: e.Aliases,
DisplayName: displayName,
ReasoningEffort: e.ReasoningEffort,
ThinkingBudgetTokens: e.ThinkingBudgetTokens,
})
}
generatedProjection[providerID] = projected
}
require.Equal(t, curatedProjection, generatedProjection,
"curation.json and knownModelsGenerated.json disagree; run `make gen/aibridge-prices`")
}
func embeddedCuration(t *testing.T) map[string][]curatedModel {
t.Helper()
var curation map[string][]curatedModel
require.NoError(t, json.Unmarshal(curationJSON, &curation))
return curation
}
+81
View File
@@ -0,0 +1,81 @@
{
"openai": [
{
"modelIdentifier": "gpt-5.6-sol",
"aliases": ["gpt-5.6"],
"reasoningEffort": "medium"
},
{
"modelIdentifier": "gpt-5.6-terra",
"reasoningEffort": "medium"
},
{
"modelIdentifier": "gpt-5.6-luna",
"reasoningEffort": "medium"
},
{
"modelIdentifier": "gpt-5.5",
"reasoningEffort": "medium"
},
{
"modelIdentifier": "gpt-5.5-pro",
"reasoningEffort": "high"
},
{
"modelIdentifier": "gpt-5.4"
},
{
"modelIdentifier": "gpt-5.4-mini",
"reasoningEffort": "medium"
},
{
"modelIdentifier": "gpt-5.4-nano"
},
{
"modelIdentifier": "gpt-5.3-codex",
"reasoningEffort": "medium"
}
],
"anthropic": [
{
"modelIdentifier": "claude-fable-5",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-mythos-5",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-opus-4-8",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-opus-4-7",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-opus-4-6",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-sonnet-5",
"reasoningEffort": "high"
},
{
"modelIdentifier": "claude-sonnet-4-6",
"reasoningEffort": "medium"
},
{
"modelIdentifier": "claude-haiku-4-5",
"aliases": ["claude-haiku-4-5-20251001"],
"displayName": "Claude Haiku 4.5",
"thinkingBudgetTokens": 8192
},
{
"modelIdentifier": "claude-sonnet-4-5",
"aliases": ["claude-sonnet-4-5-20250929"],
"displayName": "Claude Sonnet 4.5",
"thinkingBudgetTokens": 8192
}
]
}
+73 -46
View File
@@ -1,37 +1,30 @@
// aibridgepricesgen fetches model pricing from models.dev and writes a JSON
// seed file consumable by the AI Bridge cost-control loader. Output is sorted
// by (provider, model) so regenerations produce minimal diffs.
// aibridgepricesgen converts a models.dev api.json snapshot into generated
// artifacts, selected by -format:
//
// Run via the gen/aibridge-prices Make target. Kept out of `make gen` because
// the output depends on live upstream data; refreshing prices should land in
// dedicated, reviewable commits rather than appearing as drift on unrelated
// gen runs.
// - "prices": a JSON seed file consumable by the AI Gateway cost-control
// loader, sorted by (provider, model) so regenerations produce minimal
// diffs.
// - "catalog": the frontend known-models JSON, joining the snapshot with
// the editorial curation in curation.json and preserving its entry order.
//
// Run via the gen/aibridge-prices Make target, which fetches and patches the
// snapshot (_gen/models-dev.json). Kept out of `make gen` because the output
// depends on live upstream data; refreshing prices should land in dedicated,
// reviewable commits rather than appearing as drift on unrelated gen runs.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"math"
"net/http"
"os"
"sort"
"time"
"golang.org/x/xerrors"
)
const (
sourceURL = "https://models.dev/api.json"
fetchTimeout = 30 * time.Second
// Cap the upstream body read. The current api.json is ~2 MiB, so 100
// MiB is pure defense-in-depth against a misbehaving upstream eating
// arbitrary memory on developer or CI machines. An overflow surfaces
// as a JSON parse error (LimitReader truncates silently at the cap).
maxBodyBytes = 100 << 20
)
// supportedProviders lists the providers we ship prices for. Adding a
// provider here is enough to include it on the next regeneration.
var supportedProviders = []string{"anthropic", "openai"}
@@ -42,10 +35,18 @@ type upstreamProvider struct {
}
type upstreamModel struct {
Cost *upstreamCost `json:"cost"`
Name string `json:"name"`
Limit upstreamLimit `json:"limit"`
Cost *upstreamCost `json:"cost"`
}
// Pointer fields in upstreamLimit and upstreamCost distinguish "key absent"
// (nil) from "key present and zero" (0).
type upstreamLimit struct {
Context *int64 `json:"context"`
Output *int64 `json:"output"`
}
// Pointers distinguish "key absent" (nil) from "key present and zero" (0).
type upstreamCost struct {
Input *float64 `json:"input"`
Output *float64 `json:"output"`
@@ -80,17 +81,51 @@ type priceRow struct {
}
func main() {
if err := run(); err != nil {
format := flag.String("format", "prices", `output format: "prices" (cost-control seed) or "catalog" (frontend known-models JSON)`)
upstreamPath := flag.String("upstream", "", "path to a models.dev api.json snapshot (required)")
flag.Parse()
if err := run(*format, *upstreamPath); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "aibridgepricesgen: %v\n", err)
os.Exit(1)
}
}
func run() error {
upstream, err := fetch()
if err != nil {
return xerrors.Errorf("fetch %s: %w", sourceURL, err)
func run(format, upstreamPath string) error {
// Validate flags before touching the filesystem so a typo fails fast.
switch format {
case "prices", "catalog":
default:
return xerrors.Errorf(`unknown -format %q (want "prices" or "catalog")`, format)
}
if upstreamPath == "" {
return xerrors.New("-upstream is required; run via `make gen/aibridge-prices`, which fetches and patches the snapshot")
}
upstream, err := readUpstream(upstreamPath)
if err != nil {
return xerrors.Errorf("read %s: %w", upstreamPath, err)
}
if format == "catalog" {
return runCatalog(upstream)
}
return runPrices(upstream)
}
// readUpstream loads a models.dev api.json snapshot from disk, typically the
// Makefile's _gen/models-dev.json (fetched once and patched by overrides.jq).
func readUpstream(path string) (map[string]upstreamProvider, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var upstream map[string]upstreamProvider
if err := json.Unmarshal(data, &upstream); err != nil {
return nil, xerrors.Errorf("parse: %w", err)
}
return upstream, nil
}
func runPrices(upstream map[string]upstreamProvider) error {
rows, err := convert(upstream, supportedProviders)
if err != nil {
return err
@@ -105,28 +140,20 @@ func run() error {
return nil
}
func fetch() (map[string]upstreamProvider, error) {
ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
func runCatalog(upstream map[string]upstreamProvider) error {
var curation map[string][]curatedModel
if err := json.Unmarshal(curationJSON, &curation); err != nil {
return xerrors.Errorf("parse embedded curation.json: %w", err)
}
catalog, err := buildCatalog(upstream, curation)
if err != nil {
return nil, err
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
if err := writeCatalog(os.Stdout, catalog); err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, xerrors.Errorf("status %d", resp.StatusCode)
}
var data map[string]upstreamProvider
if err := json.NewDecoder(io.LimitReader(resp.Body, maxBodyBytes)).Decode(&data); err != nil {
return nil, xerrors.Errorf("parse: %w", err)
}
return data, nil
_, _ = fmt.Fprintf(os.Stderr, "aibridgepricesgen: wrote catalog for %d provider(s)\n", len(catalog))
return nil
}
// convert flattens the upstream map into table-shaped rows for the configured
+32
View File
@@ -0,0 +1,32 @@
# Patches applied to the raw models.dev api.json before aibridgepricesgen
# consumes it. The Makefile pipes the fetched payload through this filter
# (jq -f scripts/aibridgepricesgen/overrides.jq) and both generated outputs
# (prices.json and knownModelsGenerated.json) read the patched snapshot.
#
# Every patch guards its assumption about upstream, so a stale override
# fails the pipeline loudly instead of silently patching nothing.
# claude-sonnet-4-5: models.dev advertises a 1M-token context window, which
# is incorrect. Anthropic retired the 1M context window beta on May 1st,
# 2026. Ref: https://platform.claude.com/docs/en/about-claude/models/overview
if .anthropic.models | has("claude-sonnet-4-5") then
.anthropic.models."claude-sonnet-4-5".limit.context = 200000
else
error("overrides.jq: claude-sonnet-4-5 gone from upstream; drop or update its context pin")
end
# claude-mythos-5: not listed on models.dev. Anthropic documents it as sharing
# claude-fable-5's specs and pricing, so inject it as a copy with its own
# id and display name.
# Ref: https://platform.claude.com/docs/en/about-claude/pricing#model-pricing
| if (.anthropic.models | has("claude-fable-5") | not) then
error("overrides.jq: claude-fable-5 gone from upstream; the claude-mythos-5 copy has no source")
elif (.anthropic.models | has("claude-mythos-5")) then
error("overrides.jq: claude-mythos-5 now present upstream; drop the injection")
else
.anthropic.models."claude-mythos-5" = (
.anthropic.models."claude-fable-5"
| .id = "claude-mythos-5"
| .name = "Claude Mythos 5"
)
end