From 963a8d8f59c87f86457f21afb811ca0920a8371c Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 6 Oct 2026 12:44:13 +0800 Subject: [PATCH] =?UTF-8?q?provider:=20Command=20Code's=20refusal=20of=20a?= =?UTF-8?q?=20Go=20plan's=20key=20says=20where=20the=20key=20works=20(#969?= =?UTF-8?q?,=20pan17)=20=E2=80=94=20a=20Go=20key=20added=20as=20the=20Comm?= =?UTF-8?q?and=20Code=20API-key=20provider=20(the=20preset,=20on=20api.com?= =?UTF-8?q?mandcode.ai/provider)=20can=20never=20be=20served:=20Command=20?= =?UTF-8?q?Code's=20Provider=20API=20answers=20every=20request=20"Your=20G?= =?UTF-8?q?o=20plan=20doesn't=20include=20API=20access.=20Upgrade=20to=20P?= =?UTF-8?q?rovider=20or=20higher=20=E2=80=A6".=20APIError=20now=20adds=20t?= =?UTF-8?q?hat=20a=20Go=20key=20works=20through=20the=20Command=20Code=20p?= =?UTF-8?q?lugin=20(Plugins=20=E2=80=BA=20Command=20Code,=20signed=20in=20?= =?UTF-8?q?with=20the=20key),=20which=20asks=20as=20Command=20Code's=20CLI?= =?UTF-8?q?=20does.=20TestAPIErrorShapes=20and=20the=20new=20TestCommandCo?= =?UTF-8?q?deGoKeySaysWhereItWorks=20(chat,=20messages=20and=20responses?= =?UTF-8?q?=20through=20the=20gateway)=20fail=20without=20it=20and=20pass;?= =?UTF-8?q?=20go=20test=20provider/gateway/plugin/gui,=20vet,=20linux/wind?= =?UTF-8?q?ows=20builds=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/commandcode_go_key_test.go | 41 +++++++++++++++++++++ internal/provider/decide_test.go | 5 +++ internal/provider/test.go | 13 +++++++ 3 files changed, 59 insertions(+) create mode 100644 internal/gateway/commandcode_go_key_test.go diff --git a/internal/gateway/commandcode_go_key_test.go b/internal/gateway/commandcode_go_key_test.go new file mode 100644 index 00000000..712dcce7 --- /dev/null +++ b/internal/gateway/commandcode_go_key_test.go @@ -0,0 +1,41 @@ +package gateway + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/yetone/magpie/internal/provider" +) + +// A Go plan's key added as a Command Code API-key provider is refused by +// the Provider API (#969); the error the agent sees says where the key +// works, on every protocol the preset serves. +func TestCommandCodeGoKeySaysWhereItWorks(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + t.Setenv("XDG_CACHE_HOME", t.TempDir()) + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + io.WriteString(w, `{"error":{"type":"forbidden","message":"Your Go plan doesn't include API access. Upgrade to Provider or higher at https://commandcode.ai/billing to use these endpoints."}}`) + })) + defer up.Close() + if err := provider.Save(provider.Provider{ID: "commandcode", Name: "Command Code", Key: "k", Models: []string{"m1"}, + Chat: up.URL + "/provider/v1", Responses: up.URL + "/provider/v1", Anthropic: up.URL + "/provider"}); err != nil { + t.Fatal(err) + } + for _, q := range []struct{ path, body string }{ + {"/v1/chat/completions", `{"model":"commandcode/m1","messages":[{"role":"user","content":"hi"}]}`}, + {"/v1/messages", `{"model":"commandcode/m1","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}`}, + {"/v1/responses", `{"model":"commandcode/m1","input":"hi"}`}, + } { + forgetRouting() + code, body := post(t, q.path, q.body) + if code < 400 || !strings.Contains(body, "Go plan doesn't include API access") || !strings.Contains(body, "Plugins › Command Code") { + t.Errorf("%s: %d %s", q.path, code, body) + } + } +} diff --git a/internal/provider/decide_test.go b/internal/provider/decide_test.go index 45e00436..d3a22902 100644 --- a/internal/provider/decide_test.go +++ b/internal/provider/decide_test.go @@ -514,6 +514,11 @@ func TestAPIErrorShapes(t *testing.T) { if got := APIError(nil, "400 Bad Request"); got != "400 Bad Request" { t.Fatal(got) } + // Command Code's refusal of a Go plan's key says where it works (#969) + goKey := `{"error":{"type":"forbidden","message":"Your Go plan doesn't include API access. Upgrade to Provider or higher at https://commandcode.ai/billing to use these endpoints."}}` + if got := APIError([]byte(goKey), "403 Forbidden"); got != "Your Go plan doesn't include API access. Upgrade to Provider or higher at https://commandcode.ai/billing to use these endpoints. — "+CommandCodeGoHint { + t.Fatal(got) + } } // A backend that only streams is tested with a streamed request: WorkBuddy diff --git a/internal/provider/test.go b/internal/provider/test.go index c3dcd294..ff9e6b05 100644 --- a/internal/provider/test.go +++ b/internal/provider/test.go @@ -510,9 +510,22 @@ func APIError(b []byte, fallback string) string { if link, ok := Verification(b); ok { return VerifyMessage(apiError(b, fallback), link) } + if cmdGoNoAPI.Match(b) { + return apiError(b, fallback) + " — " + CommandCodeGoHint + } return apiError(b, fallback) } +// cmdGoNoAPI is Command Code's Provider API refusing a Go plan's key: +// "Your Go plan doesn't include API access. Upgrade to Provider or higher +// …" (#969). Go's key is only taken where Command Code's CLI asks, which +// the Command Code plugin does; an API-key provider (the "commandcode" +// preset) never can. +var cmdGoNoAPI = regexp.MustCompile(`(?i)\bGo plan doesn.t include API access`) + +// CommandCodeGoHint says where a Go plan's key works. +const CommandCodeGoHint = "a Go plan's key works in magpie through the Command Code plugin (Plugins › Command Code, then sign in with the key), which asks Command Code as its CLI does; a Command Code API-key provider can't use it" + // verifyWords is a refusal asking for the account to be verified, put in // words rather than as VALIDATION_REQUIRED — or already by VerifyMessage. var verifyWords = regexp.MustCompile(`(?i)verify your account|account verification required|` + verifyAdvice)