This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/backend/new.go

133 lines
3.9 KiB
Go

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"}
}