From e84fd3af994b2375426648c4ce116900c5a50737 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Tue, 30 Jun 2026 08:26:53 +0530 Subject: [PATCH 1/2] feat(catalog): centralize model policy helpers --- catalog/model_policy.go | 324 +++++++++++++++++++++++++++++++++++ catalog/model_policy_test.go | 139 +++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 catalog/model_policy.go create mode 100644 catalog/model_policy_test.go diff --git a/catalog/model_policy.go b/catalog/model_policy.go new file mode 100644 index 0000000..dc22b96 --- /dev/null +++ b/catalog/model_policy.go @@ -0,0 +1,324 @@ +package catalog + +import ( + "sort" + "strings" + + "github.com/GrayCodeAI/eyrie/catalog/registry" +) + +// ModelCostTier is a relative cost band for model selection. +type ModelCostTier int + +const ( + // CostTierCheap is appropriate for short, low-risk, or summarization work. + CostTierCheap ModelCostTier = iota + // CostTierMid is the default balanced cost band. + CostTierMid + // CostTierExpensive is appropriate for complex planning or generation work. + CostTierExpensive +) + +// ModelRole identifies the purpose of a model in a multi-model workflow. +type ModelRole string + +const ( + ModelRolePlanner ModelRole = "planner" + ModelRoleCoder ModelRole = "coder" + ModelRoleReviewer ModelRole = "reviewer" + ModelRoleCommit ModelRole = "commit" +) + +// ModelRoleAssignments maps workflow roles to concrete model IDs. +type ModelRoleAssignments struct { + Planner string `json:"planner,omitempty"` + Coder string `json:"coder,omitempty"` + Reviewer string `json:"reviewer,omitempty"` + Commit string `json:"commit,omitempty"` +} + +// ProviderDefaultModelV1 returns the provider's first catalog model. +func ProviderDefaultModelV1(compiled *CompiledCatalogV1, provider, fallback string) string { + models := ModelEntriesForProvider(compiled, provider) + if len(models) == 0 { + return fallback + } + if model := strings.TrimSpace(models[0].ID); model != "" { + return model + } + return fallback +} + +// PreferredProviderModelV1 returns the preferred model for provider and tier. +func PreferredProviderModelV1(compiled *CompiledCatalogV1, provider string, tier ModelTier, fallback string) string { + switch tier { + case TierHaiku: + return CheapestModelForProviderV1(compiled, provider, fallback) + case TierOpus: + return MostExpensiveModelForProviderV1(compiled, provider, fallback) + default: + return middleModelForProviderV1(compiled, provider, fallback) + } +} + +// CheapestModelForProviderV1 returns the lowest known input-priced model for a provider. +func CheapestModelForProviderV1(compiled *CompiledCatalogV1, provider, fallback string) string { + models := ModelEntriesForProvider(compiled, provider) + if len(models) == 0 { + return fallback + } + best := models[0] + foundPriced := false + for _, model := range models { + if strings.TrimSpace(model.ID) == "" { + continue + } + if model.InputPricePer1M <= 0 { + continue + } + if !foundPriced || model.InputPricePer1M < best.InputPricePer1M { + best = model + foundPriced = true + } + } + if id := strings.TrimSpace(best.ID); id != "" { + return id + } + return fallback +} + +// MostExpensiveModelForProviderV1 returns the highest known input-priced model for a provider. +func MostExpensiveModelForProviderV1(compiled *CompiledCatalogV1, provider, fallback string) string { + models := ModelEntriesForProvider(compiled, provider) + if len(models) == 0 { + return fallback + } + best := models[0] + for _, model := range models[1:] { + if strings.TrimSpace(model.ID) == "" { + continue + } + if model.InputPricePer1M > best.InputPricePer1M { + best = model + } + } + if id := strings.TrimSpace(best.ID); id != "" { + return id + } + return fallback +} + +// ModelCostTierOf resolves a model's cost tier from catalog family and pricing data. +func ModelCostTierOf(compiled *CompiledCatalogV1, modelName string) ModelCostTier { + if tier, ok := costTierFromCatalogFamily(compiled, modelName); ok { + return mapModelTierToCostTier(tier) + } + if tier, ok := costTierFromCatalogPricing(compiled, modelName); ok { + return tier + } + return costTierFromModelName(modelName) +} + +// ProviderForModelV1 returns the canonical owner provider for modelName. +func ProviderForModelV1(compiled *CompiledCatalogV1, modelName string) string { + if compiled == nil { + return "" + } + canonical := strings.TrimSpace(modelName) + if resolved, ok := compiled.CanonicalModelForAliasOrID(canonical); ok { + canonical = resolved + } + model := compiled.ModelsByID[canonical] + return CanonicalProviderID(model.ProviderID) +} + +// AllModelProvidersV1 lists canonical model owner providers in the catalog. +func AllModelProvidersV1(compiled *CompiledCatalogV1) []string { + if compiled == nil { + return nil + } + seen := map[string]bool{} + providers := make([]string, 0, len(compiled.ModelsByID)) + for _, model := range compiled.ModelsByID { + provider := CanonicalProviderID(model.ProviderID) + if provider == "" || seen[provider] { + continue + } + seen[provider] = true + providers = append(providers, provider) + } + sort.Strings(providers) + return providers +} + +// DefaultModelRolesV1 uses the primary model for interactive roles and the cheapest +// same-provider model for commit/summarization work when catalog data is available. +func DefaultModelRolesV1(compiled *CompiledCatalogV1, primaryModel string) ModelRoleAssignments { + primaryModel = strings.TrimSpace(primaryModel) + commit := primaryModel + if provider := ProviderForModelV1(compiled, primaryModel); provider != "" { + commit = CheapestModelForProviderV1(compiled, provider, primaryModel) + } + return ModelRoleAssignments{ + Planner: primaryModel, + Coder: primaryModel, + Reviewer: primaryModel, + Commit: commit, + } +} + +// ModelForRoleV1 resolves a role assignment with coder then catalog fallback. +func ModelForRoleV1(compiled *CompiledCatalogV1, roles ModelRoleAssignments, role ModelRole) string { + var model string + switch role { + case ModelRolePlanner: + model = roles.Planner + case ModelRoleCoder: + model = roles.Coder + case ModelRoleReviewer: + model = roles.Reviewer + case ModelRoleCommit: + model = roles.Commit + } + if strings.TrimSpace(model) != "" { + return model + } + if strings.TrimSpace(roles.Coder) != "" { + return roles.Coder + } + return PrimaryModelV1(compiled) +} + +// PrimaryModelV1 returns a stable best-effort model from chat-preferred providers. +func PrimaryModelV1(compiled *CompiledCatalogV1) string { + for _, provider := range registry.ChatProviderPreferenceOrder() { + if model := ProviderDefaultModelV1(compiled, provider, ""); model != "" { + return model + } + } + for _, provider := range AllModelProvidersV1(compiled) { + if model := ProviderDefaultModelV1(compiled, provider, ""); model != "" { + return model + } + } + return "" +} + +func middleModelForProviderV1(compiled *CompiledCatalogV1, provider, fallback string) string { + models := ModelEntriesForProvider(compiled, provider) + if len(models) == 0 { + return fallback + } + priced := make([]ModelCatalogEntry, 0, len(models)) + for _, model := range models { + if strings.TrimSpace(model.ID) != "" && model.InputPricePer1M > 0 { + priced = append(priced, model) + } + } + if len(priced) == 0 { + return ProviderDefaultModelV1(compiled, provider, fallback) + } + sort.SliceStable(priced, func(i, j int) bool { + if priced[i].InputPricePer1M == priced[j].InputPricePer1M { + return priced[i].ID < priced[j].ID + } + return priced[i].InputPricePer1M < priced[j].InputPricePer1M + }) + return priced[len(priced)/2].ID +} + +func costTierFromCatalogFamily(compiled *CompiledCatalogV1, modelName string) (ModelTier, bool) { + if compiled == nil { + return "", false + } + canonical := strings.TrimSpace(modelName) + if resolved, ok := compiled.CanonicalModelForAliasOrID(canonical); ok { + canonical = resolved + } + model := compiled.ModelsByID[canonical] + if model.ID == "" { + return "", false + } + switch strings.ToLower(strings.TrimSpace(model.Family)) { + case "haiku", "cheap", "lite", "flash", "mini": + return TierHaiku, true + case "opus", "pro", "max", "heavy", "ultra": + return TierOpus, true + case "sonnet", "standard", "balanced", "medium": + return TierSonnet, true + default: + return "", false + } +} + +func costTierFromCatalogPricing(compiled *CompiledCatalogV1, modelName string) (ModelCostTier, bool) { + if compiled == nil { + return 0, false + } + canonical := strings.TrimSpace(modelName) + if resolved, ok := compiled.CanonicalModelForAliasOrID(canonical); ok { + canonical = resolved + } + model := compiled.ModelsByID[canonical] + if model.ID == "" { + return 0, false + } + offering := firstOfferingForModel(compiled, canonical) + price := offering.Pricing.RatesPer1M["input_tokens"] + if price <= 0 { + return 0, false + } + models := ModelEntriesForProvider(compiled, model.ProviderID) + prices := make([]float64, 0, len(models)) + seen := map[float64]bool{} + for _, candidate := range models { + if candidate.InputPricePer1M <= 0 || seen[candidate.InputPricePer1M] { + continue + } + seen[candidate.InputPricePer1M] = true + prices = append(prices, candidate.InputPricePer1M) + } + if len(prices) < 2 { + return 0, false + } + sort.Float64s(prices) + switch { + case price <= prices[0]: + return CostTierCheap, true + case price >= prices[len(prices)-1]: + return CostTierExpensive, true + default: + return CostTierMid, true + } +} + +func mapModelTierToCostTier(tier ModelTier) ModelCostTier { + switch tier { + case TierHaiku: + return CostTierCheap + case TierOpus: + return CostTierExpensive + default: + return CostTierMid + } +} + +func costTierFromModelName(modelName string) ModelCostTier { + lower := strings.ToLower(strings.TrimSpace(modelName)) + for _, pattern := range cheapModelNamePatterns { + if strings.Contains(lower, pattern) { + return CostTierCheap + } + } + for _, pattern := range expensiveModelNamePatterns { + if strings.Contains(lower, pattern) { + return CostTierExpensive + } + } + return CostTierMid +} + +var ( + cheapModelNamePatterns = []string{"haiku", "mini", "flash", "lite", "nano", "micro", "small", "tiny"} + expensiveModelNamePatterns = []string{"opus", "pro", "max", "ultra", "heavy", "large", "o1", "o3"} +) diff --git a/catalog/model_policy_test.go b/catalog/model_policy_test.go new file mode 100644 index 0000000..a0334eb --- /dev/null +++ b/catalog/model_policy_test.go @@ -0,0 +1,139 @@ +package catalog + +import ( + "testing" + "time" +) + +func testPolicyCatalogV1(t *testing.T) *CompiledCatalogV1 { + t.Helper() + now := time.Now().UTC() + cat := &CatalogV1{ + SchemaVersion: CatalogV1SchemaVersion, + GeneratedAt: now, + StaleAfter: now.Add(time.Hour), + Providers: map[string]ProviderV1{ + "anthropic": {ID: "anthropic", Name: "Anthropic"}, + }, + APIProtocols: map[string]APIProtocolV1{ + "anthropic-messages": {ID: "anthropic-messages", Name: "Anthropic Messages"}, + }, + Deployments: map[string]DeploymentV1{ + "anthropic-direct": { + ID: "anthropic-direct", + Name: "Anthropic", + ProviderID: "anthropic", + APIProtocolID: "anthropic-messages", + AdapterConstructor: "anthropic", + NativeModelIDSource: NativeModelIDCatalogKnown, + ModelMappingsRequired: false, + }, + }, + Models: map[string]ModelV1{ + "anthropic/claude-haiku": { + ID: "anthropic/claude-haiku", + ProviderID: "anthropic", + Name: "Claude Haiku", + Family: "haiku", + }, + "anthropic/claude-sonnet": { + ID: "anthropic/claude-sonnet", + ProviderID: "anthropic", + Name: "Claude Sonnet", + Family: "sonnet", + }, + "anthropic/claude-opus": { + ID: "anthropic/claude-opus", + ProviderID: "anthropic", + Name: "Claude Opus", + Family: "opus", + }, + }, + Offerings: []ModelOfferingV1{ + testPolicyOffering("anthropic/claude-haiku", "claude-haiku", 0.25, 1), + testPolicyOffering("anthropic/claude-sonnet", "claude-sonnet", 3, 15), + testPolicyOffering("anthropic/claude-opus", "claude-opus", 15, 75), + }, + Aliases: map[string]string{ + "claude-haiku": "anthropic/claude-haiku", + "claude-sonnet": "anthropic/claude-sonnet", + "claude-opus": "anthropic/claude-opus", + }, + } + compiled, err := CompileCatalogV1(cat) + if err != nil { + t.Fatalf("CompileCatalogV1 failed: %v", err) + } + return compiled +} + +func testPolicyOffering(canonicalModelID, nativeModelID string, input, output float64) ModelOfferingV1 { + return ModelOfferingV1{ + ID: "anthropic-direct:" + nativeModelID, + CanonicalModelID: canonicalModelID, + DeploymentID: "anthropic-direct", + NativeModelID: nativeModelID, + Pricing: PricingV1{ + Status: PricingKnown, + Currency: "USD", + RatesPer1M: map[string]float64{"input_tokens": input, "output_tokens": output}, + }, + } +} + +func TestModelPolicyPreferredProviderModelV1(t *testing.T) { + t.Parallel() + compiled := testPolicyCatalogV1(t) + + tests := []struct { + tier ModelTier + want string + }{ + {TierHaiku, "claude-haiku"}, + {TierSonnet, "claude-sonnet"}, + {TierOpus, "claude-opus"}, + } + for _, tt := range tests { + got := PreferredProviderModelV1(compiled, "anthropic", tt.tier, "") + if got != tt.want { + t.Fatalf("PreferredProviderModelV1(%s) = %q, want %q", tt.tier, got, tt.want) + } + } +} + +func TestModelPolicyCostTierOf(t *testing.T) { + t.Parallel() + compiled := testPolicyCatalogV1(t) + + tests := []struct { + model string + want ModelCostTier + }{ + {"claude-haiku", CostTierCheap}, + {"claude-sonnet", CostTierMid}, + {"claude-opus", CostTierExpensive}, + {"unknown-model-mini", CostTierCheap}, + } + for _, tt := range tests { + got := ModelCostTierOf(compiled, tt.model) + if got != tt.want { + t.Fatalf("ModelCostTierOf(%q) = %v, want %v", tt.model, got, tt.want) + } + } +} + +func TestModelPolicyDefaultRolesV1(t *testing.T) { + t.Parallel() + compiled := testPolicyCatalogV1(t) + + roles := DefaultModelRolesV1(compiled, "claude-opus") + if roles.Planner != "claude-opus" || roles.Coder != "claude-opus" || roles.Reviewer != "claude-opus" { + t.Fatalf("interactive roles should use primary model, got %+v", roles) + } + if roles.Commit != "claude-haiku" { + t.Fatalf("commit role = %q, want cheapest same-provider model", roles.Commit) + } + if got := ModelForRoleV1(compiled, ModelRoleAssignments{Coder: "coder"}, ModelRolePlanner); got != "coder" { + t.Fatalf("ModelForRoleV1 fallback = %q, want coder", got) + } +} From e6adcc4cc23a9cd952ed4dae26e8f9b434c4f151 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Tue, 30 Jun 2026 08:29:24 +0530 Subject: [PATCH 2/2] feat(catalog): expose tier model preferences --- catalog/model_policy.go | 32 ++++++++++++++++++++++++++++++++ catalog/model_policy_test.go | 10 ++++++++++ 2 files changed, 42 insertions(+) diff --git a/catalog/model_policy.go b/catalog/model_policy.go index dc22b96..61cbd64 100644 --- a/catalog/model_policy.go +++ b/catalog/model_policy.go @@ -61,6 +61,38 @@ func PreferredProviderModelV1(compiled *CompiledCatalogV1, provider string, tier } } +// PreferredModelsForTierV1 returns unique preferred models for a tier, starting +// with primaryProvider and then following the registry chat preference order. +func PreferredModelsForTierV1(compiled *CompiledCatalogV1, primaryProvider string, tier ModelTier, limit int) []string { + seenProviders := map[string]bool{} + seenModels := map[string]bool{} + providers := make([]string, 0, len(registry.ChatProviderPreferenceOrder())+1) + if primary := CanonicalProviderID(strings.TrimSpace(primaryProvider)); primary != "" { + providers = append(providers, primary) + } + providers = append(providers, registry.ChatProviderPreferenceOrder()...) + providers = append(providers, AllModelProvidersV1(compiled)...) + + models := make([]string, 0, len(providers)) + for _, provider := range providers { + provider = CanonicalProviderID(strings.TrimSpace(provider)) + if provider == "" || seenProviders[provider] { + continue + } + seenProviders[provider] = true + model := PreferredProviderModelV1(compiled, provider, tier, "") + if model == "" || seenModels[model] { + continue + } + seenModels[model] = true + models = append(models, model) + if limit > 0 && len(models) >= limit { + return models + } + } + return models +} + // CheapestModelForProviderV1 returns the lowest known input-priced model for a provider. func CheapestModelForProviderV1(compiled *CompiledCatalogV1, provider, fallback string) string { models := ModelEntriesForProvider(compiled, provider) diff --git a/catalog/model_policy_test.go b/catalog/model_policy_test.go index a0334eb..6fa9d13 100644 --- a/catalog/model_policy_test.go +++ b/catalog/model_policy_test.go @@ -101,6 +101,16 @@ func TestModelPolicyPreferredProviderModelV1(t *testing.T) { } } +func TestModelPolicyPreferredModelsForTierV1(t *testing.T) { + t.Parallel() + compiled := testPolicyCatalogV1(t) + + got := PreferredModelsForTierV1(compiled, "anthropic", TierHaiku, 3) + if len(got) != 1 || got[0] != "claude-haiku" { + t.Fatalf("PreferredModelsForTierV1 = %v, want [claude-haiku]", got) + } +} + func TestModelPolicyCostTierOf(t *testing.T) { t.Parallel() compiled := testPolicyCatalogV1(t)