diff --git a/cmd/config/opencode_test.go b/cmd/config/opencode_test.go index 9e4402e4f..552435453 100644 --- a/cmd/config/opencode_test.go +++ b/cmd/config/opencode_test.go @@ -3,6 +3,8 @@ package config import ( "encoding/json" "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -657,6 +659,54 @@ func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) { } } +func TestOpenCodeEdit_BackfillsCloudModelLimitOnExistingEntry(t *testing.T) { + o := &OpenCode{} + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/show" { + fmt.Fprintf(w, `{"capabilities":[],"model_info":{},"remote_model":"glm-5"}`) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer srv.Close() + t.Setenv("OLLAMA_HOST", srv.URL) + + configDir := filepath.Join(tmpDir, ".config", "opencode") + configPath := filepath.Join(configDir, "opencode.json") + os.MkdirAll(configDir, 0o755) + os.WriteFile(configPath, []byte(`{ + "provider": { + "ollama": { + "models": { + "glm-5:cloud": { + "name": "glm-5:cloud", + "_launch": true + } + } + } + } + }`), 0o644) + + if err := o.Edit([]string{"glm-5:cloud"}); err != nil { + t.Fatal(err) + } + + entry := readOpenCodeModel(t, configPath, "glm-5:cloud") + limit, ok := entry["limit"].(map[string]any) + if !ok { + t.Fatal("cloud model limit was not added on re-edit") + } + if limit["context"] != float64(202_752) { + t.Errorf("context = %v, want 202752", limit["context"]) + } + if limit["output"] != float64(131_072) { + t.Errorf("output = %v, want 131072", limit["output"]) + } +} + func TestLookupCloudModelLimit(t *testing.T) { tests := []struct { name string diff --git a/cmd/config/pi.go b/cmd/config/pi.go index e40b50db4..f2ae0c5f7 100644 --- a/cmd/config/pi.go +++ b/cmd/config/pi.go @@ -107,7 +107,8 @@ func (p *Pi) Edit(models []string) error { // Build new models list: // 1. Keep user-managed models (no _launch marker) - untouched - // 2. Keep ollama-managed models (_launch marker) that are still selected + // 2. Keep ollama-managed models (_launch marker) that are still selected, + // except stale cloud entries that should be rebuilt below // 3. Add new ollama-managed models var newModels []any for _, m := range existingModels { @@ -117,7 +118,13 @@ func (p *Pi) Edit(models []string) error { if !isPiOllamaModel(modelObj) { newModels = append(newModels, m) } else if selectedSet[id] { - // Ollama-managed and still selected - keep it + // Rebuild stale managed cloud entries so createConfig refreshes + // the whole entry instead of patching it in place. + if !hasContextWindow(modelObj) { + if _, ok := lookupCloudModelLimit(id); ok { + continue + } + } newModels = append(newModels, m) selectedSet[id] = false } @@ -199,6 +206,19 @@ func isPiOllamaModel(cfg map[string]any) bool { return false } +func hasContextWindow(cfg map[string]any) bool { + switch v := cfg["contextWindow"].(type) { + case float64: + return v > 0 + case int: + return v > 0 + case int64: + return v > 0 + default: + return false + } +} + // createConfig builds Pi model config with capability detection func createConfig(ctx context.Context, client *api.Client, modelID string) map[string]any { cfg := map[string]any{ diff --git a/cmd/config/pi_test.go b/cmd/config/pi_test.go index e6176d415..1ca572e81 100644 --- a/cmd/config/pi_test.go +++ b/cmd/config/pi_test.go @@ -192,6 +192,48 @@ func TestPiEdit(t *testing.T) { } }) + t.Run("rebuilds stale existing managed cloud model", func(t *testing.T) { + cleanup() + os.MkdirAll(configDir, 0o755) + + existingConfig := `{ + "providers": { + "ollama": { + "baseUrl": "http://localhost:11434/v1", + "api": "openai-completions", + "apiKey": "ollama", + "models": [ + {"id": "glm-5:cloud", "_launch": true, "legacyField": "stale"} + ] + } + } + }` + if err := os.WriteFile(configPath, []byte(existingConfig), 0o644); err != nil { + t.Fatal(err) + } + + if err := pi.Edit([]string{"glm-5:cloud"}); err != nil { + t.Fatalf("Edit() error = %v", err) + } + + cfg := readConfig() + providers := cfg["providers"].(map[string]any) + ollama := providers["ollama"].(map[string]any) + modelsArray := ollama["models"].([]any) + modelEntry := modelsArray[0].(map[string]any) + + if modelEntry["contextWindow"] != float64(202_752) { + t.Errorf("contextWindow = %v, want 202752", modelEntry["contextWindow"]) + } + input, ok := modelEntry["input"].([]any) + if !ok || len(input) != 1 || input[0] != "text" { + t.Errorf("input = %v, want [text]", modelEntry["input"]) + } + if _, ok := modelEntry["legacyField"]; ok { + t.Error("legacyField should be removed when stale managed cloud entry is rebuilt") + } + }) + t.Run("replaces old models with new ones", func(t *testing.T) { cleanup() os.MkdirAll(configDir, 0o755)