mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-19 16:39:15 +00:00
Image models previously could not be scheduled through a priority chain: the chat AUTO chain explicitly skips image-kind slots, and AUTO image requests fell back to unordered registry discovery. - config: add auto_image rules (auto_image yaml / image_rules json); legacy auto rules keep their meaning as the chat chain - core: buildAutoImageChain mirrors buildAutoChain with inverted kind filter (image-only); SaveAutoImageRules + AutoImageRules/AutoImageChain - scheduler: ChainImage walks the chain tier-by-tier with round-robin and preference ordering, skipping cooling slots - gateway: handleImage AUTO now runs down AutoImageChain when one is configured (falls back to legacy discovery otherwise) and records the actual served model; handleAutoAPI GET returns image_rules and PUT accepts image_rules independently of rules - webui: priority page gains a chat/image toggle editing two independent lane sets; add-slot picker filters by active kind; persistAuto writes only the active chain's field
186 lines
5.6 KiB
Go
186 lines
5.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"llmsproxy/internal/config"
|
|
)
|
|
|
|
// handleKeysAPI manages gateway keys: GET /api/keys (admin: all keys),
|
|
// GET /api/keys/me (own key for any role), POST /api/keys (admin: create),
|
|
// PUT /api/keys/{key} (admin: update), DELETE /api/keys/{key} (admin: remove).
|
|
func (g *Gateway) handleKeysAPI(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/keys")
|
|
path = strings.Trim(path, "/")
|
|
role := reqRole(r.Context())
|
|
|
|
if path == "me" {
|
|
g.handleKeyMe(w, r)
|
|
return
|
|
}
|
|
if role != "admin" {
|
|
writeError(w, http.StatusForbidden, "forbidden", "admin role required")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
if path != "" {
|
|
writeError(w, http.StatusNotFound, "not_found", "use GET /api/keys")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"keys": g.core.ListKeys()})
|
|
case http.MethodPost:
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
Models []config.ModelScope `json:"models"`
|
|
Note string `json:"note"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json: "+err.Error())
|
|
return
|
|
}
|
|
if body.Role == "" {
|
|
body.Role = "user"
|
|
}
|
|
if body.Role != "admin" && body.Role != "user" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "role must be admin or user")
|
|
return
|
|
}
|
|
rec, err := g.core.CreateKey(body.Name, body.Role, body.Models, body.Note)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "key_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true, "key": rec})
|
|
case http.MethodPut, http.MethodPatch:
|
|
if path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "key required")
|
|
return
|
|
}
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
Models []config.ModelScope `json:"models"`
|
|
Note string `json:"note"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json: "+err.Error())
|
|
return
|
|
}
|
|
rec, err := g.core.UpdateKey(path, body.Name, body.Role, body.Models, body.Note)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "key_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true, "key": rec})
|
|
case http.MethodDelete:
|
|
if path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "key required")
|
|
return
|
|
}
|
|
if path == reqKey(r.Context()) {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "cannot delete the key you are logged in with")
|
|
return
|
|
}
|
|
ok, err := g.core.DeleteKey(path)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "key_error", err.Error())
|
|
return
|
|
}
|
|
if !ok {
|
|
writeError(w, http.StatusNotFound, "not_found", "key not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "")
|
|
}
|
|
}
|
|
|
|
// handleKeyMe returns the authenticated key's own record (users see only
|
|
// themselves; admins can use this as a convenience too).
|
|
func (g *Gateway) handleKeyMe(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "use GET")
|
|
return
|
|
}
|
|
rec, ok := g.core.FindKey(reqKey(r.Context()))
|
|
if !ok {
|
|
writeError(w, http.StatusUnauthorized, "invalid_api_key", "key not found")
|
|
return
|
|
}
|
|
if !rec.Seed {
|
|
for _, s := range g.core.GatewayKeys() {
|
|
if s == rec.Key {
|
|
rec.Seed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"key": rec})
|
|
}
|
|
|
|
// allowedModels returns the model scope for the request's key; nil means
|
|
// unrestricted (admin keys and user keys without an explicit scope).
|
|
func (g *Gateway) allowedModels(ctx context.Context) []config.ModelScope {
|
|
if reqRole(ctx) == "admin" {
|
|
return nil
|
|
}
|
|
rec, ok := g.core.FindKey(reqKey(ctx))
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return rec.Models
|
|
}
|
|
|
|
// handleAutoAPI manages the AUTO scheduling slots: GET /api/auto returns the
|
|
// current rules; PUT /api/auto replaces them (admin only).
|
|
func (g *Gateway) handleAutoAPI(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"rules": g.core.AutoRules(),
|
|
"image_rules": g.core.AutoImageRules(),
|
|
"states": g.core.AutoSlotStates(),
|
|
})
|
|
return
|
|
}
|
|
if reqRole(r.Context()) != "admin" {
|
|
writeError(w, http.StatusForbidden, "forbidden", "admin role required")
|
|
return
|
|
}
|
|
switch r.Method {
|
|
case http.MethodPut, http.MethodPost:
|
|
var body struct {
|
|
Rules []config.ModelScope `json:"rules"`
|
|
ImageRules []config.ModelScope `json:"image_rules"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json: "+err.Error())
|
|
return
|
|
}
|
|
if body.Rules != nil {
|
|
if err := g.core.SaveAutoRules(body.Rules); err != nil {
|
|
writeError(w, http.StatusBadRequest, "auto_error", err.Error())
|
|
return
|
|
}
|
|
}
|
|
if body.ImageRules != nil {
|
|
if err := g.core.SaveAutoImageRules(body.ImageRules); err != nil {
|
|
writeError(w, http.StatusBadRequest, "auto_error", err.Error())
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"rules": g.core.AutoRules(),
|
|
"image_rules": g.core.AutoImageRules(),
|
|
})
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "")
|
|
}
|
|
} |