Files
yetone 46f03154ff gateway: a gateway key can be held to some models (#882, LCorleone) — a key could reach every model magpie serves. A key now keeps an optional list of models (caller-keys.json "models", none = every model, so older files read as before): <provider>/<model> or <provider>/*, matched against the provider that serves the call, so a bare model name is resolved first and OpenRouter's "anthropic/x" is not Anthropic's. Such a key sees only its models in /v1/models, /v1/models/{id} and the Anthropic (Claude Desktop) and Gemini lists, a routing group only when every member is its; asking for another, by id, bare name or through a group with a member it may not use, is refused before any provider is asked with a 403 in the API's own error shape (permission_error) that names the key and its models, logged as a rejected call like an over-limit key's 429; a fallback it may not use is skipped; embeddings, images and videos it names are held too. magpie's own calls for it (a web search, a picture described, Codex titles with the model Settings name) are marked in their context, not by a User-Agent a client could send, and go on. Set it with the new badge on the key's row in the Gateway page (the app's menu: All models, every model of a provider, each model; sent when the menu closes) or magpie gateway-key models <id> openai/gpt-5 anthropic/* (all takes it off); gateway-key list has a MODELS column. The app's menu, fitting neither below nor above its button, now goes on the roomier side and scrolls instead of running off the window. en/zh/ja/de. TestGatewayKeyModelsHoldAKey (lists, 403s by id, bare name and group, Anthropic shape, fallback skipped, magpie's own call, a free key) fails without the change; TestModelPatterns, TestKeyModelsKept, TestGatewayKeyModelsCLI, TestCallerKeyModelsRoutes pass; gateway-key-models.test.cjs (Chromium and WebKit, en and zh: no native select, menu inside a 420px window, picks sent on close, All models, no scroll, no left border, 560px fits) passes with gateway-connect-keys, gateway-key-limit, gateway-caller-keys and click-scroll. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 16:52:23 +08:00

90 lines
2.9 KiB
Go

package access
import (
"os"
"slices"
"strings"
"testing"
)
func TestModelPatterns(t *testing.T) {
ps := []string{"openai/gpt-5", "anthropic/*", "relay/*-mini"}
for id, want := range map[string]bool{
"openai/gpt-5": true,
"OpenAI/GPT-5": true,
"openai/gpt-5-mini": false,
"anthropic/claude-x": true,
"anthropic/vendor/x": true,
"anthropics/x": false,
"relay/m-mini": true,
"relay/m": false,
"openrouter/anthropic/x": false,
} {
if got := ModelAllowed(ps, id); got != want {
t.Errorf("%s: %v", id, got)
}
}
if !ModelAllowed(nil, "any/thing") {
t.Fatal("no patterns hold a model")
}
who := Identity{Models: ps}
if !who.Restricted() || who.Allows("relay/m", "") || !who.Allows("relay/m", "openai/gpt-5") || (Identity{}).Restricted() || !(Identity{}).Allows("x/y") {
t.Fatal("Identity.Allows")
}
got, err := CleanModels([]string{" openai/gpt-5 ", "", "OPENAI/GPT-5", "anthropic/*"})
if err != nil || !slices.Equal(got, []string{"openai/gpt-5", "anthropic/*"}) {
t.Fatal(got, err)
}
for _, bad := range []string{"gpt-5", "/gpt-5", "openai/", strings.Repeat("a", 300) + "/b"} {
if _, err := CleanModels([]string{bad}); err == nil {
t.Error("accepted", bad)
}
}
}
// A key's models are kept in caller-keys.json, stay through a rotation and
// a restart, reach the gateway with the key, and an older file without
// them reads as every model (#882).
func TestKeyModelsKept(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
secret, err := Update("add-key", Change{Name: "Phone"})
if err != nil {
t.Fatal(err)
}
keys, _ := List()
id := keys[0].ID
if who, ok := Authenticate(secret); !ok || who.Restricted() {
t.Fatal("a new key is held", who)
}
if _, err := Update("models-key", Change{Key: id, Models: []string{"gpt-5"}}); err == nil {
t.Fatal("accepted a model without its provider")
}
if _, err := Update("models-key", Change{Key: "missing", Models: []string{"a/b"}}); err == nil {
t.Fatal("held a missing key")
}
if _, err := Update("models-key", Change{Key: id, Models: []string{"openai/gpt-5", "anthropic/*"}}); err != nil {
t.Fatal(err)
}
b, _ := os.ReadFile(Path())
if !strings.Contains(string(b), `"models": [`) {
t.Fatal("caller-keys.json", string(b))
}
secret, err = Update("rotate-key", Change{Key: id})
if err != nil {
t.Fatal(err)
}
who, ok := Authenticate(secret)
if !ok || !slices.Equal(who.Models, []string{"openai/gpt-5", "anthropic/*"}) || !who.Allows("anthropic/claude") || who.Allows("openai/gpt-4o") {
t.Fatal("authenticated as", who, ok)
}
if _, err := Update("models-key", Change{Key: id}); err != nil {
t.Fatal(err)
}
if who, _ := Authenticate(secret); who.Restricted() {
t.Fatal("every model kept a list", who)
}
if b, _ := os.ReadFile(Path()); strings.Contains(string(b), `"models"`) {
t.Fatal("an empty list written", string(b))
}
}