From 62ac272db9b95293cdc6650dc4af7b65647031dc Mon Sep 17 00:00:00 2001 From: joo <14088938+sungizhou@user.noreply.gitee.com> Date: Fri, 29 May 2026 00:04:23 +0800 Subject: [PATCH] fix(core): preserve opencode provider settings Co-authored-by: Cursor --- core/internal/apply/target_opencode.go | 60 +------- core/internal/apply/target_opencode_test.go | 161 ++++++++++++++++++++ core/internal/buildinfo/buildinfo.go | 2 +- 3 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 core/internal/apply/target_opencode_test.go diff --git a/core/internal/apply/target_opencode.go b/core/internal/apply/target_opencode.go index 8b41976a..e38d1eb1 100644 --- a/core/internal/apply/target_opencode.go +++ b/core/internal/apply/target_opencode.go @@ -57,42 +57,20 @@ func (openCodeTarget) Apply(p profile.Profile) error { switch p.APIStyle { case apistyle.Claude: - ent := map[string]any{} - if cur, ok := prov["anthropic"].(map[string]any); ok && cur != nil { - ent = cur - } - opts := map[string]any{} - if o, ok := ent["options"].(map[string]any); ok && o != nil { - opts = o - } - opts["baseURL"] = ensureAnthropicWireBaseURL(p.BaseURL) - opts["apiKey"] = p.APIKey - ent["options"] = opts - prov["anthropic"] = ent - root["model"] = "anthropic/" + seg + prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/anthropic", seg, ensureAnthropicWireBaseURL(p.BaseURL)) + root["model"] = opencodeRelayID + "/" + seg case apistyle.OpenAIChat: - prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/openai-compatible", seg) + prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/openai-compatible", seg, ensureOpenCodeSDKBaseURL(p.BaseURL)) root["model"] = opencodeRelayID + "/" + seg case apistyle.OpenAIResponses: - prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/openai", seg) + prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/openai", seg, ensureOpenCodeSDKBaseURL(p.BaseURL)) root["model"] = opencodeRelayID + "/" + seg case apistyle.Gemini: - ent := map[string]any{} - if cur, ok := prov["gemini"].(map[string]any); ok && cur != nil { - ent = cur - } - opts := map[string]any{} - if o, ok := ent["options"].(map[string]any); ok && o != nil { - opts = o - } - opts["baseURL"] = ensureOpenCodeSDKBaseURL(p.BaseURL) - opts["apiKey"] = p.APIKey - ent["options"] = opts - prov["gemini"] = ent - root["model"] = "gemini/" + seg + prov[opencodeRelayID] = openCodeRelayEntry(p, "@ai-sdk/google", seg, ensureOpenCodeSDKBaseURL(p.BaseURL)) + root["model"] = opencodeRelayID + "/" + seg default: return fmt.Errorf("unsupported api style %q for opencode", p.APIStyle) @@ -124,28 +102,6 @@ func (openCodeTarget) ResetDefault() error { prov, _ := root["provider"].(map[string]any) if prov != nil { delete(prov, opencodeRelayID) - for _, id := range []string{"anthropic", "gemini"} { - ent, _ := prov[id].(map[string]any) - if ent == nil { - continue - } - opts, _ := ent["options"].(map[string]any) - if opts == nil { - continue - } - delete(opts, "baseURL") - delete(opts, "apiKey") - if len(opts) == 0 { - delete(ent, "options") - } else { - ent["options"] = opts - } - if len(ent) == 0 { - delete(prov, id) - } else { - prov[id] = ent - } - } if len(prov) == 0 { delete(root, "provider") } else { @@ -160,12 +116,12 @@ func (openCodeTarget) ResetDefault() error { return writeFileAtomic(writePath, out, 0o600) } -func openCodeRelayEntry(p profile.Profile, npm, modelSeg string) map[string]any { +func openCodeRelayEntry(p profile.Profile, npm, modelSeg, baseURL string) map[string]any { return map[string]any{ "npm": npm, "name": "clovapi", "options": map[string]any{ - "baseURL": ensureOpenCodeSDKBaseURL(p.BaseURL), + "baseURL": baseURL, "apiKey": p.APIKey, }, "models": map[string]any{ diff --git a/core/internal/apply/target_opencode_test.go b/core/internal/apply/target_opencode_test.go new file mode 100644 index 00000000..6adf1c1a --- /dev/null +++ b/core/internal/apply/target_opencode_test.go @@ -0,0 +1,161 @@ +package apply + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/clovapi/switcher/internal/agentkind" + "github.com/clovapi/switcher/internal/apistyle" + "github.com/clovapi/switcher/internal/profile" +) + +func TestOpenCodeApplyAndResetPreserveStockProviderSettings(t *testing.T) { + tests := []struct { + name string + style apistyle.Style + stockID string + stockBase string + stockKey string + wantNPM string + wantBase string + profileURL string + }{ + { + name: "claude preserves anthropic", + style: apistyle.Claude, + stockID: "anthropic", + stockBase: "https://api.anthropic.example/v1", + stockKey: "user-anthropic-key", + wantNPM: "@ai-sdk/anthropic", + wantBase: "http://127.0.0.1:8080/proxy/claude", + profileURL: "http://127.0.0.1:8080/proxy/claude/v1", + }, + { + name: "gemini preserves gemini", + style: apistyle.Gemini, + stockID: "gemini", + stockBase: "https://generativelanguage.googleapis.com/v1beta", + stockKey: "user-gemini-key", + wantNPM: "@ai-sdk/google", + wantBase: "http://127.0.0.1:8080/proxy/gemini/v1", + profileURL: "http://127.0.0.1:8080/proxy/gemini", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv(envOpenCodeDirOverride, filepath.Join(dir, ".config", "opencode")) + + configDir := filepath.Join(dir, ".config", "opencode") + if err := os.MkdirAll(configDir, 0o700); err != nil { + t.Fatal(err) + } + configPath := filepath.Join(configDir, "opencode.json") + writeOpenCodeTestConfig(t, configPath, map[string]any{ + "provider": map[string]any{ + tt.stockID: map[string]any{ + "options": map[string]any{ + "baseURL": tt.stockBase, + "apiKey": tt.stockKey, + }, + }, + }, + }) + + p := profile.Profile{ + CLI: agentkind.OpenCode, + APIStyle: tt.style, + BaseURL: tt.profileURL, + APIKey: "clovapi-key", + Model: "test-model", + } + if err := (openCodeTarget{}).Apply(p); err != nil { + t.Fatal(err) + } + + applied := readOpenCodeTestConfig(t, configPath) + assertOpenCodeStockProvider(t, applied, tt.stockID, tt.stockBase, tt.stockKey) + clovapi := providerEntry(t, applied, opencodeRelayID) + if clovapi["npm"] != tt.wantNPM { + t.Fatalf("clovapi npm: %v", clovapi["npm"]) + } + opts := mapEntry(t, clovapi, "options") + if opts["baseURL"] != tt.wantBase { + t.Fatalf("clovapi baseURL: %v", opts["baseURL"]) + } + if opts["apiKey"] != "clovapi-key" { + t.Fatalf("clovapi apiKey: %v", opts["apiKey"]) + } + if applied["model"] != opencodeRelayID+"/test-model" { + t.Fatalf("model after apply: %v", applied["model"]) + } + + if err := (openCodeTarget{}).ResetDefault(); err != nil { + t.Fatal(err) + } + + reset := readOpenCodeTestConfig(t, configPath) + assertOpenCodeStockProvider(t, reset, tt.stockID, tt.stockBase, tt.stockKey) + providers := mapEntry(t, reset, "provider") + if _, ok := providers[opencodeRelayID]; ok { + t.Fatalf("clovapi provider still present after reset: %#v", providers[opencodeRelayID]) + } + if _, ok := reset["model"]; ok { + t.Fatalf("model still present after reset: %v", reset["model"]) + } + }) + } +} + +func writeOpenCodeTestConfig(t *testing.T, path string, root map[string]any) { + t.Helper() + data, err := json.MarshalIndent(root, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatal(err) + } +} + +func readOpenCodeTestConfig(t *testing.T, path string) map[string]any { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var root map[string]any + if err := json.Unmarshal(data, &root); err != nil { + t.Fatal(err) + } + return root +} + +func assertOpenCodeStockProvider(t *testing.T, root map[string]any, id, wantBase, wantKey string) { + t.Helper() + opts := mapEntry(t, providerEntry(t, root, id), "options") + if opts["baseURL"] != wantBase { + t.Fatalf("%s baseURL: %v", id, opts["baseURL"]) + } + if opts["apiKey"] != wantKey { + t.Fatalf("%s apiKey: %v", id, opts["apiKey"]) + } +} + +func providerEntry(t *testing.T, root map[string]any, id string) map[string]any { + t.Helper() + return mapEntry(t, mapEntry(t, root, "provider"), id) +} + +func mapEntry(t *testing.T, root map[string]any, key string) map[string]any { + t.Helper() + v, ok := root[key].(map[string]any) + if !ok || v == nil { + t.Fatalf("%s is not an object: %#v", key, root[key]) + } + return v +} diff --git a/core/internal/buildinfo/buildinfo.go b/core/internal/buildinfo/buildinfo.go index f60bc3c4..54c2dfdd 100644 --- a/core/internal/buildinfo/buildinfo.go +++ b/core/internal/buildinfo/buildinfo.go @@ -4,7 +4,7 @@ import "strings" // Set at link time via -ldflags (see .goreleaser.yaml). var ( - Version = "dev0.1.42" + Version = "dev0.1.43" Commit = "none" Date = "unknown" )