package handlers import ( "context" "log" "strings" "git.gobha.me/xcaliber/chat-switchboard/config" "git.gobha.me/xcaliber/chat-switchboard/crypto" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) // Known provider default endpoints for seeding. var seedDefaultEndpoints = map[string]string{ "openai": "https://api.openai.com/v1", "anthropic": "https://api.anthropic.com", "openrouter": "https://openrouter.ai/api/v1", "venice": "https://api.venice.ai/api/v1", "mistral": "https://api.mistral.ai/v1", "groq": "https://api.groq.com/openai/v1", "together": "https://api.together.xyz/v1", "fireworks": "https://api.fireworks.ai/inference/v1", "deepseek": "https://api.deepseek.com/v1", "perplexity": "https://api.perplexity.ai", } // SeedProviders creates global providers from the SEED_PROVIDERS env var. // // Format: "provider:api_key[:name],provider:api_key[:name]" // // Examples: // // SEED_PROVIDERS="openai:sk-xxx,anthropic:sk-ant-xxx" // SEED_PROVIDERS="openai:sk-xxx:My OpenAI,openrouter:sk-or-xxx" // // Idempotent: skips if a global provider with the same name already exists. // Blocked in production. func SeedProviders(cfg *config.Config, stores store.Stores, resolver *crypto.KeyResolver) { if cfg.SeedProviders == "" { return } if cfg.Environment == "production" { log.Printf("⚠ SEED_PROVIDERS ignored in production environment") return } ctx := context.Background() entries := strings.Split(cfg.SeedProviders, ",") log.Printf(" 🌱 SEED_PROVIDERS: %d entries to process", len(entries)) for _, entry := range entries { entry = strings.TrimSpace(entry) if entry == "" { continue } parts := strings.SplitN(entry, ":", 3) if len(parts) < 2 { log.Printf("⚠ Seed provider skipped (bad format, want provider:key[:name]): %q", entry) continue } provider := strings.ToLower(strings.TrimSpace(parts[0])) apiKey := strings.TrimSpace(parts[1]) // Derive name: explicit or "OpenAI (seed)", "Anthropic (seed)", etc. name := "" if len(parts) >= 3 { name = strings.TrimSpace(parts[2]) } if name == "" { // Capitalize provider name name = strings.ToUpper(provider[:1]) + provider[1:] + " (seed)" } // Resolve endpoint endpoint, ok := seedDefaultEndpoints[provider] if !ok { // Unknown provider — treat as openai-compatible with custom endpoint log.Printf("⚠ Seed provider '%s': unknown provider, skipping (no default endpoint)", provider) continue } if apiKey == "" { log.Printf("⚠ Seed provider '%s': empty API key, skipping", name) continue } // Check if already exists (idempotent) existing, _ := stores.Providers.ListGlobal(ctx) found := false for _, p := range existing { if p.Name == name || (p.Provider == provider && p.Scope == models.ScopeGlobal) { found = true break } } if found { log.Printf(" 🌱 Seed provider '%s' already exists, skipping", name) continue } // Create provider config pcfg := &models.ProviderConfig{ Name: name, Provider: provider, Endpoint: endpoint, Scope: models.ScopeGlobal, KeyScope: models.ScopeGlobal, IsActive: true, } // Encrypt API key if resolver != nil { enc, nonce, err := resolver.EncryptForScope(apiKey, "global", "") if err != nil { log.Printf("⚠ Seed provider '%s': encrypt failed: %v", name, err) continue } pcfg.APIKeyEnc = enc pcfg.KeyNonce = nonce } else { pcfg.APIKeyEnc = []byte(apiKey) } if err := stores.Providers.Create(ctx, pcfg); err != nil { log.Printf("⚠ Seed provider '%s': create failed: %v", name, err) continue } // Auto-fetch and enable models result, err := syncAndEnableProviderModels(ctx, stores, pcfg, apiKey) if err != nil { log.Printf(" 🌱 Seed provider '%s' created (model fetch failed: %v)", name, err) } else { log.Printf(" 🌱 Seed provider '%s' created (%d models fetched)", name, result.Total) } } }