mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
启动时那条「gateway_keys is EMPTY — without a key every request is rejected」 读的是 legacy 的 cfg.GatewayKeys 段,而鉴权实际用 cfg.Keys(core.ListKeys)。 seedKeys 首次启动把 gateway_keys 搬进 keys[] 之后,YAML 里那个列表就不再 被鉴权使用。于是在它被清空(例如轮换掉 starter key 之后)而 keys[] 仍有 7 把可用 key(含 admin)时,进程每次启动都谎报「所有请求都会被拒绝」。 实测:生产日志出现该警告,而同一个 key 请求 /v1/models 返回 200。 - main.go 改为检查 c.ListKeys(),文案改成不绑定字段名。 - 顺带删掉 gateway.New 的 gatewayKeys 参数:函数体从未使用它, 只读 ListKeys(),留着会继续诱导人以为鉴权来自那个列表。 判据:e2e/TestStartupWarningReflectsRealKeysNotLegacyList —— 构造 「gateway_keys 空 + keys[] 有 key」的真实形态,先断言该 key 确实能鉴权, 再断言日志里不再出现那句谎报。变异验证:回退成 GatewayKeys() 即变红。
130 lines
4.7 KiB
Go
130 lines
4.7 KiB
Go
// Command llmsproxy is a standalone gateway that exposes multiple upstream LLM
|
|
// sources behind a unified OpenAI-compatible HTTP API. Protocol differences are
|
|
// handled by Lua adapters (per source), optionally signing outgoing requests.
|
|
// It supports explicit model selection or AUTO routing by priority, image
|
|
// generation, multimodal payloads, a web UI for adapter/source management, and
|
|
// concurrent/queued scheduling with backoff and failover.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"llmsproxy/internal/config"
|
|
"llmsproxy/internal/core"
|
|
"llmsproxy/internal/gateway"
|
|
)
|
|
|
|
func main() {
|
|
cfgPath := flag.String("config", "config.yaml", "path to gateway config file")
|
|
checkOnly := flag.Bool("check", false, "validate the config file and exit (0 = valid, 1 = invalid); nothing is started and no file is written")
|
|
showSecrets := flag.Bool("show-secrets", false, "print the config's credentials in the clear and exit; nothing is started and no file is written")
|
|
flag.Parse()
|
|
|
|
// -check is the deploy-time preflight: parse and validate the config
|
|
// without starting the Lua VM, touching runtime.json, or binding a port.
|
|
// It deliberately does NOT call EnsureDefault, so a missing file is an
|
|
// error here instead of being silently created.
|
|
if *checkOnly {
|
|
if _, err := os.Stat(*cfgPath); err != nil {
|
|
log.Fatalf("[llmsproxy] check: %v", err)
|
|
}
|
|
if _, err := config.Load(*cfgPath); err != nil {
|
|
log.Fatalf("[llmsproxy] check: %v", err)
|
|
}
|
|
log.Printf("[llmsproxy] check: %s is valid", *cfgPath)
|
|
return
|
|
}
|
|
|
|
// -show-secrets is the operator escape hatch for a config whose
|
|
// credentials are sealed at rest: it prints them to stdout and exits
|
|
// without starting anything or writing anything. Nothing else in the
|
|
// program prints a credential, and this path never logs to a file.
|
|
if *showSecrets {
|
|
if _, err := os.Stat(*cfgPath); err != nil {
|
|
log.Fatalf("[llmsproxy] show-secrets: %v", err)
|
|
}
|
|
if err := config.PrintSecrets(*cfgPath); err != nil {
|
|
log.Fatalf("[llmsproxy] show-secrets: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
created, err := config.EnsureDefault(*cfgPath)
|
|
if err != nil {
|
|
log.Fatalf("[llmsproxy] config: %v", err)
|
|
}
|
|
|
|
c, err := core.New(*cfgPath)
|
|
if err != nil {
|
|
log.Fatalf("[llmsproxy] core: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
if created {
|
|
for _, k := range c.GatewayKeys() {
|
|
log.Printf("[llmsproxy] generated default config at %s — admin key: %s (rotate it in the WebUI after first login)", *cfgPath, k)
|
|
}
|
|
}
|
|
|
|
gw, err := gateway.New(c)
|
|
if err != nil {
|
|
log.Fatalf("[llmsproxy] gateway: %v", err)
|
|
}
|
|
|
|
// Ops hygiene: surface the two most common footguns instead of silently
|
|
// running with them.
|
|
//
|
|
// Read the AUTHORITATIVE source. Auth uses core.ListKeys() (cfg.Keys), not
|
|
// the legacy cfg.GatewayKeys list: seedKeys copies gateway_keys into keys[]
|
|
// on first start and the YAML list is ignored for auth afterwards. Checking
|
|
// GatewayKeys() here made the warning lie — once the legacy list was empty
|
|
// (e.g. after rotating the starter key away) it reported "every request is
|
|
// rejected" while seven working keys, one of them admin, were in service.
|
|
if keys := c.ListKeys(); len(keys) == 0 {
|
|
log.Printf("[llmsproxy] WARNING: no gateway key configured — every request will be rejected. Add one in the WebUI or set gateway_keys")
|
|
} else {
|
|
for _, k := range keys {
|
|
if k.Key == "sk-gw-local-0001" || k.Key == "sk-local-0001" {
|
|
log.Printf("[llmsproxy] WARNING: gateway key %q looks like the starter/example key — rotate it before exposing the gateway", k.Key)
|
|
}
|
|
}
|
|
}
|
|
if l := c.Listen(); strings.HasPrefix(l, "0.0.0.0:") || strings.HasPrefix(l, "::") {
|
|
log.Printf("[llmsproxy] WARNING: listen=%s binds ALL interfaces — bind an internal address in production", l)
|
|
}
|
|
|
|
srv := &http.Server{
|
|
Addr: c.Listen(),
|
|
Handler: gw.Handler(),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
go func() {
|
|
cert, key := c.TLS()
|
|
if cert != "" && key != "" {
|
|
log.Printf("[llmsproxy] listening HTTPS on %s (cert=%q) (default_model=%s, models=%v, adapters=%d)",
|
|
c.Listen(), cert, c.DefaultModel(), c.Registry().ModelList(), len(c.ListAdapters()))
|
|
if err := srv.ListenAndServeTLS(cert, key); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("[llmsproxy] server: %v", err)
|
|
}
|
|
return
|
|
}
|
|
log.Printf("[llmsproxy] listening HTTP on %s (default_model=%s, models=%v, adapters=%d)",
|
|
c.Listen(), c.DefaultModel(), c.Registry().ModelList(), len(c.ListAdapters()))
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("[llmsproxy] server: %v", err)
|
|
}
|
|
}()
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
|
<-stop
|
|
log.Printf("[llmsproxy] shutting down")
|
|
}
|