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.
79 lines
3.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|