From b0486663d225b4a2601bd0e59dd0b1dd6e94fb85 Mon Sep 17 00:00:00 2001 From: yetone Date: Fri, 2 Oct 2026 22:31:49 +0800 Subject: [PATCH] =?UTF-8?q?providers:=20OpenCode=20Zen's=20free=20models?= =?UTF-8?q?=20answer=20with=20no=20key,=20as=20OpenCode=20signed=20out=20a?= =?UTF-8?q?sks=20them=20(#572:=20opencode=20zen=E7=9A=84free=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=8C=E5=8F=AA=E8=83=BD=E5=9C=A8=E4=BB=96=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E7=9A=84cli=E9=87=8C=E9=9D=A2=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E8=83=BD=E5=90=A6=E5=81=9A=E6=8F=92=E4=BB=B6=E5=8F=8D=E4=BB=A3?= =?UTF-8?q?=E5=87=BA=E6=9D=A5=EF=BC=9F)=20=E2=80=94=20magpie=20has=20asked?= =?UTF-8?q?=20Zen's=20free=20models=20as=20OpenCode=20does=20since=20v0.1.?= =?UTF-8?q?615=20(zenfree.go),=20but=20the=20OpenCode=20Zen=20preset=20wan?= =?UTF-8?q?ted=20a=20key,=20which=20means=20an=20opencode.ai=20account.=20?= =?UTF-8?q?OpenCode=20itself,=20signed=20out,=20asks=20Zen=20with=20the=20?= =?UTF-8?q?key=20"public",=20and=20Zen=20lists=20and=20serves=20its=20-fre?= =?UTF-8?q?e=20models=20to=20it=20(checked:=20/zen/v1/models=20with=20it?= =?UTF-8?q?=20lists=20twelve).=20The=20preset's=20key=20is=20now=20optiona?= =?UTF-8?q?l=20("optional:=20free=20models=20need=20no=20key"),=20and=20a?= =?UTF-8?q?=20Zen=20provider=20saved=20with=20none=20asks=20with=20OpenCod?= =?UTF-8?q?e's=20(normalize,=20provider.OpenCodeAnonymousKey);=20a=20key?= =?UTF-8?q?=20of=20the=20user's=20own=20is=20kept,=20and=20the=20editor's?= =?UTF-8?q?=20blank=20key=20still=20keeps=20the=20one=20saved.=20TestOpenC?= =?UTF-8?q?odeZenNeedsNoKey=20(keyless=20Add=20refused=20without=20the=20c?= =?UTF-8?q?hange)=20is=20new.=20go=20vet=20./internal/...,=20go=20build=20?= =?UTF-8?q?-tags=20nogui=20(darwin,=20GOOS=3Dlinux),=20GOOS=3Dwindows=20go?= =?UTF-8?q?=20build,=20go=20test=20-tags=20nogui=20-count=3D1=20./...=20un?= =?UTF-8?q?der=20a=20temp=20HOME=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/opencode_client.go | 4 +++ internal/provider/opencode_zen_test.go | 40 ++++++++++++++++++++++++++ internal/provider/presets.go | 5 ++-- internal/provider/provider.go | 6 ++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/internal/provider/opencode_client.go b/internal/provider/opencode_client.go index 73d3405e..19068a18 100644 --- a/internal/provider/opencode_client.go +++ b/internal/provider/opencode_client.go @@ -15,6 +15,10 @@ import ( // "OpenCode 1.18.0 or newer is required to use the free tier"). const OpenCodeVersion = "1.18.34" +// OpenCodeAnonymousKey is the key OpenCode asks Zen with when no one is +// signed in to it, which Zen's free models answer. +const OpenCodeAnonymousKey = "public" + // OpenCodeClient makes a request to OpenCode's gateway (Zen or Go) carry // what OpenCode itself sends there (packages/opencode/src/session/llm/ // request.ts, for a provider whose id starts with "opencode"): its diff --git a/internal/provider/opencode_zen_test.go b/internal/provider/opencode_zen_test.go index 028b7751..5d0c9b36 100644 --- a/internal/provider/opencode_zen_test.go +++ b/internal/provider/opencode_zen_test.go @@ -2,6 +2,7 @@ package provider import ( "encoding/json" + "path/filepath" "testing" "github.com/yetone/magpie/internal/catalog" @@ -43,3 +44,42 @@ func TestZenFreeProbe(t *testing.T) { } } } + +// #572: an OpenCode Zen provider added with no key asks with the one +// OpenCode sends signed out, which Zen's free models answer; a key of the +// user's own is kept. +func TestOpenCodeZenNeedsNoKey(t *testing.T) { + isolate(t) + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) + t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache")) + p, err := FromPreset("opencode-zen") + if err != nil { + t.Fatal(err) + } + p.Models = []string{"mimo-v2.6-flash-free"} + id, err := Add(p) + if err != nil { + t.Fatal(err) + } + got, err := Find(id) + if err != nil { + t.Fatal(err) + } + if got.Key != OpenCodeAnonymousKey { + t.Fatalf("key %q", got.Key) + } + p.Name, p.Key = "Zen paid", "sk-own" + if id, err = Add(p); err != nil { + t.Fatal(err) + } + if got, _ = Find(id); got.Key != "sk-own" { + t.Fatalf("own key %q", got.Key) + } + // another provider still needs one + if _, err := Add(Provider{Name: "Relay", Chat: "https://api.relay.example/v1", Models: []string{"m"}}); err == nil { + t.Fatal("a relay with no key") + } +} diff --git a/internal/provider/presets.go b/internal/provider/presets.go index 4d4038db..a9fa6531 100644 --- a/internal/provider/presets.go +++ b/internal/provider/presets.go @@ -312,8 +312,9 @@ var presets = []PresetDef{ Models: []string{"cline-pass/glm-5.3", "cline-pass/glm-5.3-flash", "cline-pass/kimi-k3", "cline-pass/deepseek-v4-pro", "cline-pass/deepseek-v4.1-flash", "cline-pass/mimo-v2.5", "cline-pass/mimo-v2.5-pro", "cline-pass/minimax-m3", "cline-pass/muse-spark-1.3-contributor", "cline-pass/qwen3.8-max", "cline-pass/qwen3.7-max", "cline-pass/qwen3.7-plus"}}, - {ID: "opencode-zen", Name: "OpenCode Zen", Icon: "opencode", Kind: KindRelay, Catalog: "opencode", - Chat: "https://opencode.ai/zen/v1", Responses: "https://opencode.ai/zen/v1", Anthropic: "https://opencode.ai/zen", + {ID: "opencode-zen", Name: "OpenCode Zen", Icon: "opencode", Kind: KindRelay, Catalog: "opencode", NoKey: true, + KeyHint: "optional: free models need no key", + Chat: "https://opencode.ai/zen/v1", Responses: "https://opencode.ai/zen/v1", Anthropic: "https://opencode.ai/zen", // its free models (-free) are served to OpenCode alone, which // magpie asks them as (OpenCodeFree) Website: "https://opencode.ai/docs/zen", KeysURL: "https://opencode.ai/auth"}, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f47744fb..c648777e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -694,6 +694,12 @@ func normalize(p Provider) Provider { if p.Preset == "qianfan-token-plan" { p.Preset = "baidu-qianfan" } + // OpenCode Zen serves its free models (-free) signed out, to the key + // OpenCode itself sends then: a Zen provider saved with no key of its + // own asks with that one + if p.Preset == "opencode-zen" && p.Key == "" { + p.Key = OpenCodeAnonymousKey + } if p.Routing != Ordered && p.Routing != Rotate && p.Routing != LeastUsed && p.Routing != Pace { p.Routing = "" }