Files
ModelRouter/cmd/modelrouter/main.go
JianFeeeee 688dce88d1 refactor(rename)!: llmsproxy → modelrouter(module / 二进制 / 路径 / env)
产品名早已是 ModelRouter,但仓库标识符仍沿用旧名。这次统一,读写两侧都做兼容,
已部署的机器不会因为顺序问题立刻失效。

- go module `llmsproxy` → `modelrouter`;全仓 import 与 `cmd/modelrouter` 同步。
- `Makefile BINARY=modelrouter`;发行包名、systemd unit、/usr/share 资源、
  `/usr/local/bin` 目标全部改为 modelrouter。
- 默认配置目录 `/etc/llmsproxy` → `/etc/modelrouter`。**读取侧回退**:插件与
  `storeDir` 仍会检查旧路径,所以"先换二进制、后改 unit"不会立即失效。
- 环境变量 `LLMSPROXY_*`(PLUGIN_DIR / XDISPLAY_BASE / ARCH / LIVE_ZEN /
  BUILD_DIR)改为 `MODELROUTER_*`;`LLMS_PROXY_MASTER_KEY` →
  `MODELROUTER_MASTER_KEY`。旧名在读取侧继续接受。
- 桌面端内嵌核心 `bin/llmsproxy` → `bin/modelrouter`,并保留旧文件名回退,
  避免升级到一半的 profile 被判成"core binary missing"。
- 新增 `packaging/migrate-to-modelrouter.sh`:**默认 dry-run**,复制而非移动、
  旧目录永不删除、支持 `--rollback`;随 release tar 分发并由 core-dist 校验。
- `deploy.sh` 增加改名保护:发现旧 `/etc/llmsproxy` 而新配置不存在时**中止**,
  否则会部署出一个"能启动但没有 key、没有源"的空实例。
- 临时目录前缀 `/tmp/llmsproxy-*` → `/tmp/modelrouter-*`;清理逻辑同时回收两代。

兼容别名(`__llmsproxy_*` 全局、`llmsproxy` SDK 别名、旧路径/旧 env)刻意保留,
它们是升级路径而非遗留;docs/plugins.md 已标注为废弃。

本提交未包含:`adapters/opencodezen.lua`、`adapter_opencodezen_test.go`、
`adapter_opencodezen_live_test.go`(OpenCode UA/免费池的既有改动,另批提交)。
2026-10-26 22:28:08 +08:00

130 lines
4.8 KiB
Go

// Command modelrouter 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"
"modelrouter/internal/config"
"modelrouter/internal/core"
"modelrouter/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("[modelrouter] check: %v", err)
}
if _, err := config.Load(*cfgPath); err != nil {
log.Fatalf("[modelrouter] check: %v", err)
}
log.Printf("[modelrouter] 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("[modelrouter] show-secrets: %v", err)
}
if err := config.PrintSecrets(*cfgPath); err != nil {
log.Fatalf("[modelrouter] show-secrets: %v", err)
}
return
}
created, err := config.EnsureDefault(*cfgPath)
if err != nil {
log.Fatalf("[modelrouter] config: %v", err)
}
c, err := core.New(*cfgPath)
if err != nil {
log.Fatalf("[modelrouter] core: %v", err)
}
defer c.Close()
if created {
for _, k := range c.GatewayKeys() {
log.Printf("[modelrouter] 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("[modelrouter] 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("[modelrouter] 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("[modelrouter] 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("[modelrouter] 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("[modelrouter] 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("[modelrouter] server: %v", err)
}
return
}
log.Printf("[modelrouter] 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("[modelrouter] server: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Printf("[modelrouter] shutting down")
}