mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 15:53:27 +02:00
launch/opencode: use inline config (#15462)
This commit is contained in:
@@ -3,20 +3,22 @@ package launch
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ollama/ollama/cmd/internal/fileutil"
|
"github.com/ollama/ollama/cmd/internal/fileutil"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenCode implements Runner and Editor for OpenCode integration
|
// OpenCode implements Runner and Editor for OpenCode integration.
|
||||||
type OpenCode struct{}
|
// Config is passed via OPENCODE_CONFIG_CONTENT env var at launch time
|
||||||
|
// instead of writing to opencode's config files.
|
||||||
|
type OpenCode struct {
|
||||||
|
configContent string // JSON config built by Edit, passed to Run via env var
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenCode) String() string { return "OpenCode" }
|
func (o *OpenCode) String() string { return "OpenCode" }
|
||||||
|
|
||||||
@@ -51,25 +53,51 @@ func (o *OpenCode) Run(model string, args []string) error {
|
|||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
if content := o.resolveContent(model); content != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "OPENCODE_CONFIG_CONTENT="+content)
|
||||||
|
}
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveContent returns the inline config to send via OPENCODE_CONFIG_CONTENT.
|
||||||
|
// Returns content built by Edit if available, otherwise builds from model.json
|
||||||
|
// with the requested model as primary (e.g. re-launch with saved config).
|
||||||
|
func (o *OpenCode) resolveContent(model string) string {
|
||||||
|
if o.configContent != "" {
|
||||||
|
return o.configContent
|
||||||
|
}
|
||||||
|
models := readModelJSONModels()
|
||||||
|
if !slices.Contains(models, model) {
|
||||||
|
models = append([]string{model}, models...)
|
||||||
|
}
|
||||||
|
content, err := buildInlineConfig(model, models)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenCode) Paths() []string {
|
func (o *OpenCode) Paths() []string {
|
||||||
home, err := os.UserHomeDir()
|
sp, err := openCodeStatePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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 {
|
if _, err := os.Stat(sp); err == nil {
|
||||||
paths = append(paths, sp)
|
return []string{sp}
|
||||||
}
|
}
|
||||||
return paths
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openCodeStatePath returns the path to opencode's model state file.
|
||||||
|
// TODO: this hardcodes the Linux/macOS XDG path. On Windows, opencode stores
|
||||||
|
// state under %LOCALAPPDATA% (or similar) — verify and branch on runtime.GOOS.
|
||||||
|
func openCodeStatePath() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".local", "state", "opencode", "model.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenCode) Edit(modelList []string) error {
|
func (o *OpenCode) Edit(modelList []string) error {
|
||||||
@@ -77,110 +105,17 @@ func (o *OpenCode) Edit(modelList []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
content, err := buildInlineConfig(modelList[0], modelList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
o.configContent = content
|
||||||
|
|
||||||
configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
|
// Write model state file so models appear in OpenCode's model picker
|
||||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
statePath, err := openCodeStatePath()
|
||||||
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",
|
|
||||||
"options": map[string]any{
|
|
||||||
"baseURL": envconfig.Host().String() + "/v1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate legacy provider name
|
|
||||||
if name, _ := ollama["name"].(string); name == "Ollama (local)" {
|
|
||||||
ollama["name"] = "Ollama"
|
|
||||||
}
|
|
||||||
|
|
||||||
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]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isCloudModelName(model) {
|
|
||||||
if l, ok := lookupCloudModelLimit(model); ok {
|
|
||||||
existing["limit"] = map[string]any{
|
|
||||||
"context": l.Context,
|
|
||||||
"output": l.Output,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
entry := map[string]any{
|
|
||||||
"name": model,
|
|
||||||
"_launch": true,
|
|
||||||
}
|
|
||||||
if isCloudModelName(model) {
|
|
||||||
if l, ok := lookupCloudModelLimit(model); ok {
|
|
||||||
entry["limit"] = map[string]any{
|
|
||||||
"context": l.Context,
|
|
||||||
"output": l.Output,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
models[model] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
ollama["models"] = models
|
|
||||||
provider["ollama"] = ollama
|
|
||||||
config["provider"] = provider
|
|
||||||
config["model"] = "ollama/" + modelList[0]
|
|
||||||
|
|
||||||
configData, err := json.MarshalIndent(config, "", " ")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fileutil.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 {
|
if err := os.MkdirAll(filepath.Dir(statePath), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -232,33 +167,82 @@ func (o *OpenCode) Edit(modelList []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenCode) Models() []string {
|
func (o *OpenCode) Models() []string {
|
||||||
home, err := os.UserHomeDir()
|
return nil
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
config, err := fileutil.ReadJSON(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
|
// buildInlineConfig produces the JSON string for OPENCODE_CONFIG_CONTENT.
|
||||||
func isOllamaModel(cfg map[string]any) bool {
|
// primary is the model to launch with, models is the full list of available models.
|
||||||
if v, ok := cfg["_launch"].(bool); ok && v {
|
func buildInlineConfig(primary string, models []string) (string, error) {
|
||||||
return true
|
if primary == "" || len(models) == 0 {
|
||||||
|
return "", fmt.Errorf("buildInlineConfig: primary and models are required")
|
||||||
}
|
}
|
||||||
// previously used [Ollama] as a suffix for the model managed by ollama launch
|
config := map[string]any{
|
||||||
if name, ok := cfg["name"].(string); ok {
|
"$schema": "https://opencode.ai/config.json",
|
||||||
return strings.HasSuffix(name, "[Ollama]")
|
"provider": map[string]any{
|
||||||
|
"ollama": map[string]any{
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "Ollama",
|
||||||
|
"options": map[string]any{
|
||||||
|
"baseURL": envconfig.Host().String() + "/v1",
|
||||||
|
},
|
||||||
|
"models": buildModelEntries(models),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"model": "ollama/" + primary,
|
||||||
}
|
}
|
||||||
return false
|
data, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readModelJSONModels reads ollama model IDs from the opencode model.json state file
|
||||||
|
func readModelJSONModels() []string {
|
||||||
|
statePath, err := openCodeStatePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(statePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var state map[string]any
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
recent, _ := state["recent"].([]any)
|
||||||
|
var models []string
|
||||||
|
for _, entry := range recent {
|
||||||
|
e, ok := entry.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e["providerID"] != "ollama" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if id, ok := e["modelID"].(string); ok && id != "" {
|
||||||
|
models = append(models, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildModelEntries(modelList []string) map[string]any {
|
||||||
|
models := make(map[string]any)
|
||||||
|
for _, model := range modelList {
|
||||||
|
entry := map[string]any{
|
||||||
|
"name": model,
|
||||||
|
}
|
||||||
|
if isCloudModelName(model) {
|
||||||
|
if l, ok := lookupCloudModelLimit(model); ok {
|
||||||
|
entry["limit"] = map[string]any{
|
||||||
|
"context": l.Context,
|
||||||
|
"output": l.Output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
models[model] = entry
|
||||||
|
}
|
||||||
|
return models
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ func TestEditorRunsDoNotRewriteConfig(t *testing.T) {
|
|||||||
binary: "opencode",
|
binary: "opencode",
|
||||||
runner: &OpenCode{},
|
runner: &OpenCode{},
|
||||||
checkPath: func(home string) string {
|
checkPath: func(home string) string {
|
||||||
return filepath.Join(home, ".config", "opencode", "opencode.json")
|
return filepath.Join(home, ".local", "state", "opencode", "model.json")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user