Files
magpie/internal/provider/opencode_endpoints_test.go
yetone b0d456c3b2 providers: an OpenCode Go or Zen provider saved with its chat URL alone sends GPT, Grok, Muse Spark and MiniMax on the API OpenCode serves them on, not chat, and "Model does not support this protocol" says which URL was asked (#1215)
The reporter's "OpenCode Go 1x" had only chat=https://opencode.ai/zen/go/v1
and no models.dev catalog (its native_endpoints were /v1/chat/completions
for every model). Go serves each model on one API of three, as models.dev's
per-model provider.npm says and OpenCode itself asks it (@ai-sdk/openai ->
/responses, @ai-sdk/anthropic -> /messages, the rest chat), so with chat the
only URL, gpt-6-luna, gpt-5.6-luna, grok-4.6/4.7, minimax-m2.7 and
muse-spark-1.2/1.3-contributor had nowhere else to go and Go answered 400
on all three of magpie's entry points.

- normalize fills in the other two of Zen's or Go's APIs (/v1 for chat and
  Responses, the root for Messages) and its catalog, only where the
  provider has none of its own (openCodeEndpoints); other hosts unchanged.
- ListedAPIs reads the per-model API from the list of the gateway the URL
  is at (opencode-go for /zen/go, opencode for /zen), whatever catalog was
  saved: Go and Zen serve MiniMax M2.7 on different APIs, and an import at
  /zen/v1 could be saved with opencode-go's.
- A wrong-endpoint error with no other API left now ends "(asked at <url>;
  <model> is served on /v1/responses, which <provider> has no URL for)".

Verified: TestOpenCodeGoModelsGoOnTheirOwnAPI (gateway, all 3 client APIs x
5 models through the handler) and TestOpenCodeEndpointsFilledIn fail without
the change with the reporter's exact "OpenCode Go 1x: Model does not support
this protocol."; TestOtherRelayKeepsItsOneAPI and the relay cases keep
other providers as they were. go test -tags nogui ./... green under a temp
HOME; go vet, go build, GOOS=linux/windows builds. Real upstream: probed
https://opencode.ai/zen/go/v1 with no key only (grok-4.7 is "not supported
for format oa-compat" on chat and "format anthropic" on messages, passes to
the key check on responses); no keyed request was made.
2026-10-08 03:18:08 +08:00

79 lines
3.7 KiB
Go

package provider
import (
"os"
"path/filepath"
"slices"
"testing"
"github.com/yetone/magpie/internal/catalog"
)
// #1215: a provider at OpenCode's gateway saved with one URL gets the
// other two of that gateway's, and the models.dev list of the gateway its
// URL is at says which API each model goes on — Go's list for Go and
// Zen's for Zen, even when it was saved with the other's catalog. A URL
// or catalog the user gave stays theirs, and other hosts are left alone.
func TestOpenCodeEndpointsFilledIn(t *testing.T) {
isolate(t)
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
cache := t.TempDir()
t.Setenv("XDG_CACHE_HOME", cache)
os.MkdirAll(filepath.Join(cache, "magpie"), 0o755)
// as models.dev lists them (2026-10-08): MiniMax M2.7 is Anthropic's
// SDK on Go and the OpenAI-compatible one on Zen
if err := os.WriteFile(catalog.CachePath(), []byte(`{
"opencode-go": {"models": {
"gpt-6-luna": {"id":"gpt-6-luna","provider":{"npm":"@ai-sdk/openai"}},
"minimax-m2.7": {"id":"minimax-m2.7","provider":{"npm":"@ai-sdk/anthropic"}},
"glm-5.3": {"id":"glm-5.3"}}},
"opencode": {"models": {
"claude-opus-5": {"id":"claude-opus-5","provider":{"npm":"@ai-sdk/anthropic"}},
"minimax-m2.7": {"id":"minimax-m2.7"}}}
}`), 0o644); err != nil {
t.Fatal(err)
}
catalog.Reset()
t.Cleanup(catalog.Reset)
for _, tc := range []struct {
name string
in Provider
chat, responses, anthropic, catlog string
apis map[string]Protocol // "" for none named
}{
{"go, chat alone", Provider{ID: "opencode-go", Chat: "https://opencode.ai/zen/go/v1"},
"https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go", "opencode-go",
map[string]Protocol{"gpt-6-luna": Responses, "minimax-m2.7": Anthropic, "glm-5.3": ""}},
{"go, anthropic alone with /v1", Provider{ID: "ocgo", Anthropic: "https://opencode.ai/zen/go/v1"},
"https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go/v1", "https://opencode.ai/zen/go", "opencode-go",
map[string]Protocol{"gpt-6-luna": Responses}},
{"zen saved with go's catalog", Provider{ID: "opencode-go", Catalog: "opencode-go", Chat: "https://opencode.ai/zen/v1"},
"https://opencode.ai/zen/v1", "https://opencode.ai/zen/v1", "https://opencode.ai/zen", "opencode-go",
map[string]Protocol{"claude-opus-5": Anthropic, "minimax-m2.7": ""}},
{"a URL of the user's own stays", Provider{ID: "oc", Chat: "https://opencode.ai/zen/go/v1", Responses: "https://proxy.test/go/v1"},
"https://opencode.ai/zen/go/v1", "https://proxy.test/go/v1", "https://opencode.ai/zen/go", "opencode-go", nil},
{"another relay", Provider{ID: "relay", Catalog: "opencode-go", Chat: "https://relay.test/v1"},
"https://relay.test/v1", "", "", "opencode-go",
map[string]Protocol{"gpt-6-luna": ""}},
{"another path at opencode.ai", Provider{ID: "oc-other", Chat: "https://opencode.ai/other/v1"},
"https://opencode.ai/other/v1", "", "", "", nil},
} {
p := normalize(tc.in)
if p.Chat != tc.chat || p.Responses != tc.responses || p.Anthropic != tc.anthropic || p.Catalog != tc.catlog {
t.Errorf("%s: chat %q responses %q anthropic %q catalog %q", tc.name, p.Chat, p.Responses, p.Anthropic, p.Catalog)
}
for model, want := range tc.apis {
got := p.APIs(model)
if want == "" && got != nil || want != "" && !slices.Equal(got, []Protocol{want}) {
t.Errorf("%s: %s APIs %v, want %q", tc.name, model, got, want)
}
if want != "" && p.Native(model) != want {
t.Errorf("%s: %s native %q, want %q", tc.name, model, p.Native(model), want)
}
}
}
}