mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
179 lines
5.6 KiB
Go
179 lines
5.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"llmsproxy/internal/config"
|
|
)
|
|
|
|
type adapterPayload struct {
|
|
Name string `json:"name"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
func (g *Gateway) handleAdaptersAPI(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/adapters")
|
|
path = strings.Trim(path, "/")
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
if path == "" {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"adapters": g.core.ListAdapters()})
|
|
return
|
|
}
|
|
writeError(w, http.StatusNotFound, "not_found", "adapter code not exposed; edit in UI")
|
|
case http.MethodPost:
|
|
var p adapterPayload
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json: "+err.Error())
|
|
return
|
|
}
|
|
if err := g.core.UploadAdapter(p.Name, p.Code); err != nil {
|
|
writeError(w, http.StatusBadRequest, "adapter_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true, "name": p.Name})
|
|
case http.MethodDelete:
|
|
if path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "adapter name required")
|
|
return
|
|
}
|
|
if err := g.core.RemoveAdapter(path); err != nil {
|
|
writeError(w, http.StatusBadRequest, "adapter_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "")
|
|
}
|
|
}
|
|
|
|
// sourcePayload mirrors config.Source for JSON web UI editing.
|
|
type sourcePayload struct {
|
|
Name string `json:"name"`
|
|
BaseURL string `json:"base_url"`
|
|
APIKey string `json:"api_key"`
|
|
Adapter string `json:"adapter"`
|
|
Endpoint string `json:"endpoint"`
|
|
ImageEndpoint string `json:"image_endpoint"`
|
|
Models []config.Model `json:"models"`
|
|
Headers map[string]string `json:"headers"`
|
|
Meta map[string]interface{} `json:"meta"`
|
|
Temperature float64 `json:"temperature"`
|
|
MaxTokens int `json:"max_tokens"`
|
|
MaxConcurrent int `json:"max_concurrent"`
|
|
}
|
|
|
|
func (g *Gateway) handleSourcesAPI(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/sources")
|
|
path = strings.Trim(path, "/")
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"sources": g.core.Sources()})
|
|
case http.MethodPost:
|
|
var p sourcePayload
|
|
body, _ := io.ReadAll(r.Body)
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "invalid json: "+err.Error())
|
|
return
|
|
}
|
|
src := config.Source{
|
|
Name: p.Name,
|
|
BaseURL: p.BaseURL,
|
|
APIKey: p.APIKey,
|
|
Adapter: p.Adapter,
|
|
Endpoint: p.Endpoint,
|
|
ImageEndpoint: p.ImageEndpoint,
|
|
Models: p.Models,
|
|
Headers: p.Headers,
|
|
Meta: p.Meta,
|
|
Temperature: p.Temperature,
|
|
MaxTokens: p.MaxTokens,
|
|
MaxConcurrent: p.MaxConcurrent,
|
|
}
|
|
if err := g.core.AddSource(src); err != nil {
|
|
writeError(w, http.StatusBadRequest, "source_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
|
case http.MethodDelete:
|
|
if path == "" {
|
|
writeError(w, http.StatusBadRequest, "invalid_request", "source name required")
|
|
return
|
|
}
|
|
if err := g.core.RemoveSource(path); err != nil {
|
|
writeError(w, http.StatusBadRequest, "source_error", err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"ok": true})
|
|
default:
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "")
|
|
}
|
|
}
|
|
// handleStatsAPI returns per-key / per-model / per-source usage aggregates and
|
|
// the recent request audit trail.
|
|
func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "use GET")
|
|
return
|
|
}
|
|
limit := 500
|
|
if v := r.URL.Query().Get("limit"); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 5000 {
|
|
limit = n
|
|
}
|
|
}
|
|
key := r.URL.Query().Get("key")
|
|
if reqRole(r.Context()) != "admin" {
|
|
// user keys may only see their own usage
|
|
key = keyID(reqKey(r.Context()))
|
|
}
|
|
if r.URL.Query().Get("export") == "csv" {
|
|
from, _ := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64)
|
|
to, _ := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64)
|
|
if to == 0 {
|
|
to = time.Now().UnixMilli()
|
|
}
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=llmsproxy-requests.csv")
|
|
cw := csv.NewWriter(w)
|
|
names := map[string]string{}
|
|
for _, k := range g.core.ListKeys() {
|
|
names[keyID(k.Key)] = k.Name
|
|
}
|
|
_ = cw.Write([]string{"time", "key", "key_name", "type", "model", "source", "status", "ok", "prompt_tokens", "completion_tokens", "latency_ms", "error"})
|
|
for _, rec := range g.stats.Records(from, to, key) {
|
|
_ = cw.Write([]string{
|
|
time.UnixMilli(rec.Time).Format(time.RFC3339),
|
|
rec.Key,
|
|
names[rec.Key],
|
|
rec.Type,
|
|
rec.Model,
|
|
rec.Source,
|
|
strconv.Itoa(rec.Status),
|
|
strconv.FormatBool(rec.OK),
|
|
strconv.FormatInt(rec.Prompt, 10),
|
|
strconv.FormatInt(rec.Compl, 10),
|
|
strconv.FormatInt(rec.LatMs, 10),
|
|
rec.Err,
|
|
})
|
|
}
|
|
cw.Flush()
|
|
return
|
|
}
|
|
snap := g.stats.Snapshot(limit, key)
|
|
keyNames := map[string]string{}
|
|
for _, k := range g.core.ListKeys() {
|
|
keyNames[keyID(k.Key)] = k.Name
|
|
}
|
|
snap["key_names"] = keyNames
|
|
writeJSON(w, http.StatusOK, snap)
|
|
}
|