Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ jobs:
dependency-review:
name: dependency review
runs-on: ubuntu-latest
continue-on-error: true # requires GitHub Dependency graph (repo settings)
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
36 changes: 36 additions & 0 deletions catalog/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package catalog

import "time"

const bootstrapSource = "bootstrap"

// BootstrapSource returns the provenance label for the embedded catalog seed.
func BootstrapSource() string {
return bootstrapSource
}

// BootstrapCatalogV1 returns deployment/provider wiring only — no chat models.
// Chat models come from the published catalog cache and live provider discovery.
func BootstrapCatalogV1() CatalogV1 {
generatedAt := time.Now().UTC().Truncate(time.Second)
c := CatalogV1{
SchemaVersion: CatalogV1SchemaVersion,
GeneratedAt: generatedAt,
StaleAfter: generatedAt.Add(24 * time.Hour),
Providers: defaultProvidersV1(),
APIProtocols: defaultAPIProtocolsV1(),
Deployments: defaultDeploymentsV1(),
Models: map[string]ModelV1{},
Aliases: map[string]string{},
Offerings: nil,
Provenance: &CatalogProvenanceV1{Source: bootstrapSource, ObservedAt: generatedAt},
}
EnsureDeploymentEnvFallbacks(&c)
EnsureCredentialRegistryInCatalog(&c)
return c
}

// IsBootstrapCatalog reports whether c is the empty wiring-only catalog.
func IsBootstrapCatalog(c *CatalogV1) bool {
return c != nil && c.Provenance != nil && c.Provenance.Source == bootstrapSource
}
2 changes: 1 addition & 1 deletion catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestGetModelDeprecationWarning(t *testing.T) {
}

