Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion external/eyrie
53 changes: 18 additions & 35 deletions internal/provider/routing/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"sort"
"sync"

"github.com/GrayCodeAI/eyrie/catalog"

Check failure on line 11 in internal/provider/routing/catalog.go

View workflow job for this annotation

GitHub Actions / deadcode

could not import github.com/GrayCodeAI/eyrie/catalog (invalid package name: "")
)

// ModelInfo describes a known LLM model (view over eyrie catalog entries).
Expand Down Expand Up @@ -57,6 +57,17 @@
}
}

func fromEyrieEntry(entry catalog.ModelCatalogEntry, provider string) ModelInfo {
return ModelInfo{
Name: entry.ID,
Provider: provider,
ContextSize: entry.ContextWindow,
InputPrice: entry.InputPricePer1M,
OutputPrice: entry.OutputPricePer1M,
Description: entry.DisplayName,
}
}

// Find looks up a model by name via eyrie's JSON catalog.
func Find(name string) (ModelInfo, bool) {
if compiled := eyrieCatalogV1(); compiled != nil {
Expand All @@ -71,19 +82,13 @@

// ByProvider returns all models for a given provider from eyrie's catalog.
func ByProvider(provider string) []ModelInfo {
provider = canonicalProvider(provider)
provider = catalog.CanonicalProviderID(provider)
compiled := eyrieCatalogV1()
out := []ModelInfo{}
if compiled != nil {
modelIDs := make([]string, 0, len(compiled.ModelsByID))
for id, model := range compiled.ModelsByID {
if canonicalProvider(model.ProviderID) == provider {
modelIDs = append(modelIDs, id)
}
}
sort.Strings(modelIDs)
for _, id := range modelIDs {
out = append(out, fromEyrieV1(compiled.ModelsByID[id], firstOffering(compiled, id, "")))
entries := catalog.ModelEntriesForProvider(compiled, provider)
for _, entry := range entries {
out = append(out, fromEyrieEntry(entry, provider))
}
}
return out
Expand All @@ -104,26 +109,12 @@

// DefaultModel returns the first catalog model for a provider via eyrie JSON.
func DefaultModel(provider string) string {
models := ByProvider(provider)
if len(models) > 0 {
return models[0].Name
}
return ""
return catalog.ProviderDefaultModelV1(eyrieCatalogV1(), provider, "")
}

// AllProviders returns all canonical model owner providers from eyrie's catalog.
func AllProviders() []string {
seen := map[string]bool{}
var out []string
if compiled := eyrieCatalogV1(); compiled != nil {
for _, model := range compiled.ModelsByID {
provider := canonicalProvider(model.ProviderID)
if provider != "" && !seen[provider] {
seen[provider] = true
out = append(out, provider)
}
}
}
out := catalog.AllModelProvidersV1(eyrieCatalogV1())
sort.Strings(out)
return out
}
Expand All @@ -147,13 +138,5 @@
}

func canonicalProvider(provider string) string {
switch provider {
case "gemini":
return "google"
case "grok":
return "xai"
// Z.AI uses zai_payg and zai_coding directly — no aliases.
default:
return provider
}
return catalog.CanonicalProviderID(provider)
}
74 changes: 20 additions & 54 deletions internal/provider/routing/roles.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package routing

import "strings"
import (
"strings"

eycatalog "github.com/GrayCodeAI/eyrie/catalog"
)

// Role identifies the purpose of a model within a multi-model workflow.
type Role string
Expand All @@ -24,73 +28,35 @@ type ModelRoles struct {
// DefaultRoles returns a ModelRoles where every role uses primaryModel except
// Commit, which defaults to the cheapest available model from the catalog.
func DefaultRoles(primaryModel string) ModelRoles {
commit := CheapestForProvider(providerOf(primaryModel), primaryModel)
return ModelRoles{
Planner: primaryModel,
Coder: primaryModel,
Reviewer: primaryModel,
Commit: commit,
}
return fromEyrieRoles(eycatalog.DefaultModelRolesV1(eyrieCatalogV1(), primaryModel))
}

// ModelForRole returns the model name assigned to role, falling back to the
// Coder model (primary) if the role-specific field is empty.
func (r ModelRoles) ModelForRole(role Role) string {
var m string
switch role {
case RolePlanner:
m = r.Planner
case RoleCoder:
m = r.Coder
case RoleReviewer:
m = r.Reviewer
case RoleCommit:
m = r.Commit
}
if strings.TrimSpace(m) == "" {
if strings.TrimSpace(r.Coder) != "" {
return r.Coder
}
return primaryModel()
}
return m
return eycatalog.ModelForRoleV1(eyrieCatalogV1(), toEyrieRoles(r), eycatalog.ModelRole(role))
}

// CheapestForProvider queries eyrie's catalog at runtime and returns the
// cheapest model for the given provider. No hardcoded model names.
func CheapestForProvider(provider, fallback string) string {
models := ByProvider(provider)
if len(models) == 0 {
return fallback
}
cheapest := models[0]
for _, m := range models[1:] {
if m.InputPrice > 0 && m.InputPrice < cheapest.InputPrice {
cheapest = m
}
}
if cheapest.Name != "" {
return cheapest.Name
}
return fallback
return eycatalog.CheapestModelForProviderV1(eyrieCatalogV1(), provider, fallback)
}

// providerOf extracts the provider from a model name by looking it up in the catalog.
func providerOf(modelName string) string {
info, ok := Find(modelName)
if ok {
return canonicalProvider(info.Provider)
func toEyrieRoles(r ModelRoles) eycatalog.ModelRoleAssignments {
return eycatalog.ModelRoleAssignments{
Planner: strings.TrimSpace(r.Planner),
Coder: strings.TrimSpace(r.Coder),
Reviewer: strings.TrimSpace(r.Reviewer),
Commit: strings.TrimSpace(r.Commit),
}
return ""
}

// primaryModel returns a reasonable fallback by querying what's available.
func primaryModel() string {
providers := AllProviders()
for _, p := range providers {
if m := DefaultModel(p); m != "" {
return m
}
func fromEyrieRoles(r eycatalog.ModelRoleAssignments) ModelRoles {
return ModelRoles{
Planner: strings.TrimSpace(r.Planner),
Coder: strings.TrimSpace(r.Coder),
Reviewer: strings.TrimSpace(r.Reviewer),
Commit: strings.TrimSpace(r.Commit),
}
return ""
}
161 changes: 18 additions & 143 deletions internal/provider/routing/tiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,19 @@ import (
)

// CostTier is a relative cost band for cascade routing (cheap / mid / expensive).
type CostTier int
type CostTier = eycatalog.ModelCostTier

const (
CostTierCheap CostTier = iota
CostTierMid
CostTierExpensive
CostTierCheap = eycatalog.CostTierCheap
CostTierMid = eycatalog.CostTierMid
CostTierExpensive = eycatalog.CostTierExpensive
)

// CostTierOf resolves a model's cost tier from eyrie catalog data (family and
// within-provider pricing). Unknown models default to mid-tier.
// CostTierOf resolves a model's cost tier from eyrie catalog policy.
func CostTierOf(modelName string) CostTier {
if tier, ok := tierFromCatalogFamily(modelName); ok {
return mapEyrieTier(tier)
}
if tier, ok := tierFromCatalogPricing(modelName); ok {
return tier
}
return tierFromName(modelName)
return eycatalog.ModelCostTierOf(eyrieCatalogV1(), modelName)
}

// tierFromName infers cost tier from well-known model name patterns.
// This is a fallback when the eyrie catalog is unavailable or incomplete.
func tierFromName(modelName string) CostTier {
lower := strings.ToLower(strings.TrimSpace(modelName))
for _, pat := range cheapPatterns {
if strings.Contains(lower, pat) {
return CostTierCheap
}
}
for _, pat := range expensivePatterns {
if strings.Contains(lower, pat) {
return CostTierExpensive
}
}
return CostTierMid
}

var (
cheapPatterns = []string{"haiku", "mini", "flash", "lite", "nano", "micro", "small", "tiny"}
expensivePatterns = []string{"opus", "pro", "max", "ultra", "heavy", "large", "o1", "o3"}
)

// TierModels returns eyrie-preferred model IDs for haiku, sonnet, and opus tiers.
func TierModels(provider string) (haiku, sonnet, opus string) {
return PreferredModelForTier(provider, eycatalog.TierHaiku, ""),
Expand Down Expand Up @@ -122,132 +93,36 @@ func catalogModelNames(compiled *eycatalog.CompiledCatalogV1) []string {
// DefaultHealthTiers builds complexity-based routing tiers from the eyrie catalog.
func DefaultHealthTiers(primaryProvider string) []ModelTier {
primaryProvider = canonicalProvider(primaryProvider)
if primaryProvider == "" {
primaryProvider = "anthropic"
}
light := tierModelList(primaryProvider, eycatalog.TierHaiku, "openai", "gemini")
standard := tierModelList(primaryProvider, eycatalog.TierSonnet, "openai", "gemini")
heavy := tierModelList(primaryProvider, eycatalog.TierOpus, "openai", "gemini")
light := tierModelList(primaryProvider, eycatalog.TierHaiku)
standard := tierModelList(primaryProvider, eycatalog.TierSonnet)
heavy := tierModelList(primaryProvider, eycatalog.TierOpus)
return []ModelTier{
{Name: "light", Models: light, MaxComplexity: 10.0},
{Name: "standard", Models: standard, MaxComplexity: 30.0},
{Name: "heavy", Models: heavy, MaxComplexity: 1e9},
}
}

func tierModelList(primaryProvider string, tier eycatalog.ModelTier, extraProviders ...string) []string {
func tierModelList(primaryProvider string, tier eycatalog.ModelTier) []string {
seen := map[string]bool{}
var out []string
add := func(m string) {
m = strings.TrimSpace(m)
if m != "" && !seen[m] {
seen[m] = true
out = append(out, m)
for _, model := range eycatalog.PreferredModelsForTierV1(eyrieCatalogV1(), primaryProvider, tier, 3) {
model = strings.TrimSpace(model)
if model == "" || seen[model] {
continue
}
}
add(PreferredModelForTier(primaryProvider, tier, ""))
for _, p := range extraProviders {
add(PreferredModelForTier(p, tier, ""))
seen[model] = true
out = append(out, model)
}
return out
}

// PreferredModelForTier returns the eyrie-preferred model for a provider and tier.
func PreferredModelForTier(provider string, tier eycatalog.ModelTier, fallback string) string {
provider = canonicalProvider(provider)
if provider == "" {
return fallback
}
if m := eycatalog.GetPreferredProviderModel(provider, tier, nil); m != "" {
return m
}
return fallback
return eycatalog.PreferredProviderModelV1(eyrieCatalogV1(), provider, tier, fallback)
}

// MostExpensiveForProvider picks the highest input-priced model for a provider.
func MostExpensiveForProvider(provider, fallback string) string {
models := ByProvider(canonicalProvider(provider))
if len(models) == 0 {
return fallback
}
best := models[0]
for _, m := range models[1:] {
if m.InputPrice > best.InputPrice {
best = m
}
}
if best.Name != "" {
return best.Name
}
return fallback
}

func mapEyrieTier(tier eycatalog.ModelTier) CostTier {
switch tier {
case eycatalog.TierHaiku:
return CostTierCheap
case eycatalog.TierOpus:
return CostTierExpensive
default:
return CostTierMid
}
}

func tierFromCatalogFamily(modelName string) (eycatalog.ModelTier, bool) {
compiled := eyrieCatalogV1()
if compiled == nil {
return "", false
}
canonical := modelName
if c, ok := compiled.CanonicalModelForAliasOrID(modelName); ok {
canonical = c
}
model := compiled.ModelsByID[canonical]
if model.ID == "" {
return "", false
}
switch strings.ToLower(strings.TrimSpace(model.Family)) {
case "haiku", "cheap", "lite", "flash", "mini":
return eycatalog.TierHaiku, true
case "opus", "pro", "max", "heavy", "ultra":
return eycatalog.TierOpus, true
case "sonnet", "standard", "balanced", "medium":
return eycatalog.TierSonnet, true
}
return "", false
}

func tierFromCatalogPricing(modelName string) (CostTier, bool) {
info, ok := Find(modelName)
if !ok || info.InputPrice <= 0 {
return 0, false
}
models := ByProvider(canonicalProvider(info.Provider))
if len(models) < 2 {
return 0, false
}

prices := make([]float64, 0, len(models))
seen := map[float64]bool{}
for _, m := range models {
if m.InputPrice <= 0 || seen[m.InputPrice] {
continue
}
seen[m.InputPrice] = true
prices = append(prices, m.InputPrice)
}
if len(prices) < 2 {
return 0, false
}
sort.Float64s(prices)

price := info.InputPrice
switch {
case price <= prices[0]:
return CostTierCheap, true
case price >= prices[len(prices)-1]:
return CostTierExpensive, true
default:
return CostTierMid, true
}
return eycatalog.MostExpensiveModelForProviderV1(eyrieCatalogV1(), provider, fallback)
}
Loading
Loading