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.