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
50 changes: 50 additions & 0 deletions client/lazy_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package client

import "context"

// LazyProvider adapts EyrieClient to the Provider interface without eagerly
// resolving credentials or constructing a concrete provider.
type LazyProvider struct {
client *EyrieClient
provider string
}

// NewLazyProvider creates a provider wrapper that resolves the concrete
// provider only when chat or ping operations are invoked.
func NewLazyProvider(cfg *EyrieConfig) *LazyProvider {
c := Client(cfg)
provider := c.defaultProvider
if cfg != nil && cfg.Provider != "" {
provider = cfg.Provider
}
return &LazyProvider{
client: c,
provider: provider,
}
}

func (p *LazyProvider) Chat(ctx context.Context, messages []EyrieMessage, opts ChatOptions) (*EyrieResponse, error) {
if opts.Provider == "" {
opts.Provider = p.provider
}
return p.client.Chat(ctx, messages, opts)
}

func (p *LazyProvider) StreamChat(ctx context.Context, messages []EyrieMessage, opts ChatOptions) (*StreamResult, error) {
if opts.Provider == "" {
opts.Provider = p.provider
}
return p.client.StreamChat(ctx, messages, opts)
}

func (p *LazyProvider) Ping(ctx context.Context) error {
return p.client.Ping(ctx, p.provider)
}

func (p *LazyProvider) Name() string {
return p.provider
}

func (p *LazyProvider) SetAPIKey(provider, apiKey string) {
p.client.SetAPIKey(provider, apiKey)
}
166 changes: 166 additions & 0 deletions runtime/default_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,38 @@ package runtime

import (
"context"
"os"
"sort"
"strings"
"time"

"github.com/GrayCodeAI/eyrie/catalog"
"github.com/GrayCodeAI/eyrie/config"
"github.com/GrayCodeAI/eyrie/credentials"
)

var chatProviderPreferenceOrder = []string{
"openai",
"anthropic",
"openrouter",
"grok",
"gemini",
"vertex",
"bedrock",
"zai_coding",
"zai_payg",
"canopywave",
"deepseek",
"azure",
"opencodego",
"kimi",
"xiaomi_mimo_payg",
"xiaomi_mimo_token_plan",
"minimax_token_plan",
"minimax_payg",
"ollama",
}

// DefaultModelProviderFilter returns the catalog provider id to use when listing models
// with no explicit provider (e.g. /config model picker after paste-key).
// Order: provider.json default → first configured deployment (stable sort by id).
Expand Down Expand Up @@ -35,3 +61,143 @@ func DefaultModelProviderFilter(ctx context.Context) string {
}
return ""
}

// PreferredProvider returns the runtime-owned provider choice when a host has
// not pinned one explicitly. Active selection wins first, then inferred model
// ownership, then configured providers ordered by runtime preference, and
// finally credential detection as a last resort.
func PreferredProvider(ctx context.Context) string {
if ctx == nil {
ctx = context.Background()
}
if provider := normalizeRuntimeProviderID(ActiveProvider(ctx)); provider != "" && providerConfigured(ctx, provider) {
return provider
}
if model := ActiveModel(ctx); model != "" {
if provider := inferProviderForModel(ctx, model); provider != "" && providerConfigured(ctx, provider) {
return provider
}
}
if provider := preferredConfiguredProvider(ctx); provider != "" {
return provider
}
return preferredDetectedProvider()
}

func preferredConfiguredProvider(ctx context.Context) string {
rt, err := Load(ctx)
if err != nil || rt == nil {
return ""
}
rows, err := rt.DeploymentRows()
if err != nil || len(rows) == 0 {
return ""
}
configured := make(map[string]struct{}, len(rows))
for _, row := range rows {
if !row.Configured {
continue
}
if provider := catalog.CanonicalProviderID(row.ProviderID); provider != "" {
configured[provider] = struct{}{}
}
}
for _, provider := range chatProviderPreferenceOrder {
if _, ok := configured[provider]; ok {
return provider
}
}

ordered := make([]string, 0, len(configured))
for provider := range configured {
ordered = append(ordered, provider)
}
sort.Strings(ordered)
if len(ordered) == 0 {
return ""
}
return ordered[0]
}

func preferredDetectedProvider() string {
for _, provider := range chatProviderPreferenceOrder {
switch provider {
case "ollama":
if runtimeEnvValue("OLLAMA_BASE_URL") != "" {
return provider
}
default:
profile, ok := runtimeProfileForProvider(provider)
if !ok {
continue
}
ready := true
for _, envKey := range profile.DetectionEnv {
if runtimeEnvValue(envKey) == "" {
ready = false
break
}
}
if ready {
return provider
}
}
}
return ""
}

func runtimeProfileForProvider(provider string) (config.RuntimeProviderProfile, bool) {
switch provider {
case "anthropic":
return config.AnthropicRuntimeProfile, true
case "openai":
return config.OpenAIRuntimeProfile, true
case "openrouter":
return config.OpenRouterRuntimeProfile, true
case "grok":
return config.GrokRuntimeProfile, true
case "gemini":
return config.GeminiRuntimeProfile, true
case "vertex":
return config.VertexRuntimeProfile, true
case "bedrock":
return config.BedrockRuntimeProfile, true
case "zai_coding":
return config.ZAICodingRuntimeProfile, true
case "zai_payg":
return config.ZAIPaygRuntimeProfile, true
case "canopywave":
return config.CanopyWaveRuntimeProfile, true
case "deepseek":
return config.DeepSeekRuntimeProfile, true
case "azure":
return config.AzureRuntimeProfile, true
case "opencodego":
return config.OpenCodeGoRuntimeProfile, true
case "kimi":
return config.KimiRuntimeProfile, true
case "xiaomi_mimo_payg":
return config.XiaomiPaygRuntimeProfile, true
case "xiaomi_mimo_token_plan":
return config.XiaomiTokenPlanRuntimeProfile, true
case "minimax_token_plan":
return config.MiniMaxTokenPlanRuntimeProfile, true
case "minimax_payg":
return config.MiniMaxPaygRuntimeProfile, true
default:
return config.RuntimeProviderProfile{}, false
}
}

func runtimeEnvValue(key string) string {
key = strings.TrimSpace(key)
if key == "" {
return ""
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if value := credentials.LookupSecret(ctx, key); value != "" {
return value
}
return strings.TrimSpace(os.Getenv(key))
}
Loading
Loading