providers: OpenCode Zen's free models answer with no key, as OpenCode signed out asks them (#572: opencode zen的free模型,只能在他自己的cli里面用,能否做插件反代出来?) — magpie has asked Zen's free models as OpenCode does since v0.1.615 (zenfree.go), but the OpenCode Zen preset wanted a key, which means an opencode.ai account. OpenCode itself, signed out, asks Zen with the key "public", and Zen lists and serves its -free models to it (checked: /zen/v1/models with it lists twelve). The preset's key is now optional ("optional: free models need no key"), and a Zen provider saved with none asks with OpenCode's (normalize, provider.OpenCodeAnonymousKey); a key of the user's own is kept, and the editor's blank key still keeps the one saved. TestOpenCodeZenNeedsNoKey (keyless Add refused without the change) is new. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.

This commit is contained in:
yetone committed 2026-10-02 22:31:49 +08:00
1 parent d3c3059a19
commit b0486663d2
4 files changed
+53 -2

No files matched your search

+4
View File
@@ -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
+40
View File
@@ -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")
}
}
+3 -2
View File
@@ -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"},
+6
View File
@@ -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 = ""
}