Files
magpie/internal/provider/opencode_endpoints.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

71 lines
2.7 KiB
Go

package provider
import (
"cmp"
"net/url"
"strings"
)
// openCodeRoot is the root of OpenCode's gateway a base URL is at — Zen's
// (https://opencode.ai/zen) or Go's (…/zen/go), given with /v1 or without —
// and the models.dev provider that lists its models: opencode or
// opencode-go. Both are "" for any other URL.
func openCodeRoot(base string) (root, catalogID string) {
u, err := url.Parse(strings.TrimRight(strings.TrimSpace(base), "/"))
if err != nil || u.Scheme == "" {
return "", ""
}
if h := strings.ToLower(u.Hostname()); h != "opencode.ai" && !strings.HasSuffix(h, ".opencode.ai") {
return "", ""
}
switch path := strings.TrimSuffix(strings.TrimRight(u.Path, "/"), "/v1"); path {
case "/zen/go":
return u.Scheme + "://" + u.Host + path, "opencode-go"
case "/zen":
return u.Scheme + "://" + u.Host + path, "opencode"
}
return "", ""
}
// openCodeEndpoints fills in the APIs of OpenCode's gateway a provider at
// it was saved without. Zen and Go serve each model on one API of three,
// all under one root — chat completions and Responses at /v1, Anthropic's
// Messages at the root — and turn it away on the other two. OpenCode asks
// a model through the AI SDK package models.dev's per-model provider.npm
// names: @ai-sdk/openai (Responses) for GPT, Grok and Muse Spark,
// @ai-sdk/anthropic (Messages) for Claude, MiniMax and some Qwen, and
// @ai-sdk/openai-compatible (chat) for the rest. A provider at Go added by
// hand or brought from another app with its chat URL alone had nowhere
// but chat to send GPT, Grok, MiniMax or Muse Spark, which Go answers
// with "Model does not support this protocol" (#1215). A URL the user gave
// an API keeps it, and so does a catalog they named.
func (p *Provider) openCodeEndpoints() {
root, catalogID := "", ""
for _, u := range []string{p.Chat, p.Responses, p.Anthropic} {
if root, catalogID = openCodeRoot(u); root != "" {
break
}
}
if root == "" {
return
}
p.Chat = cmp.Or(p.Chat, root+"/v1")
p.Responses = cmp.Or(p.Responses, root+"/v1")
p.Anthropic = cmp.Or(p.Anthropic, root)
p.Catalog = cmp.Or(p.Catalog, catalogID)
}
// openCodeCatalogs are the models.dev providers that say which API a model
// of OpenCode's gateway is served on: the list of the gateway its URL is
// at (Go's for Go, Zen's for Zen) alone, whatever catalog the provider was
// saved with — the two serve one model on different APIs (MiniMax M2.7 on
// Messages at Go, on chat at Zen) — or, at another path, its own.
func (p Provider) openCodeCatalogs() []string {
for _, u := range []string{p.Chat, p.Responses, p.Anthropic} {
if _, id := openCodeRoot(u); id != "" {
return []string{id}
}
}
return p.Catalogs()
}