mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
feat(keys): role-based gateway keys with admin management UI and per-user model scope
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"llmsproxy/internal/config"
|
||||
"llmsproxy/internal/core"
|
||||
)
|
||||
|
||||
@ -23,28 +24,20 @@ var uiFS embed.FS
|
||||
|
||||
// Gateway is the HTTP handler for the OpenAI-compatible endpoint + web UI.
|
||||
type Gateway struct {
|
||||
core *core.Core
|
||||
apiKeys map[string]bool
|
||||
ui http.Handler
|
||||
stats *Stats
|
||||
core *core.Core
|
||||
ui http.Handler
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
func New(c *core.Core, gatewayKeys []string) (*Gateway, error) {
|
||||
keys := map[string]bool{}
|
||||
for _, k := range gatewayKeys {
|
||||
if k != "" {
|
||||
keys[k] = true
|
||||
}
|
||||
}
|
||||
sub, err := fs.Sub(uiFS, "ui")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Gateway{
|
||||
core: c,
|
||||
apiKeys: keys,
|
||||
ui: http.FileServer(http.FS(sub)),
|
||||
stats: NewStats(3000),
|
||||
core: c,
|
||||
ui: http.FileServer(http.FS(sub)),
|
||||
stats: NewStats(3000),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -77,6 +70,8 @@ func (g *Gateway) routes(w http.ResponseWriter, r *http.Request) {
|
||||
g.handleStatusAPI(w, r)
|
||||
case r.URL.Path == "/api/stats" || strings.HasPrefix(r.URL.Path, "/api/stats/"):
|
||||
g.handleStatsAPI(w, r)
|
||||
case r.URL.Path == "/api/keys" || strings.HasPrefix(r.URL.Path, "/api/keys/"):
|
||||
g.handleKeysAPI(w, r)
|
||||
case r.URL.Path == "/login":
|
||||
g.handleLogin(w, r)
|
||||
case r.URL.Path == "/api/login":
|
||||
@ -105,10 +100,6 @@ func (g *Gateway) serveUI(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (g *Gateway) auth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(g.apiKeys) == 0 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
key := ""
|
||||
if h := r.Header.Get("Authorization"); h != "" {
|
||||
parts := strings.SplitN(h, " ", 2)
|
||||
@ -124,7 +115,8 @@ func (g *Gateway) auth(next http.Handler) http.Handler {
|
||||
key = c.Value
|
||||
}
|
||||
}
|
||||
if !g.apiKeys[key] {
|
||||
rec, ok := g.core.FindKey(key)
|
||||
if !ok {
|
||||
if isAPIPath(r.URL.Path) {
|
||||
writeError(w, http.StatusUnauthorized, "invalid_api_key", "invalid gateway api key")
|
||||
return
|
||||
@ -133,21 +125,36 @@ func (g *Gateway) auth(next http.Handler) http.Handler {
|
||||
http.Redirect(w, r, "/login?continue="+url.QueryEscape(r.URL.Path), http.StatusFound)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(withKey(r.Context(), key)))
|
||||
next.ServeHTTP(w, r.WithContext(withAuth(r.Context(), key, rec.Role)))
|
||||
})
|
||||
}
|
||||
|
||||
// keyCtxKey is the context key carrying the authenticated gateway key.
|
||||
type keyCtxKey struct{}
|
||||
// authCtx carries the authenticated gateway key and its role.
|
||||
type authCtx struct {
|
||||
key string
|
||||
role string
|
||||
}
|
||||
|
||||
func withKey(ctx context.Context, key string) context.Context {
|
||||
return context.WithValue(ctx, keyCtxKey{}, key)
|
||||
type authCtxKeyT struct{}
|
||||
|
||||
func withAuth(ctx context.Context, key, role string) context.Context {
|
||||
return context.WithValue(ctx, authCtxKeyT{}, authCtx{key: key, role: role})
|
||||
}
|
||||
|
||||
// reqKey returns the authenticated gateway key id (masked suffix for display).
|
||||
func reqKey(ctx context.Context) string {
|
||||
k, _ := ctx.Value(keyCtxKey{}).(string)
|
||||
return k
|
||||
if a, ok := ctx.Value(authCtxKeyT{}).(authCtx); ok {
|
||||
return a.key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// reqRole returns the authenticated key role ("admin" or "user").
|
||||
func reqRole(ctx context.Context) string {
|
||||
if a, ok := ctx.Value(authCtxKeyT{}).(authCtx); ok {
|
||||
return a.role
|
||||
}
|
||||
return "user"
|
||||
}
|
||||
|
||||
// keyID returns a short stable id for a gateway key (last 6 chars).
|
||||
@ -232,9 +239,9 @@ var LANG='%[3]s';
|
||||
var CONT='%[2]s';
|
||||
var I={zh:{title:'ModelRouter',sub:'统一 LLM 网关 · 登录',ph:'输入网关 API Key',btn:'登录',lang:'English'},
|
||||
en:{title:'ModelRouter',sub:'Unified LLM Gateway · Sign in',ph:'Enter gateway API key',btn:'Sign in',lang:'中文'}};
|
||||
function apply(){var t=I[LANG]||I.zh;document.querySelector('[data-i=title]').textContent=t.title;
|
||||
function apply(){var t=I[LANG]||I.zh;document.querySelector('[data-i=title]').textContent=t.title;
|
||||
document.querySelector('[data-i=sub]').textContent=t.sub;
|
||||
document.querySelector('[data-i=ph]').placeholder=t.ph;
|
||||
var ph=document.querySelector('#key');if(ph)ph.placeholder=t.ph;
|
||||
document.querySelector('[data-i=btn]').textContent=t.btn;
|
||||
document.querySelector('[data-i=lang]').textContent=t.lang;
|
||||
document.documentElement.lang=LANG==='zh'?'zh':'en';}
|
||||
@ -265,7 +272,7 @@ func (g *Gateway) handleLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json")
|
||||
return
|
||||
}
|
||||
if !g.apiKeys[body.Key] {
|
||||
if _, ok := g.core.FindKey(body.Key); !ok {
|
||||
writeError(w, http.StatusUnauthorized, "invalid_api_key", "invalid gateway api key")
|
||||
return
|
||||
}
|
||||
@ -286,6 +293,9 @@ func (g *Gateway) handleModels(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
models := g.core.Registry().ModelList()
|
||||
if allow := g.allowedModels(r.Context()); allow != nil {
|
||||
models = intersectModels(models, allow)
|
||||
}
|
||||
type modelObj struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
@ -308,9 +318,13 @@ func (g *Gateway) handleStatusAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(host, ":") {
|
||||
host = "127.0.0.1" + host
|
||||
}
|
||||
keys := make([]string, 0, len(g.apiKeys))
|
||||
for k := range g.apiKeys {
|
||||
keys = append(keys, k)
|
||||
ks := make([]config.GWKey, 0, len(g.core.ListKeys()))
|
||||
for _, k := range g.core.ListKeys() {
|
||||
ks = append(ks, config.GWKey{
|
||||
Key: k.Key,
|
||||
Role: k.Role,
|
||||
Name: k.Name,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"default_model": g.core.DefaultModel(),
|
||||
@ -318,7 +332,7 @@ func (g *Gateway) handleStatusAPI(w http.ResponseWriter, r *http.Request) {
|
||||
"sources": g.core.Registry().Status(),
|
||||
"adapters": g.core.ListAdapters(),
|
||||
"base_url": "http://" + host + "/v1",
|
||||
"gateway_keys": keys,
|
||||
"gateway_keys": ks,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user