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
15 changes: 2 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,7 @@ jobs:
extra_args: --only-verified

# -------------------------------------------------------------------------
# 8. Dependency review — only on pull requests.
# -------------------------------------------------------------------------
dependency-review:
name: dependency review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0

# -------------------------------------------------------------------------
# 9. Markdown lint — validate documentation quality.
# 8. Markdown lint — validate documentation quality.
# -------------------------------------------------------------------------
markdown:
name: markdown
Expand All @@ -211,7 +200,7 @@ jobs:
markdownlint-cli2 '**/*.md'

# -------------------------------------------------------------------------
# 10. Cross-platform build matrix — zero CGO, all targets.
# 9. Cross-platform build matrix — zero CGO, all targets.
# -------------------------------------------------------------------------
build:
name: build (${{ matrix.goos }}/${{ matrix.goarch }})
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
157 changes: 157 additions & 0 deletions catalog/compiled_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package catalog

import (
"encoding/json"
"sort"
"strings"

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

// 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
}
if spec, ok := registry.SpecByProviderID(provider); ok {
entries := modelEntriesForDeployment(compiled, spec.DeploymentID)
if spec.ModelStrategy == registry.StrategyLiveOnly {
return entries
}
if len(entries) > 0 {
return entries
}
}
if dep := listingDeploymentForProvider(provider); dep != "" {
return modelEntriesForDeployment(compiled, dep)
}
return modelEntriesByProviderID(compiled, provider)
}

func modelEntriesByProviderID(compiled *CompiledCatalogV1, provider string) []ModelCatalogEntry {
seen := map[string]bool{}
var out []ModelCatalogEntry
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 {
entry := modelEntryFromOffering(compiled.ModelsByID[id], firstOfferingForModel(compiled, id))
if entry.ID == "" || seen[entry.ID] {
continue
}
seen[entry.ID] = true
out = append(out, entry)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}

func listingDeploymentForProvider(provider string) string {
if spec, ok := registry.SpecByProviderID(provider); ok && spec.ModelStrategy == registry.StrategyLiveOnly {
return spec.DeploymentID
}
return ""
}

func modelEntriesForDeployment(compiled *CompiledCatalogV1, deploymentID string) []ModelCatalogEntry {
if compiled == nil || deploymentID == "" {
return nil
}
offerings := compiled.OfferingsByDeployment[deploymentID]
sort.SliceStable(offerings, func(i, j int) bool {
return offerings[i].NativeModelID < offerings[j].NativeModelID
})
seen := map[string]bool{}
var out []ModelCatalogEntry
for _, offering := range offerings {
model, ok := compiled.ModelsByID[offering.CanonicalModelID]
if !ok {
continue
}
entry := modelEntryFromOffering(model, offering)
if entry.ID == "" || seen[entry.ID] {
continue
}
seen[entry.ID] = true
out = append(out, entry)
}
return out
}

func modelEntryFromOffering(model ModelV1, offering ModelOfferingV1) ModelCatalogEntry {
id := strings.TrimSpace(model.ID)
if native := strings.TrimSpace(offering.NativeModelID); native != "" {
id = native
}
inPrice, outPrice := 0.0, 0.0
if offering.Pricing.RatesPer1M != nil {
inPrice = offering.Pricing.RatesPer1M["input_tokens"]
outPrice = offering.Pricing.RatesPer1M["output_tokens"]
}
return ModelCatalogEntry{
ID: id,
DisplayName: strings.TrimSpace(model.Name),
Description: descriptionFromLiveMetadata(offering.LiveMetadata),
Owner: modelOwnerFromOffering(offering),
ContextWindow: model.ContextWindow,
MaxOutput: model.MaxOutput,
InputPricePer1M: inPrice,
OutputPricePer1M: outPrice,
ServerTools: serverToolsFromOffering(offering),
LiveMetadata: offering.LiveMetadata,
}
}

func descriptionFromLiveMetadata(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var meta struct {
Description string `json:"description"`
}
if err := json.Unmarshal(raw, &meta); err != nil {
return ""
}
return strings.TrimSpace(meta.Description)
}

func modelOwnerFromOffering(offering ModelOfferingV1) string {
if o := ownerFromLiveMetadata(offering.LiveMetadata); o != "" {
return o
}
return ownerFromModelID(offering.NativeModelID)
}

func serverToolsFromOffering(offering ModelOfferingV1) []string {
if offering.Capabilities.ServerTools == nil {
return nil
}
var out []string
for tool, state := range offering.Capabilities.ServerTools {
if state == CapabilitySupported && strings.TrimSpace(tool) != "" {
out = append(out, tool)
}
}
sort.Strings(out)
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]
}
84 changes: 84 additions & 0 deletions catalog/compiled_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package catalog

import "testing"

func TestModelEntriesForProvider_OpenRouterUsesOfferings(t *testing.T) {
raw := []byte(`{"id":"anthropic/claude-sonnet-4-6","architecture":{"modality":"text"}}`)
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",
NativeModelID: "anthropic/claude-sonnet-4-6",
LiveMetadata: raw,
}},
},
}
entries := ModelEntriesForProvider(compiled, "openrouter")
if len(entries) != 1 || entries[0].ID != "anthropic/claude-sonnet-4-6" {
t.Fatalf("openrouter entries: %+v", entries)
}
if string(entries[0].LiveMetadata) != string(raw) {
t.Fatalf("live metadata missing: %+v", entries[0])
}
}

func TestModelEntriesForProvider_CanopyWaveUsesDeploymentOfferings(t *testing.T) {
raw := []byte(`{"id":"moonshotai/kimi-k2.6","name":"Kimi K2.6","owned_by":"moonshotai"}`)
compiled := &CompiledCatalogV1{
ModelsByID: map[string]ModelV1{
"moonshotai/kimi-k2.6": {ID: "moonshotai/kimi-k2.6", Name: "Kimi K2.6", ProviderID: "moonshotai"},
},
OfferingsByDeployment: map[string][]ModelOfferingV1{
"canopywave": {{
CanonicalModelID: "moonshotai/kimi-k2.6",
DeploymentID: "canopywave",
NativeModelID: "moonshotai/kimi-k2.6",
LiveMetadata: raw,
}},
},
}
entries := ModelEntriesForProvider(compiled, "canopywave")
if len(entries) != 1 || entries[0].ID != "moonshotai/kimi-k2.6" {
t.Fatalf("canopywave entries: %+v", entries)
}
if string(entries[0].LiveMetadata) != string(raw) {
t.Fatalf("live metadata missing: %+v", entries[0])
}
}

func TestModelEntriesForProvider_GeminiUsesDirectDeploymentOfferings(t *testing.T) {
compiled := &CompiledCatalogV1{
ModelsByID: map[string]ModelV1{
"gemini-flash": {ID: "gemini-flash", Name: "Flash", ProviderID: "google"},
"gemini-pro": {ID: "gemini-pro", Name: "Pro", ProviderID: "google"},
"other-model": {ID: "other-model", Name: "Other", ProviderID: "google"},
},
OfferingsByDeployment: map[string][]ModelOfferingV1{
"gemini-direct": {
{CanonicalModelID: "gemini-flash", DeploymentID: "gemini-direct", NativeModelID: "gemini-flash"},
{CanonicalModelID: "gemini-pro", DeploymentID: "gemini-direct", NativeModelID: "gemini-pro"},
},
},
}
entries := ModelEntriesForProvider(compiled, "gemini")
if len(entries) != 2 {
t.Fatalf("expected 2 gemini-direct offerings, got %d: %+v", len(entries), 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)
}
}
Loading