Files
july_lu 47c2382467 gateway: a gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
2026-10-06 10:51:50 +08:00

78 lines
2.2 KiB
Go

package access
import (
"errors"
"regexp"
"slices"
"strings"
)
// CleanModels is a key's model list as kept (#882): each pattern trimmed,
// empty and repeated ones dropped. A pattern names a provider and a model,
// "<provider>/<model>", every model of one, "<provider>/*", or a routing
// group, "group/<name>".
func CleanModels(in []string) ([]string, error) {
var out []string
for _, m := range in {
m = strings.TrimSpace(m)
if m == "" || slices.ContainsFunc(out, func(o string) bool { return strings.EqualFold(o, m) }) {
continue
}
if !strings.Contains(m, "/") || strings.HasPrefix(m, "/") || strings.HasSuffix(m, "/") {
return nil, errors.New("A key's model is <provider>/<model> or <provider>/*, like openai/gpt-5 or anthropic/*: " + m)
}
if len(m) > 300 {
return nil, errors.New("A key's model is at most 300 characters")
}
out = append(out, m)
}
if len(out) > 500 {
return nil, errors.New("A key can list at most 500 models")
}
return out, nil
}
// Restricted says the key may use only the models or accounts it lists.
func (who Identity) Restricted() bool { return len(who.Models) > 0 || len(who.Accounts) > 0 }
// Allows says one of ids is among the key's models; any, for a key that
// lists none. The accounts a key may be held to say nothing of its
// models: they hold the candidates of a plan, not its models.
func (who Identity) Allows(ids ...string) bool {
if len(who.Models) == 0 {
return true
}
for _, id := range ids {
if id != "" && ModelAllowed(who.Models, id) {
return true
}
}
return false
}
// ModelAllowed says id ("<provider>/<model>") matches one of patterns,
// letter case aside; a "*" in a pattern stands for any run of characters,
// slashes too. No patterns allow every model.
func ModelAllowed(patterns []string, id string) bool {
if len(patterns) == 0 {
return true
}
for _, p := range patterns {
if strings.EqualFold(p, id) {
return true
}
if strings.Contains(p, "*") && globRe(p).MatchString(id) {
return true
}
}
return false
}
func globRe(p string) *regexp.Regexp {
parts := strings.Split(p, "*")
for i, s := range parts {
parts[i] = regexp.QuoteMeta(s)
}
return regexp.MustCompile("(?is)^" + strings.Join(parts, ".*") + "$")
}