From b1fa0c5dfac4ca96f2f8529578564a7a4ecee653 Mon Sep 17 00:00:00 2001 From: yetone Date: Mon, 28 Sep 2026 10:24:56 +0800 Subject: [PATCH] =?UTF-8?q?gateway:=20to=20the=20key=20magpie-claude-deskt?= =?UTF-8?q?op,=20/v1/models=20lists=20every=20model=20by=20an=20id=20Claud?= =?UTF-8?q?e=20Desktop=20takes=20=E2=80=94=20anthropic/=20for=20one=20?= =?UTF-8?q?that=20doesn't=20say=20claude,=20sonnet,=20opus,=20haiku=20or?= =?UTF-8?q?=20anthropic=20=E2=80=94=20so=20its=20Test=20connection=20passe?= =?UTF-8?q?s=20and=20its=20models=20can=20be=20picked=20from=20the=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/desktop_test.go | 10 ++++++++++ internal/gateway/gateway.go | 24 ++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/gateway/desktop_test.go b/internal/gateway/desktop_test.go index b97a0e09..cd81a19c 100644 --- a/internal/gateway/desktop_test.go +++ b/internal/gateway/desktop_test.go @@ -38,6 +38,16 @@ func TestAnthropicPrefixedModel(t *testing.T) { if rec.Code != 200 { t.Fatalf("HEAD /api/hello: %d", rec.Code) } + req := httptest.NewRequest("GET", "/v1/models", nil) + req.Header.Set("x-api-key", Token+"-claude-desktop") + rec = httptest.NewRecorder() + New().Handler().ServeHTTP(rec, req) + if !strings.Contains(rec.Body.String(), `"id":"anthropic/fake/m1"`) { + t.Fatalf("Claude Desktop's list: %s", rec.Body) + } + if claudeLooking("devin/claude-opus-5-5") != "devin/claude-opus-5-5" { + t.Fatal("a Claude id was prefixed") + } if got := unprefixed("claude-sonnet-4-5"); got != "claude-sonnet-4-5" { t.Fatalf("unprefixed changed %q", got) } diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 32b7c49e..3e5ec1e6 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -302,8 +302,13 @@ func catalogFor(r *http.Request) []provider.Entry { func (s *Server) models(w http.ResponseWriter, r *http.Request) { data := []map[string]any{} + desktop := agentOf(r) == "claude-desktop" for _, e := range catalogFor(r) { - data = append(data, modelObject(e)) + m := modelObject(e) + if desktop { + m["id"] = claudeLooking(e.ID) + } + data = append(data, m) } out := map[string]any{"object": "list", "data": data, "has_more": false} if len(data) > 0 { @@ -327,7 +332,8 @@ func (s *Server) model(w http.ResponseWriter, r *http.Request) { // front of it. Claude Desktop, pointed at a third-party gateway, takes only // models whose id says claude, sonnet, opus, haiku or anthropic, so any of // magpie's is asked for there as anthropic/. An id that is magpie's -// as it stands (a provider named anthropic) is left alone. +// as it stands (a provider named anthropic) is left alone. With the key +// magpie-claude-desktop, /v1/models lists them so. func unprefixed(id string) string { rest, ok := strings.CutPrefix(id, "anthropic/") if !ok || rest == "" { @@ -342,6 +348,20 @@ func unprefixed(id string) string { return rest } +// claudeLooking is a model's id as Claude Desktop is shown it, to the key +// magpie-claude-desktop: it takes only an id that says claude, sonnet, opus, +// haiku or anthropic, so any other is put after anthropic/ (unprefixed +// serves it again). +func claudeLooking(id string) string { + l := strings.ToLower(id) + for _, w := range []string{"claude", "sonnet", "opus", "haiku", "anthropic"} { + if strings.Contains(l, w) { + return id + } + } + return "anthropic/" + id +} + // countTokens answers Anthropic's count_tokens: through the provider when // it implements counting, else a rough estimate. A failed connection or // limited key yields to the next key; other failures reach the client.