mirror of
https://github.com/ollama/ollama.git
synced 2026-01-28 17:23:24 -05:00
225 lines
5.3 KiB
Go
225 lines
5.3 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// OpenCode implements Runner and Editor for OpenCode integration
|
|
type OpenCode struct{}
|
|
|
|
func (o *OpenCode) String() string { return "OpenCode" }
|
|
|
|
func (o *OpenCode) Run(model string) error {
|
|
if _, err := exec.LookPath("opencode"); err != nil {
|
|
return fmt.Errorf("opencode is not installed, install from https://opencode.ai")
|
|
}
|
|
|
|
// Call Edit() to ensure config is up-to-date before launch
|
|
models := []string{model}
|
|
if config, err := loadIntegration("opencode"); err == nil && len(config.Models) > 0 {
|
|
models = config.Models
|
|
}
|
|
if err := o.Edit(models); err != nil {
|
|
return fmt.Errorf("setup failed: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command("opencode")
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func (o *OpenCode) Paths() []string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var paths []string
|
|
p := filepath.Join(home, ".config", "opencode", "opencode.json")
|
|
if _, err := os.Stat(p); err == nil {
|
|
paths = append(paths, p)
|
|
}
|
|
sp := filepath.Join(home, ".local", "state", "opencode", "model.json")
|
|
if _, err := os.Stat(sp); err == nil {
|
|
paths = append(paths, sp)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (o *OpenCode) Edit(modelList []string) error {
|
|
if len(modelList) == 0 {
|
|
return nil
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := make(map[string]any)
|
|
if data, err := os.ReadFile(configPath); err == nil {
|
|
_ = json.Unmarshal(data, &config) // Ignore parse errors; treat missing/corrupt files as empty
|
|
}
|
|
|
|
config["$schema"] = "https://opencode.ai/config.json"
|
|
|
|
provider, ok := config["provider"].(map[string]any)
|
|
if !ok {
|
|
provider = make(map[string]any)
|
|
}
|
|
|
|
ollama, ok := provider["ollama"].(map[string]any)
|
|
if !ok {
|
|
ollama = map[string]any{
|
|
"npm": "@ai-sdk/openai-compatible",
|
|
"name": "Ollama (local)",
|
|
"options": map[string]any{
|
|
"baseURL": "http://localhost:11434/v1",
|
|
},
|
|
}
|
|
}
|
|
|
|
models, ok := ollama["models"].(map[string]any)
|
|
if !ok {
|
|
models = make(map[string]any)
|
|
}
|
|
|
|
selectedSet := make(map[string]bool)
|
|
for _, m := range modelList {
|
|
selectedSet[m] = true
|
|
}
|
|
|
|
for name, cfg := range models {
|
|
if cfgMap, ok := cfg.(map[string]any); ok {
|
|
if isOllamaModel(cfgMap) && !selectedSet[name] {
|
|
delete(models, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, model := range modelList {
|
|
if existing, ok := models[model].(map[string]any); ok {
|
|
// migrate existing models without _launch marker
|
|
if isOllamaModel(existing) {
|
|
existing["_launch"] = true
|
|
if name, ok := existing["name"].(string); ok {
|
|
existing["name"] = strings.TrimSuffix(name, " [Ollama]")
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
models[model] = map[string]any{
|
|
"name": model,
|
|
"_launch": true,
|
|
}
|
|
}
|
|
|
|
ollama["models"] = models
|
|
provider["ollama"] = ollama
|
|
config["provider"] = provider
|
|
|
|
configData, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeWithBackup(configPath, configData); err != nil {
|
|
return err
|
|
}
|
|
|
|
statePath := filepath.Join(home, ".local", "state", "opencode", "model.json")
|
|
if err := os.MkdirAll(filepath.Dir(statePath), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
state := map[string]any{
|
|
"recent": []any{},
|
|
"favorite": []any{},
|
|
"variant": map[string]any{},
|
|
}
|
|
if data, err := os.ReadFile(statePath); err == nil {
|
|
_ = json.Unmarshal(data, &state) // Ignore parse errors; use defaults
|
|
}
|
|
|
|
recent, _ := state["recent"].([]any)
|
|
|
|
modelSet := make(map[string]bool)
|
|
for _, m := range modelList {
|
|
modelSet[m] = true
|
|
}
|
|
|
|
// Filter out existing Ollama models we're about to re-add
|
|
newRecent := slices.DeleteFunc(slices.Clone(recent), func(entry any) bool {
|
|
e, ok := entry.(map[string]any)
|
|
if !ok || e["providerID"] != "ollama" {
|
|
return false
|
|
}
|
|
modelID, _ := e["modelID"].(string)
|
|
return modelSet[modelID]
|
|
})
|
|
|
|
// Prepend models in reverse order so first model ends up first
|
|
for _, model := range slices.Backward(modelList) {
|
|
newRecent = slices.Insert(newRecent, 0, any(map[string]any{
|
|
"providerID": "ollama",
|
|
"modelID": model,
|
|
}))
|
|
}
|
|
|
|
const maxRecentModels = 10
|
|
newRecent = newRecent[:min(len(newRecent), maxRecentModels)]
|
|
|
|
state["recent"] = newRecent
|
|
|
|
stateData, err := json.MarshalIndent(state, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeWithBackup(statePath, stateData)
|
|
}
|
|
|
|
func (o *OpenCode) Models() []string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
config, err := readJSONFile(filepath.Join(home, ".config", "opencode", "opencode.json"))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
provider, _ := config["provider"].(map[string]any)
|
|
ollama, _ := provider["ollama"].(map[string]any)
|
|
models, _ := ollama["models"].(map[string]any)
|
|
if len(models) == 0 {
|
|
return nil
|
|
}
|
|
keys := slices.Collect(maps.Keys(models))
|
|
slices.Sort(keys)
|
|
return keys
|
|
}
|
|
|
|
// isOllamaModel reports whether a model config entry is managed by us
|
|
func isOllamaModel(cfg map[string]any) bool {
|
|
if v, ok := cfg["_launch"].(bool); ok && v {
|
|
return true
|
|
}
|
|
// previously used [Ollama] as a suffix for the model managed by ollama launch
|
|
if name, ok := cfg["name"].(string); ok {
|
|
return strings.HasSuffix(name, "[Ollama]")
|
|
}
|
|
return false
|
|
}
|