func TestModelsForProvider(t *testing.T) {
cat := DefaultModelCatalog()
cat := testLegacyModelCatalog()
models := ModelsForProvider(&cat, "anthropic")
if len(models) == 0 {
t.Error("expected anthropic models in default catalog")
Expand Down
69 changes: 69 additions & 0 deletions catalog/compiled_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package catalog

import "sort"

// ModelEntriesForProvider lists models from a compiled v1 catalog for one provider.
// New models appear here automatically when the eyrie catalog is updated — hosts must not hardcode IDs.
func ModelEntriesForProvider(compiled *CompiledCatalogV1, provider string) []ModelCatalogEntry {
if compiled == nil {
return nil
}
provider = CanonicalProviderID(provider)
if provider == "" {
return nil
}
seen := map[string]bool{}
var out []ModelCatalogEntry
add := func(model ModelV1, offering ModelOfferingV1) {
if model.ID == "" || seen[model.ID] {
return
}
seen[model.ID] = true
inPrice, outPrice := 0.0, 0.0
if offering.Pricing.RatesPer1M != nil {
inPrice = offering.Pricing.RatesPer1M["input_tokens"]
outPrice = offering.Pricing.RatesPer1M["output_tokens"]
}
out = append(out, ModelCatalogEntry{
ID: model.ID,
DisplayName: model.Name,
ContextWindow: model.ContextWindow,
MaxOutput: model.MaxOutput,
InputPricePer1M: inPrice,
OutputPricePer1M: outPrice,
})
}
if provider == "openrouter" {
for _, offering := range compiled.OfferingsByDeployment["openrouter"] {
model, ok := compiled.ModelsByID[offering.CanonicalModelID]
if !ok {
continue
}
add(model, offering)
}
} else {
ids := make([]string, 0, len(compiled.ModelsByID))
for id, model := range compiled.ModelsByID {
if CanonicalProviderID(model.ProviderID) == provider {
ids = append(ids, id)
}
}
sort.Strings(ids)
for _, id := range ids {
add(compiled.ModelsByID[id], firstOfferingForModel(compiled, id))
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}

func firstOfferingForModel(compiled *CompiledCatalogV1, canonicalModelID string) ModelOfferingV1 {
offerings := compiled.OfferingsByCanonicalModel[canonicalModelID]
if len(offerings) == 0 {
return ModelOfferingV1{}
}
sort.SliceStable(offerings, func(i, j int) bool {
return offerings[i].DeploymentID < offerings[j].DeploymentID
})
return offerings[0]
}
34 changes: 34 additions & 0 deletions catalog/compiled_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package catalog

import "testing"

func TestModelEntriesForProvider_OpenRouterUsesOfferings(t *testing.T) {
compiled := &CompiledCatalogV1{
ModelsByID: map[string]ModelV1{
"anthropic/claude-sonnet-4-6": {ID: "anthropic/claude-sonnet-4-6", Name: "Sonnet", ProviderID: "anthropic"},
},
OfferingsByDeployment: map[string][]ModelOfferingV1{
"openrouter": {{
CanonicalModelID: "anthropic/claude-sonnet-4-6",
DeploymentID: "openrouter",
}},
},
}
entries := ModelEntriesForProvider(compiled, "openrouter")
if len(entries) != 1 || entries[0].ID != "anthropic/claude-sonnet-4-6" {
t.Fatalf("openrouter entries: %+v", entries)
}
}

func TestModelEntriesForProvider_AnthropicFiltersByProvider(t *testing.T) {
compiled := &CompiledCatalogV1{
ModelsByID: map[string]ModelV1{
"anthropic/claude-sonnet-4-6": {ID: "anthropic/claude-sonnet-4-6", Name: "Sonnet", ProviderID: "anthropic"},
"openai/gpt-4o": {ID: "openai/gpt-4o", Name: "GPT-4o", ProviderID: "openai"},
},
}
entries := ModelEntriesForProvider(compiled, "anthropic")
if len(entries) != 1 || entries[0].ID != "anthropic/claude-sonnet-4-6" {
t.Fatalf("anthropic entries: %+v", entries)
}
}
94 changes: 94 additions & 0 deletions catalog/credential_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package catalog

import (
"github.com/GrayCodeAI/eyrie/catalog/registry"
)

// CredentialProviderSpec defines paste-key setup metadata (derived from registry).
type CredentialProviderSpec struct {
ProviderID string
DisplayName string
DeploymentID string
EnvVar string
KeyPrefixes []string
ProbeKind string
ProbeBaseURL string
RequiresKey bool
SortOrder int
}

// CredentialProviderRegistry is derived from catalog/registry (single source of truth).
var CredentialProviderRegistry = deriveCredentialRegistry()

func deriveCredentialRegistry() []CredentialProviderSpec {
rows := registry.CredentialRegistry()
out := make([]CredentialProviderSpec, len(rows))
for i, r := range rows {
out[i] = CredentialProviderSpec{
ProviderID: r.ProviderID,
DisplayName: r.DisplayName,
DeploymentID: r.DeploymentID,
EnvVar: r.EnvVar,
KeyPrefixes: r.KeyPrefixes,
ProbeKind: r.ProbeKind,
ProbeBaseURL: r.ProbeBaseURL,
RequiresKey: r.RequiresKey,
SortOrder: r.SortOrder,
}
}
return out
}

// EnsureCredentialRegistryInCatalog merges registry providers/deployments into catalog v1.
func EnsureCredentialRegistryInCatalog(c *CatalogV1) {
if c == nil {
return
}
if c.Providers == nil {
c.Providers = map[string]ProviderV1{}
}
if c.Deployments == nil {
c.Deployments = map[string]DeploymentV1{}
}
for _, spec := range registry.All() {
pid := CanonicalProviderID(spec.ProviderID)
if c.Providers[pid].ID == "" {
c.Providers[pid] = ProviderV1{ID: pid, Name: spec.DisplayName}
}
if c.Deployments[spec.DeploymentID].ID == "" {
c.Deployments[spec.DeploymentID] = DeploymentV1{
ID: spec.DeploymentID,
Name: spec.DisplayName,
ProviderID: pid,
APIProtocolID: spec.APIProtocolID,
AdapterConstructor: spec.AdapterID,
NativeModelIDSource: NativeModelIDDiscovered,
}
}
}
EnsureDeploymentEnvFallbacks(c)
}

func SpecByEnvVar(env string) (CredentialProviderSpec, bool) {
for _, s := range CredentialProviderRegistry {
if s.EnvVar == env {
return s, true
}
}
return CredentialProviderSpec{}, false
}

func SpecByProviderID(id string) (CredentialProviderSpec, bool) {
id = CanonicalProviderID(id)
for _, s := range CredentialProviderRegistry {
if CanonicalProviderID(s.ProviderID) == id {
return s, true
}
}
return CredentialProviderSpec{}, false
}

// ProviderDisplayName returns UI label from registry.
func ProviderDisplayName(providerID string) string {
return registry.DisplayName(providerID)
}
31 changes: 31 additions & 0 deletions catalog/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package catalog

// Credentials carries API keys and related env (base URLs) for provider-backed catalog discovery.
// Keys use standard env var names (e.g. OPENROUTER_API_KEY). Populate via config.DiscoveryCredentials.
// or pass an explicit map from hawk — do not hardcode provider lists in hawk.
type Credentials struct {
APIKeys map[string]string
}

// Env returns a copy of the key map suitable for FetchModelCatalog.
func (c Credentials) Env() map[string]string {
out := make(map[string]string, len(c.APIKeys))
for k, v := range c.APIKeys {
if k != "" && v != "" {
out[k] = v
}
}
return out
}

// MergeCredentials merges additional keys into c (later keys win).
func (c *Credentials) Merge(other Credentials) {
if c.APIKeys == nil {
c.APIKeys = map[string]string{}
}
for k, v := range other.APIKeys {
if k != "" && v != "" {
c.APIKeys[k] = v
}
}
}
Loading
Loading