ollie/backend/new.go

152 lines
4.6 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_BACKEND ollama | openai | openrouter | anthropic | copilot | kiro | gemini
// 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("OLLAMA_HOST"))
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
}
}