From 8b04c84925915ecedf4366be14d679d165484332 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 07:37:10 +0000 Subject: [PATCH 1/4] Initial plan From 7977a1155634949e7cc7535828df45e35e8f2908 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 07:47:32 +0000 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20consolidate=20duplicated=20mode?= =?UTF-8?q?l-normalization=20helpers=20(pkg/cli=20=E2=86=94=20pkg/modelsde?= =?UTF-8?q?v)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export NormalizeProvider and NormalizeComparableModelID from pkg/modelsdev/catalog.go - Remove byte-for-byte duplicate normalizeCatalogProvider / normalizeComparableModelID from pkg/cli/model_costs.go and replace all call sites with the modelsdev versions - Add NormalizeProvider / NormalizeComparableModelID unit tests in catalog_test.go - Update TestNormalizeCatalogProvider in model_costs_test.go to use modelsdev.NormalizeProvider Closes #43270 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/model_costs.go | 25 ++++++------------------- pkg/cli/model_costs_test.go | 4 +++- pkg/modelsdev/catalog.go | 18 +++++++++++------- pkg/modelsdev/catalog_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/pkg/cli/model_costs.go b/pkg/cli/model_costs.go index cc4f2370381..545c56eabfb 100644 --- a/pkg/cli/model_costs.go +++ b/pkg/cli/model_costs.go @@ -81,9 +81,9 @@ func initModelPrices() { func findModelPricing(provider, model string) (map[string]float64, bool) { initModelPrices() - normalizedProvider := normalizeCatalogProvider(provider) + normalizedProvider := modelsdev.NormalizeProvider(provider) normalizedModel := strings.ToLower(strings.TrimSpace(model)) - comparableModel := normalizeComparableModelID(normalizedModel) + comparableModel := modelsdev.NormalizeComparableModelID(normalizedModel) if normalizedModel == "" { //nolint:tolowerequalfold return nil, false } @@ -92,10 +92,10 @@ func findModelPricing(provider, model string) (map[string]float64, bool) { if !strings.Contains(fullID, "/") && normalizedProvider != "" { fullID = normalizedProvider + "/" + normalizedModel } - comparableFullID := normalizeComparableModelID(fullID) + comparableFullID := modelsdev.NormalizeComparableModelID(fullID) for _, record := range modelPriceRecords { - if (fullID != "" && record.id == fullID) || (comparableFullID != "" && normalizeComparableModelID(record.id) == comparableFullID) { + if (fullID != "" && record.id == fullID) || (comparableFullID != "" && modelsdev.NormalizeComparableModelID(record.id) == comparableFullID) { modelCostsLog.Printf("Exact pricing match: provider=%s, model=%s -> %s", provider, model, record.id) return record.pricing, true } @@ -107,7 +107,7 @@ func findModelPricing(provider, model string) (map[string]float64, bool) { bestGenericLen := -1 for _, record := range modelPriceRecords { - comparableRecordModel := normalizeComparableModelID(record.model) + comparableRecordModel := modelsdev.NormalizeComparableModelID(record.model) if record.model == normalizedModel || comparableRecordModel == comparableModel { if normalizedProvider != "" && record.provider == normalizedProvider { return record.pricing, true @@ -142,19 +142,6 @@ func findModelPricing(provider, model string) (map[string]float64, bool) { return nil, false } -func normalizeCatalogProvider(provider string) string { - switch strings.ToLower(strings.TrimSpace(provider)) { - case "github", "copilot", "github_models": - return "github-copilot" - default: - return strings.ToLower(strings.TrimSpace(provider)) - } -} - -func normalizeComparableModelID(value string) string { - return strings.NewReplacer(".", "-", "_", "-").Replace(strings.ToLower(strings.TrimSpace(value))) -} - func usdToAIC(usd float64) float64 { return usd / 0.01 } @@ -167,7 +154,7 @@ func computeModelInferenceCostUSD(provider, model string, inputTokens, outputTok input := inputTokens cacheRead := cacheReadTokens - if cacheRead > 0 && providerIncludesCacheReadsInInput(normalizeCatalogProvider(provider)) { + if cacheRead > 0 && providerIncludesCacheReadsInInput(modelsdev.NormalizeProvider(provider)) { input = max(inputTokens-cacheReadTokens, 0) } diff --git a/pkg/cli/model_costs_test.go b/pkg/cli/model_costs_test.go index 5c65cf64a27..2157e567c9f 100644 --- a/pkg/cli/model_costs_test.go +++ b/pkg/cli/model_costs_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/modelsdev" ) func TestFindModelPricing(t *testing.T) { @@ -40,7 +42,7 @@ func TestNormalizeCatalogProvider(t *testing.T) { name = "" } t.Run(name, func(t *testing.T) { - got := normalizeCatalogProvider(tt.input) + got := modelsdev.NormalizeProvider(tt.input) assert.Equal(t, tt.want, got) }) } diff --git a/pkg/modelsdev/catalog.go b/pkg/modelsdev/catalog.go index dd1582c4cd8..4445a5ad975 100644 --- a/pkg/modelsdev/catalog.go +++ b/pkg/modelsdev/catalog.go @@ -59,13 +59,13 @@ func FindPricing(ctx context.Context, provider, model string) (map[string]float6 return nil, false } - normalizedProvider := normalizeProvider(provider) + normalizedProvider := NormalizeProvider(provider) trimmedModel := strings.TrimSpace(model) if trimmedModel == "" { return nil, false } normalizedModel := strings.ToLower(trimmedModel) - comparableModel := normalizeComparableModelID(normalizedModel) + comparableModel := NormalizeComparableModelID(normalizedModel) log.Printf("FindPricing: looking up provider=%q model=%q", normalizedProvider, normalizedModel) @@ -78,7 +78,7 @@ func FindPricing(ctx context.Context, provider, model string) (map[string]float6 } // Comparable (dot/underscore-normalized) model ID match. for mn, pricing := range providerModels { - if normalizeComparableModelID(mn) == comparableModel { + if NormalizeComparableModelID(mn) == comparableModel { log.Printf("FindPricing: provider-scoped comparable match %q for %q", mn, normalizedModel) return pricing, true } @@ -93,7 +93,7 @@ func FindPricing(ctx context.Context, provider, model string) (map[string]float6 return pricing, true } for mn, pricing := range providerModels { - if normalizeComparableModelID(mn) == comparableModel { + if NormalizeComparableModelID(mn) == comparableModel { log.Printf("FindPricing: cross-provider comparable match %q for %q", mn, normalizedModel) return pricing, true } @@ -163,7 +163,7 @@ func parseCatalog(data []byte) (pricingCache, error) { parsed := make(pricingCache) for providerName, provider := range raw.Providers { - normalizedProvider := normalizeProvider(providerName) + normalizedProvider := NormalizeProvider(providerName) if normalizedProvider == "" { continue } @@ -213,7 +213,9 @@ func parseCostMap(raw map[string]json.RawMessage) map[string]float64 { return result } -func normalizeProvider(provider string) string { +// NormalizeProvider maps provider aliases (e.g. "github", "copilot", "github_models") +// to their canonical form ("github-copilot") and lower-cases all other values. +func NormalizeProvider(provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { case "github", "copilot", "github_models": return "github-copilot" @@ -222,6 +224,8 @@ func normalizeProvider(provider string) string { } } -func normalizeComparableModelID(value string) string { +// NormalizeComparableModelID lower-cases the value and replaces "." and "_" with "-" +// so that model IDs differing only in those separators compare equal. +func NormalizeComparableModelID(value string) string { return strings.NewReplacer(".", "-", "_", "-").Replace(strings.ToLower(strings.TrimSpace(value))) } diff --git a/pkg/modelsdev/catalog_test.go b/pkg/modelsdev/catalog_test.go index 2a3a08b6d67..63ed5a4b95a 100644 --- a/pkg/modelsdev/catalog_test.go +++ b/pkg/modelsdev/catalog_test.go @@ -146,3 +146,36 @@ func TestFindPricing(t *testing.T) { assert.Nil(t, pricing) }) } + +func TestNormalizeProvider(t *testing.T) { + cases := []struct{ input, want string }{ + {"github", "github-copilot"}, + {"copilot", "github-copilot"}, + {"github_models", "github-copilot"}, + {"GITHUB_MODELS", "github-copilot"}, + {"anthropic", "anthropic"}, + {"OpenAI", "openai"}, + {" Anthropic ", "anthropic"}, + {"", ""}, + } + for _, tc := range cases { + t.Run(tc.input+"->"+tc.want, func(t *testing.T) { + assert.Equal(t, tc.want, NormalizeProvider(tc.input)) + }) + } +} + +func TestNormalizeComparableModelID(t *testing.T) { + cases := []struct{ input, want string }{ + {"claude-sonnet-4.6", "claude-sonnet-4-6"}, + {"gpt_4o", "gpt-4o"}, + {"GPT-4O", "gpt-4o"}, + {" claude.3 ", "claude-3"}, + {"", ""}, + } + for _, tc := range cases { + t.Run(tc.input+"->"+tc.want, func(t *testing.T) { + assert.Equal(t, tc.want, NormalizeComparableModelID(tc.input)) + }) + } +} From 3c3bab32902a4538455a5e70132df02dc9cf55de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:38:00 +0000 Subject: [PATCH 3/4] refactor: rename test and hoist modelIDReplacer to package-level var Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/model_costs_test.go | 2 +- pkg/modelsdev/catalog.go | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/cli/model_costs_test.go b/pkg/cli/model_costs_test.go index 2157e567c9f..c4526a4edd9 100644 --- a/pkg/cli/model_costs_test.go +++ b/pkg/cli/model_costs_test.go @@ -23,7 +23,7 @@ func TestComputeModelInferenceAIC(t *testing.T) { assert.InDelta(t, 0.54825, aic, 1e-9) } -func TestNormalizeCatalogProvider(t *testing.T) { +func TestNormalizeProvider(t *testing.T) { tests := []struct { input string want string diff --git a/pkg/modelsdev/catalog.go b/pkg/modelsdev/catalog.go index 4445a5ad975..dcb4b50a257 100644 --- a/pkg/modelsdev/catalog.go +++ b/pkg/modelsdev/catalog.go @@ -22,6 +22,10 @@ const ( // catalogURL is a variable so tests can override it with a local HTTP server. var catalogURL = "https://models.dev/catalog.json" +// modelIDReplacer normalises separator characters in model IDs so that IDs +// differing only in ".", "_", or "-" compare equal. +var modelIDReplacer = strings.NewReplacer(".", "-", "_", "-") + var log = logger.New("modelsdev:catalog") // rawCatalog mirrors the top-level models.dev catalog JSON structure. @@ -227,5 +231,5 @@ func NormalizeProvider(provider string) string { // NormalizeComparableModelID lower-cases the value and replaces "." and "_" with "-" // so that model IDs differing only in those separators compare equal. func NormalizeComparableModelID(value string) string { - return strings.NewReplacer(".", "-", "_", "-").Replace(strings.ToLower(strings.TrimSpace(value))) + return modelIDReplacer.Replace(strings.ToLower(strings.TrimSpace(value))) } From f88f40f51e85889724cd61cec7d9eeccc5a7b5e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:38:28 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20normalize=20spelling=20in=20modelIDR?= =?UTF-8?q?eplacer=20doc=20comment=20(normalises=20=E2=86=92=20normalizes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/modelsdev/catalog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/modelsdev/catalog.go b/pkg/modelsdev/catalog.go index dcb4b50a257..d381be13c32 100644 --- a/pkg/modelsdev/catalog.go +++ b/pkg/modelsdev/catalog.go @@ -22,7 +22,7 @@ const ( // catalogURL is a variable so tests can override it with a local HTTP server. var catalogURL = "https://models.dev/catalog.json" -// modelIDReplacer normalises separator characters in model IDs so that IDs +// modelIDReplacer normalizes separator characters in model IDs so that IDs // differing only in ".", "_", or "-" compare equal. var modelIDReplacer = strings.NewReplacer(".", "-", "_", "-")