package backend import ( "bufio" "fmt" "os" "strings" "ollie/paths" ) // loadEnvFile reads KEY=VALUE pairs from path and sets any key that is not // already present in the environment. Lines beginning with # and blank lines // are ignored. Errors opening the file are silently ignored (file is optional). func loadEnvFile(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, "=") if !ok { continue } k = strings.TrimSpace(k) if i := strings.Index(v, " #"); i >= 0 { v = v[:i] } v = strings.TrimSpace(v) if len(v) >= 2 && (v[0] == '"' && v[len(v)-1] == '"' || v[0] == '\'' && v[len(v)-1] == '\'') { v = v[1 : len(v)-1] } if k != "" && os.Getenv(k) == "" { os.Setenv(k, v) } } } // New constructs a Backend from environment variables. // Values are loaded from ~/.config/ollie/env before consulting the environment, // so the file acts as a default; variables already set in the environment take // precedence. // // OLLIE_BACKEND ollama | openai | openrouter | anthropic | copilot | kiro | gemini // OLLIE_OLLAMA_URL base URL for ollama (default: http://localhost:11434) // OLLIE_OPENAI_URL base URL for openai-compatible backends // OLLIE_OPENAI_KEY API key (required for openai/openrouter) // OLLIE_ANTHROPIC_KEY API key (required for anthropic) // OLLIE_GEMINI_KEY Google AI Studio API key (required for gemini; falls back to OLLIE_OPENAI_KEY) // OLLIE_COPILOT_TOKEN bearer token (required for copilot) // OLLIE_KIRO_TOKEN bearer token or sqlite:// URL for kiro/codewhisperer // (default: sqlite path auto-detected from Kiro CLI data dir) func New() (Backend, error) { return newFromEnv(paths.CfgDir() + "/env") } // NewWithName constructs a Backend for the given backend name, loading // env-file defaults for API keys/URLs but ignoring OLLIE_BACKEND. // If name is empty, falls back to OLLIE_BACKEND; errors if neither is set. func NewWithName(name string) (Backend, error) { loadEnvFile(paths.CfgDir() + "/env") if name == "" { name = os.Getenv("OLLIE_BACKEND") if name == "" { return nil, fmt.Errorf("no backend specified: set OLLIE_BACKEND or pass backend= to the session") } } return newBackend(name) } func newFromEnv(envFile string) (Backend, error) { loadEnvFile(envFile) which := os.Getenv("OLLIE_BACKEND") if which == "" { return nil, fmt.Errorf("OLLIE_BACKEND is not set") } return newBackend(which) } func newBackend(which string) (Backend, error) { switch which { case "ollama": return NewOllama(os.Getenv("OLLIE_OLLAMA_URL")) case "openai", "openrouter": url := os.Getenv("OLLIE_OPENAI_URL") key := os.Getenv("OLLIE_OPENAI_KEY") if key == "" { return nil, fmt.Errorf("OLLIE_OPENAI_KEY is required for %s backend", which) } return NewOpenAI(openAIName(which, url), url, key) case "anthropic": key := os.Getenv("OLLIE_ANTHROPIC_KEY") if key == "" { return nil, fmt.Errorf("OLLIE_ANTHROPIC_KEY is required for anthropic backend") } return NewAnthropic(key) case "copilot": token := os.Getenv("OLLIE_COPILOT_TOKEN") if token == "" { return nil, fmt.Errorf("OLLIE_COPILOT_TOKEN is required for copilot backend") } return NewCopilot(token) case "kiro", "codewhisperer": return NewCodeWhisperer(os.Getenv("OLLIE_KIRO_TOKEN")) case "gemini": key := geminiKey() if key == "" { return nil, fmt.Errorf("OLLIE_GEMINI_KEY is required for gemini backend") } return NewGemini(key) default: return nil, fmt.Errorf("unknown OLLIE_BACKEND %q (supported: ollama, openai, openrouter, anthropic, copilot, kiro, gemini)", which) } } // Backends returns the list of supported backend names. func Backends() []string { return []string{"ollama", "openai", "openrouter", "anthropic", "copilot", "kiro", "gemini"} } // openAIName derives a short backend label from the OLLIE_BACKEND value and // the base URL, so openai-compatible endpoints self-identify correctly. func openAIName(which, url string) string { url = strings.ToLower(url) switch { case strings.Contains(url, "openrouter"): return "openrouter" case strings.Contains(url, "together"): return "together" case strings.Contains(url, "groq"): return "groq" case strings.Contains(url, "mistral"): return "mistral" case strings.Contains(url, "anthropic"): return "anthropic" case strings.Contains(url, "generativelanguage") || strings.Contains(url, "google"): return "gemini" case strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1"): return "local" default: return which } }