152 lines
4.6 KiB
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
|
|
}
|
|
}
|