From 8a4b77f9daccc2509596753c0cb5564918b4ada0 Mon Sep 17 00:00:00 2001 From: Parth Sareen Date: Thu, 5 Feb 2026 19:36:46 -0500 Subject: [PATCH] cmd: set context limits for cloud models in opencode (#14107) --- cmd/config/integrations.go | 2 + cmd/config/integrations_test.go | 22 +++++ cmd/config/opencode.go | 62 ++++++++++++- cmd/config/opencode_test.go | 160 ++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 1 deletion(-) diff --git a/cmd/config/integrations.go b/cmd/config/integrations.go index 6be2f1dc2..69bf55a62 100644 --- a/cmd/config/integrations.go +++ b/cmd/config/integrations.go @@ -482,6 +482,8 @@ Examples: } } } + } else if saved, err := loadIntegration(name); err == nil && len(saved.Models) > 0 && !configFlag { + return runIntegration(name, saved.Models[0], passArgs) } else { var err error models, err = selectModels(cmd.Context(), name, "") diff --git a/cmd/config/integrations_test.go b/cmd/config/integrations_test.go index b14906db5..4c0eb4a05 100644 --- a/cmd/config/integrations_test.go +++ b/cmd/config/integrations_test.go @@ -502,6 +502,28 @@ func TestBuildModelList_ReturnsExistingAndCloudMaps(t *testing.T) { } } +func TestEditorIntegration_SavedConfigSkipsSelection(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + // Save a config for opencode so it looks like a previous launch + if err := saveIntegration("opencode", []string{"llama3.2"}); err != nil { + t.Fatal(err) + } + + // Verify loadIntegration returns the saved models + saved, err := loadIntegration("opencode") + if err != nil { + t.Fatal(err) + } + if len(saved.Models) == 0 { + t.Fatal("expected saved models") + } + if saved.Models[0] != "llama3.2" { + t.Errorf("expected llama3.2, got %s", saved.Models[0]) + } +} + func TestAliasConfigurerInterface(t *testing.T) { t.Run("claude implements AliasConfigurer", func(t *testing.T) { claude := &Claude{} diff --git a/cmd/config/opencode.go b/cmd/config/opencode.go index 06fce2743..59a6f0119 100644 --- a/cmd/config/opencode.go +++ b/cmd/config/opencode.go @@ -1,6 +1,7 @@ package config import ( + "context" "encoding/json" "fmt" "maps" @@ -10,12 +11,52 @@ import ( "slices" "strings" + "github.com/ollama/ollama/api" "github.com/ollama/ollama/envconfig" ) // OpenCode implements Runner and Editor for OpenCode integration type OpenCode struct{} +// cloudModelLimit holds context and output token limits for a cloud model. +type cloudModelLimit struct { + Context int + Output int +} + +// cloudModelLimits maps cloud model base names to their token limits. +// TODO(parthsareen): grab context/output limits from model info instead of hardcoding +var cloudModelLimits = map[string]cloudModelLimit{ + "cogito-2.1:671b": {Context: 163_840, Output: 65_536}, + "deepseek-v3.1:671b": {Context: 163_840, Output: 163_840}, + "deepseek-v3.2": {Context: 163_840, Output: 65_536}, + "glm-4.6": {Context: 202_752, Output: 131_072}, + "glm-4.7": {Context: 202_752, Output: 131_072}, + "gpt-oss:120b": {Context: 131_072, Output: 131_072}, + "gpt-oss:20b": {Context: 131_072, Output: 131_072}, + "kimi-k2:1t": {Context: 262_144, Output: 262_144}, + "kimi-k2.5": {Context: 262_144, Output: 262_144}, + "kimi-k2-thinking": {Context: 262_144, Output: 262_144}, + "nemotron-3-nano:30b": {Context: 1_048_576, Output: 131_072}, + "qwen3-coder:480b": {Context: 262_144, Output: 65_536}, + "qwen3-next:80b": {Context: 262_144, Output: 32_768}, +} + +// lookupCloudModelLimit returns the token limits for a cloud model. +// It tries the exact name first, then strips the ":cloud" suffix. +func lookupCloudModelLimit(name string) (cloudModelLimit, bool) { + if l, ok := cloudModelLimits[name]; ok { + return l, true + } + base := strings.TrimSuffix(name, ":cloud") + if base != name { + if l, ok := cloudModelLimits[base]; ok { + return l, true + } + } + return cloudModelLimit{}, false +} + func (o *OpenCode) String() string { return "OpenCode" } func (o *OpenCode) Run(model string, args []string) error { @@ -113,6 +154,8 @@ func (o *OpenCode) Edit(modelList []string) error { } } + client, _ := api.ClientFromEnvironment() + for _, model := range modelList { if existing, ok := models[model].(map[string]any); ok { // migrate existing models without _launch marker @@ -122,12 +165,29 @@ func (o *OpenCode) Edit(modelList []string) error { existing["name"] = strings.TrimSuffix(name, " [Ollama]") } } + if isCloudModel(context.Background(), client, model) { + if l, ok := lookupCloudModelLimit(model); ok { + existing["limit"] = map[string]any{ + "context": l.Context, + "output": l.Output, + } + } + } continue } - models[model] = map[string]any{ + entry := map[string]any{ "name": model, "_launch": true, } + if isCloudModel(context.Background(), client, model) { + if l, ok := lookupCloudModelLimit(model); ok { + entry["limit"] = map[string]any{ + "context": l.Context, + "output": l.Output, + } + } + } + models[model] = entry } ollama["models"] = models diff --git a/cmd/config/opencode_test.go b/cmd/config/opencode_test.go index de8a9efd2..9dc85dd10 100644 --- a/cmd/config/opencode_test.go +++ b/cmd/config/opencode_test.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "fmt" "os" "path/filepath" "testing" @@ -495,6 +496,165 @@ func TestOpenCodeEdit_SpecialCharsInModelName(t *testing.T) { } } +func readOpenCodeModel(t *testing.T, configPath, model string) map[string]any { + t.Helper() + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + var cfg map[string]any + json.Unmarshal(data, &cfg) + provider := cfg["provider"].(map[string]any) + ollama := provider["ollama"].(map[string]any) + models := ollama["models"].(map[string]any) + entry, ok := models[model].(map[string]any) + if !ok { + t.Fatalf("model %s not found in config", model) + } + return entry +} + +func TestOpenCodeEdit_LocalModelNoLimit(t *testing.T) { + o := &OpenCode{} + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + configPath := filepath.Join(tmpDir, ".config", "opencode", "opencode.json") + + if err := o.Edit([]string{"llama3.2"}); err != nil { + t.Fatal(err) + } + + entry := readOpenCodeModel(t, configPath, "llama3.2") + if entry["limit"] != nil { + t.Errorf("local model should not have limit set, got %v", entry["limit"]) + } +} + +func TestOpenCodeEdit_PreservesUserLimit(t *testing.T) { + o := &OpenCode{} + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + configDir := filepath.Join(tmpDir, ".config", "opencode") + configPath := filepath.Join(configDir, "opencode.json") + + // Set up a model with a user-configured limit + os.MkdirAll(configDir, 0o755) + os.WriteFile(configPath, []byte(`{ + "provider": { + "ollama": { + "models": { + "llama3.2": { + "name": "llama3.2", + "_launch": true, + "limit": {"context": 8192, "output": 4096} + } + } + } + } + }`), 0o644) + + // Re-edit should preserve the user's limit (not delete it) + if err := o.Edit([]string{"llama3.2"}); err != nil { + t.Fatal(err) + } + + entry := readOpenCodeModel(t, configPath, "llama3.2") + limit, ok := entry["limit"].(map[string]any) + if !ok { + t.Fatal("user-configured limit was removed") + } + if limit["context"] != float64(8192) { + t.Errorf("context limit changed: got %v, want 8192", limit["context"]) + } + if limit["output"] != float64(4096) { + t.Errorf("output limit changed: got %v, want 4096", limit["output"]) + } +} + +func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) { + // Verify that when a cloud model entry has limits set (as Edit would do), + // the structure matches what opencode expects and re-edit preserves them. + o := &OpenCode{} + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + configDir := filepath.Join(tmpDir, ".config", "opencode") + configPath := filepath.Join(configDir, "opencode.json") + + expected := cloudModelLimits["glm-4.7"] + + // Simulate a cloud model that already has the limit set by a previous Edit + os.MkdirAll(configDir, 0o755) + os.WriteFile(configPath, []byte(fmt.Sprintf(`{ + "provider": { + "ollama": { + "models": { + "glm-4.7:cloud": { + "name": "glm-4.7:cloud", + "_launch": true, + "limit": {"context": %d, "output": %d} + } + } + } + } + }`, expected.Context, expected.Output)), 0o644) + + // Re-edit should preserve the cloud model limit + if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil { + t.Fatal(err) + } + + entry := readOpenCodeModel(t, configPath, "glm-4.7:cloud") + limit, ok := entry["limit"].(map[string]any) + if !ok { + t.Fatal("cloud model limit was removed on re-edit") + } + if limit["context"] != float64(expected.Context) { + t.Errorf("context = %v, want %d", limit["context"], expected.Context) + } + if limit["output"] != float64(expected.Output) { + t.Errorf("output = %v, want %d", limit["output"], expected.Output) + } +} + +func TestLookupCloudModelLimit(t *testing.T) { + tests := []struct { + name string + wantOK bool + wantContext int + wantOutput int + }{ + {"glm-4.7", true, 202_752, 131_072}, + {"glm-4.7:cloud", true, 202_752, 131_072}, + {"kimi-k2.5", true, 262_144, 262_144}, + {"kimi-k2.5:cloud", true, 262_144, 262_144}, + {"deepseek-v3.2", true, 163_840, 65_536}, + {"deepseek-v3.2:cloud", true, 163_840, 65_536}, + {"qwen3-coder:480b", true, 262_144, 65_536}, + {"llama3.2", false, 0, 0}, + {"unknown-model:cloud", false, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l, ok := lookupCloudModelLimit(tt.name) + if ok != tt.wantOK { + t.Errorf("lookupCloudModelLimit(%q) ok = %v, want %v", tt.name, ok, tt.wantOK) + } + if ok { + if l.Context != tt.wantContext { + t.Errorf("context = %d, want %d", l.Context, tt.wantContext) + } + if l.Output != tt.wantOutput { + t.Errorf("output = %d, want %d", l.Output, tt.wantOutput) + } + } + }) + } +} + func TestOpenCodeModels_NoConfig(t *testing.T) { o := &OpenCode{} tmpDir := t.TempDir()