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_OPENAI_KEY API key (required for openai/openrouter/gemini) // 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 ~/.config/ollie/backend; errors if neither is set. func NewWithName(name string) (Backend, error) { loadEnvFile(paths.CfgDir() + "/env") if name == "" { name = readDefaultBackend() if name == "" { return nil, fmt.Errorf("no backend specified: set backend= in agent config or create ~/.config/ollie/backend") } } return newBackend(name) } // readDefaultBackend reads the default backend from ~/.config/ollie/backend. // Returns "" if the file doesn't exist or is empty. func readDefaultBackend() string { data, err := os.ReadFile(paths.CfgDir() + "/backend") if err != nil { return "" } return strings.TrimSpace(string(data)) } 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("OLLAMA_HOST")) case "openai": return NewOpenAI("openai", "https://api.openai.com/v1", os.Getenv("OLLIE_OPENAI_KEY")) case "openrouter": return NewOpenAI("openrouter", "https://openrouter.ai/api/v1", os.Getenv("OLLIE_OPENAI_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"} }