From 688dce88d190c81e8fa05800d718df26c8892591 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 26 Oct 2026 22:28:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor(rename)!:=20llmsproxy=20=E2=86=92=20mo?= =?UTF-8?q?delrouter=EF=BC=88module=20/=20=E4=BA=8C=E8=BF=9B=E5=88=B6=20/?= =?UTF-8?q?=20=E8=B7=AF=E5=BE=84=20/=20env=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 产品名早已是 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/免费池的既有改动,另批提交)。 --- Makefile | 4 +- cmd/gui/main.js | 40 ++-- cmd/gui/package.json | 8 +- cmd/gui/scripts/prepare-core.js | 14 +- cmd/{llmsproxy => modelrouter}/main.go | 42 ++-- deploy.sh | 73 ++++--- docs/channels-parity.md | 6 +- e2e/e2e_test.go | 8 +- go.mod | 2 +- internal/config/config.go | 2 +- internal/config/secret.go | 19 +- internal/core/channels_authorize_test.go | 2 +- .../core/channels_callback_loopback_test.go | 2 +- .../channels_clear_credentialless_test.go | 2 +- .../core/channels_codearts_catalog_test.go | 2 +- .../core/channels_codearts_oauth_flow_test.go | 2 +- internal/core/channels_codearts_port_test.go | 2 +- internal/core/channels_lobster_claim_test.go | 2 +- .../core/channels_lobster_exchange_test.go | 2 +- .../core/channels_lobster_loopback_test.go | 2 +- internal/core/channels_loomy_test.go | 2 +- internal/core/channels_models_remote_test.go | 2 +- internal/core/channels_more_test.go | 2 +- .../core/channels_orphan_callback_test.go | 6 +- internal/core/channels_plugin_test.go | 2 +- internal/core/channels_pool_reason_test.go | 2 +- internal/core/channels_pool_test.go | 2 +- internal/core/channels_qoder_adapter_test.go | 2 +- internal/core/channels_qoder_claim_test.go | 2 +- internal/core/channels_qoder_test.go | 2 +- internal/core/channels_raccoon_test.go | 2 +- .../core/channels_remove_source_perm_test.go | 12 +- internal/core/channels_reorder_effect_test.go | 2 +- internal/core/channels_store_atomic_test.go | 4 +- internal/core/channels_store_test.go | 10 +- internal/core/channels_stress_test.go | 4 +- internal/core/channels_trae_catalog_test.go | 2 +- .../core/channels_trae_chat_headers_test.go | 2 +- internal/core/channels_trae_checkin_test.go | 2 +- internal/core/channels_trae_derive_test.go | 2 +- .../core/channels_trae_exchange_shape_test.go | 2 +- internal/core/channels_trae_pkce_test.go | 2 +- internal/core/channels_trae_test.go | 2 +- internal/core/channels_vnc_test.go | 2 +- internal/core/channels_xdisplay_test.go | 4 +- internal/core/channels_zcode_identity_test.go | 4 +- internal/core/channels_zcode_test.go | 2 +- internal/core/core_test.go | 2 +- internal/core/device_harness_test.go | 2 +- internal/core/kernel_actions.go | 4 +- internal/core/kernel_actions_test.go | 16 +- internal/core/kernel_no_deadlock_test.go | 2 +- internal/core/key_auto_concurrency_test.go | 2 +- internal/core/key_auto_test.go | 4 +- internal/core/source_hosts_test.go | 2 +- internal/core/startup_secrets_test.go | 2 +- internal/gateway/api.go | 6 +- internal/gateway/apiv1.go | 4 +- internal/gateway/apiv1_test.go | 2 +- internal/gateway/chat.go | 10 +- internal/gateway/chat_cache_test.go | 2 +- internal/gateway/chat_reasoning_test.go | 2 +- internal/gateway/deploy_script_test.go | 8 +- internal/gateway/discover.go | 2 +- internal/gateway/discover_api.go | 2 +- internal/gateway/discover_api_test.go | 2 +- internal/gateway/gateway_test.go | 4 +- internal/gateway/key_auto_api_test.go | 4 +- internal/gateway/key_quota_api_test.go | 4 +- internal/gateway/key_quota_test.go | 2 +- internal/gateway/key_quota_wiring_test.go | 4 +- internal/gateway/keys.go | 4 +- internal/gateway/messages.go | 6 +- internal/gateway/messages_test.go | 4 +- internal/gateway/mount.go | 2 +- internal/gateway/mount_test.go | 4 +- internal/gateway/overflow_err_test.go | 4 +- internal/gateway/plugin_call_api_test.go | 4 +- internal/gateway/plugin_http_routes.go | 2 +- internal/gateway/plugin_wiring_test.go | 4 +- internal/gateway/plugins_api.go | 2 +- internal/gateway/plugins_api_test.go | 6 +- internal/gateway/serializer.go | 6 +- internal/gateway/serializer_test.go | 4 +- internal/gateway/server.go | 4 +- internal/gateway/source_fields_test.go | 4 +- internal/gateway/sources_api_test.go | 2 +- internal/gateway/stats_unknown_key_test.go | 2 +- internal/gateway/thinking_test.go | 2 +- internal/gateway/token_units_test.go | 2 +- internal/gateway/ui_image_page_test.go | 2 +- internal/provider/gemini_endpoint_test.go | 6 +- internal/provider/model_kind_test.go | 2 +- internal/provider/provider.go | 6 +- internal/provider/provider_test.go | 6 +- internal/provider/retry_policy_test.go | 4 +- internal/scheduler/empty_response_test.go | 2 +- internal/scheduler/scheduler.go | 2 +- internal/scheduler/scheduler_test.go | 2 +- packaging/config.example.yaml | 14 +- packaging/core-dist.sh | 59 +++-- packaging/install-browser-stack.sh | 2 +- packaging/migrate-to-modelrouter.sh | 201 ++++++++++++++++++ ...{llmsproxy.service => modelrouter.service} | 18 +- packaging/nfpm.yaml | 24 +-- packaging/postinst.sh | 14 +- packaging/prerm.sh | 10 +- 107 files changed, 548 insertions(+), 308 deletions(-) rename cmd/{llmsproxy => modelrouter}/main.go (70%) create mode 100755 packaging/migrate-to-modelrouter.sh rename packaging/{llmsproxy.service => modelrouter.service} (88%) diff --git a/Makefile b/Makefile index 9940bed..835e51b 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core) export BINARY -BINARY=llmsproxy +BINARY=modelrouter export BUILD_DIR BUILD_DIR=build export GUI_DIR @@ -20,7 +20,7 @@ all: build # ---- core binary (server users / embedded core) ---- build: @mkdir -p $(BUILD_DIR) - CGO_ENABLED=1 go build -tags luajit -trimpath -o $(BUILD_DIR)/$(BINARY) ./cmd/llmsproxy + CGO_ENABLED=1 go build -tags luajit -trimpath -o $(BUILD_DIR)/$(BINARY) ./cmd/modelrouter @echo "Built: $(BUILD_DIR)/$(BINARY) ($(VERSION))" # ---- core distribution packages (deb + rpm + tar.gz) ----- diff --git a/cmd/gui/main.js b/cmd/gui/main.js index 94dc370..a57c959 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -1,5 +1,5 @@ // ModelRouter Desktop GUI — main process -// Clash-Verge-style: bundles and manages an embedded llmsproxy core instance +// Desktop shell: bundles and manages an embedded modelrouter core instance // in a per-user profile dir (userData), auto-starts it, keeps a tray icon, // supports launch-at-login. Server-side users keep using the pure Go binary. const { @@ -50,11 +50,27 @@ const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml"); const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins"); // Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable). // Dev: cmd/gui/bin, populated by prepare-core.js. -const CORE_EXE = path.join( +// The embedded core is named after the product. Installs produced before the +// llmsproxy -> modelrouter rename still carry the old file name inside their +// profile, so resolution falls back to it instead of declaring the core missing. +const CORE_BIN_DIR = path.join( app.isPackaged ? process.resourcesPath : __dirname, "bin", +); +const CORE_EXE = path.join( + CORE_BIN_DIR, + process.platform === "win32" ? "modelrouter.exe" : "modelrouter", +); +const LEGACY_CORE_EXE = path.join( + CORE_BIN_DIR, process.platform === "win32" ? "llmsproxy.exe" : "llmsproxy", ); + +function resolveCoreExe() { + if (fs.existsSync(CORE_EXE)) return CORE_EXE; + if (fs.existsSync(LEGACY_CORE_EXE)) return LEGACY_CORE_EXE; + return CORE_EXE; +} // Packaged window icon: must be a REAL file on disk (extraResource), not an // asar path — Electron/GTK pulls BrowserWindow icons from the filesystem and // crashes (SIGSEGV) when given an asar virtual path on Linux. @@ -163,7 +179,7 @@ function unsealViaCore() { let key = ""; try { const out = execFileSync( - CORE_EXE, + resolveCoreExe(), ["-config", CONFIG_FILE, "-show-secrets"], { encoding: "utf-8", @@ -215,7 +231,7 @@ function coreStarted() { function writeDefaultEmbeddedConfig() { // Generate a self-contained embedded profile with a fresh random admin key. // No upstream sources are preconfigured — the desktop user adds them via - // the WebUI's sources page (like Clash Verge's empty-by-default profile). + // the WebUI's sources page (empty by default, like the WebUI profile). // If a config already exists we do NOT overwrite it (keep user's edits). if (fs.existsSync(CONFIG_FILE)) return; const key = "sk-gw-" + crypto.randomBytes(16).toString("hex"); @@ -272,8 +288,9 @@ function startCore() { console.log("core already running"); return; } - if (!fs.existsSync(CORE_EXE)) { - console.error("embedded core binary missing:", CORE_EXE); + const coreExe = resolveCoreExe(); + if (!fs.existsSync(coreExe)) { + console.error("embedded core binary missing:", coreExe); return; } // ensure profile dir + config + local adapters exist @@ -281,9 +298,8 @@ function startCore() { if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig(); pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log); fs.mkdirSync(PLUGINS_DIR, { recursive: true }); - pluginSync.syncBundledPluginsOnUpgrade(PROFILE_DIR, PLUGINS_DIR, app.getVersion(), log); - coreProc = spawn(CORE_EXE, ["-config", CONFIG_FILE], { + coreProc = spawn(coreExe, ["-config", CONFIG_FILE], { cwd: PROFILE_DIR, env: Object.assign({}, process.env, { LLMS_PROXY_GUI: "1", @@ -686,7 +702,7 @@ function createWindow() { }); } // Silent start: `--silent` CLI flag or settings.silentStart keeps the app - // in the tray without showing the window (Clash-Verge style). First run + // in the tray without showing the window (desktop-shell behaviour). First run // always shows the window so the user sees the dashboard. mainWindow.once("ready-to-show", () => { const firstRun = !fs.existsSync( @@ -810,8 +826,8 @@ ipcMain.handle("core:state", () => ({ port: settings.port || DEFAULT_PORT, profileDir: PROFILE_DIR, configFile: CONFIG_FILE, - coreExe: CORE_EXE, - coreExists: fs.existsSync(CORE_EXE), + coreExe: resolveCoreExe(), + coreExists: fs.existsSync(resolveCoreExe()), })); ipcMain.handle("core:start", () => { app.isQuitting = false; @@ -883,7 +899,7 @@ if (gotLock) { createWindow(); createTray(); - // start embedded core if not already running (another instance of llmsproxy) + // start embedded core if not already running (another instance of modelrouter) setTimeout(async () => { const p = await probeEmbedded(); if (!p.ok) { diff --git a/cmd/gui/package.json b/cmd/gui/package.json index 0cec281..d3d1ba8 100644 --- a/cmd/gui/package.json +++ b/cmd/gui/package.json @@ -39,8 +39,8 @@ }, "extraResources": [ { - "from": "bin/llmsproxy", - "to": "bin/llmsproxy" + "from": "bin/modelrouter", + "to": "bin/modelrouter" }, { "from": "build/icon.png", @@ -66,8 +66,8 @@ "icon": "build/icon.ico", "extraResources": [ { - "from": "bin/llmsproxy.exe", - "to": "bin/llmsproxy.exe" + "from": "bin/modelrouter.exe", + "to": "bin/modelrouter.exe" }, { "from": "bin/lua51.dll", diff --git a/cmd/gui/scripts/prepare-core.js b/cmd/gui/scripts/prepare-core.js index fbba678..1accfe3 100644 --- a/cmd/gui/scripts/prepare-core.js +++ b/cmd/gui/scripts/prepare-core.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Build the embedded llmsproxy core(s) for the GUI package. +// Build the embedded modelrouter core(s) for the GUI package. // node prepare-core.js -> linux (native, luajit) + win (cross, best effort) // node prepare-core.js linux -> linux only // node prepare-core.js win -> win only @@ -12,21 +12,21 @@ const binDir = path.join(guiDir, "bin"); const projectRoot = path.join(guiDir, "..", ".."); function buildLinux() { - const out = path.join(binDir, "llmsproxy"); - console.log("[prepare-core] building llmsproxy (linux/luajit)..."); + const out = path.join(binDir, "modelrouter"); + console.log("[prepare-core] building modelrouter (linux/luajit)..."); fs.mkdirSync(binDir, { recursive: true }); execFileSync( "go", - ["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/llmsproxy"], + ["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"], { cwd: projectRoot, stdio: "inherit" }, ); return out; } function buildWin() { - const out = path.join(binDir, "llmsproxy.exe"); + const out = path.join(binDir, "modelrouter.exe"); console.log( - "[prepare-core] cross-building llmsproxy.exe (windows/amd64, luajit)...", + "[prepare-core] cross-building modelrouter.exe (windows/amd64, luajit)...", ); fs.mkdirSync(binDir, { recursive: true }); const cc = process.env.MINGW_CC || "x86_64-w64-mingw32-gcc"; @@ -43,7 +43,7 @@ function buildWin() { } execFileSync( "go", - ["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/llmsproxy"], + ["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"], { cwd: projectRoot, stdio: "inherit", diff --git a/cmd/llmsproxy/main.go b/cmd/modelrouter/main.go similarity index 70% rename from cmd/llmsproxy/main.go rename to cmd/modelrouter/main.go index 519169a..3ada2f1 100644 --- a/cmd/llmsproxy/main.go +++ b/cmd/modelrouter/main.go @@ -1,4 +1,4 @@ -// Command llmsproxy is a standalone gateway that exposes multiple upstream LLM +// 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 @@ -16,9 +16,9 @@ import ( "syscall" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/core" - "llmsproxy/internal/gateway" + "modelrouter/internal/config" + "modelrouter/internal/core" + "modelrouter/internal/gateway" ) func main() { @@ -33,12 +33,12 @@ func main() { // error here instead of being silently created. if *checkOnly { if _, err := os.Stat(*cfgPath); err != nil { - log.Fatalf("[llmsproxy] check: %v", err) + log.Fatalf("[modelrouter] check: %v", err) } if _, err := config.Load(*cfgPath); err != nil { - log.Fatalf("[llmsproxy] check: %v", err) + log.Fatalf("[modelrouter] check: %v", err) } - log.Printf("[llmsproxy] check: %s is valid", *cfgPath) + log.Printf("[modelrouter] check: %s is valid", *cfgPath) return } @@ -48,34 +48,34 @@ func main() { // 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) + log.Fatalf("[modelrouter] show-secrets: %v", err) } if err := config.PrintSecrets(*cfgPath); err != nil { - log.Fatalf("[llmsproxy] show-secrets: %v", err) + log.Fatalf("[modelrouter] show-secrets: %v", err) } return } created, err := config.EnsureDefault(*cfgPath) if err != nil { - log.Fatalf("[llmsproxy] config: %v", err) + log.Fatalf("[modelrouter] config: %v", err) } c, err := core.New(*cfgPath) if err != nil { - log.Fatalf("[llmsproxy] core: %v", err) + log.Fatalf("[modelrouter] 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) + 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("[llmsproxy] gateway: %v", err) + log.Fatalf("[modelrouter] gateway: %v", err) } // Ops hygiene: surface the two most common footguns instead of silently @@ -88,16 +88,16 @@ func main() { // (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") + 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("[llmsproxy] WARNING: gateway key %q looks like the starter/example key — rotate it before exposing the gateway", k.Key) + 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("[llmsproxy] WARNING: listen=%s binds ALL interfaces — bind an internal address in production", l) + log.Printf("[modelrouter] WARNING: listen=%s binds ALL interfaces — bind an internal address in production", l) } srv := &http.Server{ @@ -108,22 +108,22 @@ func main() { 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)", + 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("[llmsproxy] server: %v", err) + log.Fatalf("[modelrouter] server: %v", err) } return } - log.Printf("[llmsproxy] listening HTTP on %s (default_model=%s, models=%v, adapters=%d)", + 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("[llmsproxy] server: %v", err) + log.Fatalf("[modelrouter] server: %v", err) } }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) <-stop - log.Printf("[llmsproxy] shutting down") + log.Printf("[modelrouter] shutting down") } diff --git a/deploy.sh b/deploy.sh index 10429bf..0d5e92d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # ============================================================ -# llmsproxy 原子化滚动部署脚本 +# modelrouter 原子化滚动部署脚本 # # 策略: # 1. /tmp (tmpfs) 构建,/usr/local/bin (ext4) 部署 -# 2. 构建后先 copy 到目标文件系统(.llmsproxy.tmp), +# 2. 构建后先 copy 到目标文件系统(.modelrouter.tmp), # 再 rename(2) 做原子替换(同一文件系统内 mv 是原子的) # 3. runtime.json 不替换(保留运行时密钥、审计状态) # 4. systemctl restart → 服务中断仅在 Go 进程重启瞬间 (<500ms) @@ -28,20 +28,21 @@ set -euo pipefail # ---------- 配置 ---------- -readonly TARGET_BIN="/usr/local/bin/llmsproxy" -readonly TARGET_CONFIG="/etc/llmsproxy/config.yaml" -readonly TARGET_ADAPTERS="/etc/llmsproxy/adapters" -readonly TARGET_PLUGINS="/etc/llmsproxy/plugins" -readonly REPO_DIR="/home/program/llmsproxy" +readonly TARGET_BIN="/usr/local/bin/modelrouter" +readonly TARGET_CONFIG="/etc/modelrouter/config.yaml" +readonly TARGET_ADAPTERS="/etc/modelrouter/adapters" +readonly TARGET_PLUGINS="/etc/modelrouter/plugins" +# 仓库所在目录不等于产品名:这里保持真实路径,除非用 MODELROUTER_REPO_DIR 覆盖。 +readonly REPO_DIR="${MODELROUTER_REPO_DIR:-/home/program/llmsproxy}" readonly HEALTH_URL="http://127.0.0.1:8081/v1/models" -readonly BIN_NAME="llmsproxy" +readonly BIN_NAME="modelrouter" # 构建目录。默认落在仓库下的构建工作区,而不是 /tmp:本机 /tmp 是 8.8G # tmpfs,实测被 Go 构建缓存占满后会让 go test/build 在链接阶段直接失败。 -# 可用 LLMSPROXY_BUILD_DIR 覆盖;同一目录也被测试用作 GOTMPDIR。 +# 可用 MODELROUTER_BUILD_DIR 覆盖;同一目录也被测试用作 GOTMPDIR。 readonly BUILD_SUBDIR=".build-work" -if [[ -n "${LLMSPROXY_BUILD_DIR:-}" ]]; then - readonly TMP_DIR="${LLMSPROXY_BUILD_DIR}" +if [[ -n "${MODELROUTER_BUILD_DIR:-}" ]]; then + readonly TMP_DIR="${MODELROUTER_BUILD_DIR}" else readonly TMP_DIR="${REPO_DIR}/${BUILD_SUBDIR}" fi @@ -59,7 +60,7 @@ usage() { cat <<'EOF' 用法: ./deploy.sh [--config ] - --config 投放这份配置到 /etc/llmsproxy/config.yaml(原子替换)。 + --config 投放这份配置到 /etc/modelrouter/config.yaml(原子替换)。 省略时不改动线上配置,只部署二进制与适配器。 无论是否指定,重启前都会用新二进制 -check 预检线上配置。 @@ -103,6 +104,17 @@ precheck() { command -v systemctl &>/dev/null || fail "未找到 systemctl" [[ $(id -u) -eq 0 ]] || fail "需要 root 权限" + # 改名保护:本脚本现在部署到 /etc/modelrouter。若机器上仍是改名前的 + # /etc/llmsproxy 且新路径尚未建立,直接继续会创建一份**空配置**并让新 + # 服务在没有 key/源的情况下起来 —— 看起来成功,实际网关拒掉所有请求。 + if [[ -d /etc/llmsproxy && ! -e "$TARGET_CONFIG" ]]; then + fail "检测到改名前的部署 /etc/llmsproxy,但 $TARGET_CONFIG 不存在。 + 先迁移(dry-run 默认,确认后加 --apply): + sudo ./packaging/migrate-to-modelrouter.sh + sudo ./packaging/migrate-to-modelrouter.sh --apply + 如需全新部署,先把旧目录移开:mv /etc/llmsproxy /etc/llmsproxy.bak" + fi + if [[ -n "$NEW_CONFIG_SRC" ]]; then [[ -f "$NEW_CONFIG_SRC" ]] || fail "待部署配置不存在: $NEW_CONFIG_SRC" # 同一文件比对:--config 指向线上文件本身时没有"投放"动作可做, @@ -153,7 +165,7 @@ build() { -trimpath \ -ldflags="-s -w" \ -o "$TMP_DIR/bin/$BIN_NAME" \ - ./cmd/llmsproxy + ./cmd/modelrouter NEW_BIN="$TMP_DIR/bin/$BIN_NAME" NEW_SIZE=$(stat -c%s "$NEW_BIN") @@ -254,7 +266,7 @@ verify_config() { # ---------- 密钥可解封校验 ---------- # config.yaml 里的凭据以 enc:v1: 密文落盘,解密钥匙是 master.key(或 -# LLMS_PROXY_MASTER_KEY)。一旦钥匙丢失/错位,配置在**启动时**才会报错, +# MODELROUTER_MASTER_KEY)。一旦钥匙丢失/错位,配置在**启动时**才会报错, # 那时服务已经处于「二进制已替换 + 配置已替换」的半死状态,只能靠回滚。 # 所以在替换任何东西之前先验:能不能用现有的钥匙解开现有的密文。 # @@ -279,21 +291,22 @@ verify_master_key() { [[ -n "$runtime_file" ]] || runtime_file="$(dirname "$TARGET_CONFIG")/runtime.json" key_file="$(dirname "$runtime_file")/master.key" - if [[ -n "${LLMS_PROXY_MASTER_KEY:-}" ]]; then - log " 钥匙来源: LLMS_PROXY_MASTER_KEY(环境变量)" + MASTER_KEY_ENV="${MODELROUTER_MASTER_KEY:-${LLMS_PROXY_MASTER_KEY:-}}" + if [[ -n "$MASTER_KEY_ENV" ]]; then + log " 钥匙来源: MODELROUTER_MASTER_KEY(环境变量)" # 走真实解密路径验证,而不是只检查格式 if out=$("$TARGET_BIN" -show-secrets -config "$TARGET_CONFIG" 2>&1); then log "OK 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)" return 0 fi - err "凭据无法用 LLMS_PROXY_MASTER_KEY 解封,未替换任何文件:" + err "凭据无法用 MODELROUTER_MASTER_KEY 解封,未替换任何文件:" printf '%s\n' "$out" >&2 return 1 fi if [[ ! -f "$key_file" ]]; then err "配置含 $sealed 处密文,但钥匙文件不存在: $key_file" - err " 没有它,部署后服务将无法启动。恢复 master.key 或设置 LLMS_PROXY_MASTER_KEY 后重试。" + err " 没有它,部署后服务将无法启动。恢复 master.key 或设置 MODELROUTER_MASTER_KEY 后重试。" return 1 fi log " 钥匙文件: $key_file ($(stat -c '%A' "$key_file" 2>/dev/null || echo '?'),$(wc -c < "$key_file") 字节)" @@ -312,7 +325,7 @@ verify_master_key() { # ---------- 备份目录保留策略 ---------- # 每次部署都新建一个 .bak.<时间戳> 目录,而回滚只用到最近一次 # ($BACKUP_BIN / $BACKUP_CONFIG 都是单文件覆盖)。多出来的目录从没人清理, -# 实测线上已积累 71 个,/etc/llmsproxy 因此涨到 122M。 +# 实测线上已积累 71 个,/etc/modelrouter 因此涨到 122M。 # # 保留最近 KEEP_BACKUPS 份足够回滚,同时给目录数设上限——否则一次误配置 # 就可能在几秒内造出成百上千个目录。 @@ -327,7 +340,7 @@ KEEP_BACKUPS=5 prune_backups() { local prefix="$1" - local base="/etc/llmsproxy" + local base="/etc/modelrouter" [[ -d "$base" ]] || return 0 # 防误用:前缀必须是已知的两个(避免有人传空串删掉整个目录) case "$prefix" in @@ -393,7 +406,7 @@ sync_adapters() { [[ -d "$SRC_ADAPTERS" ]] || fail "内嵌适配器目录不存在: $SRC_ADAPTERS" # 备份旧的适配器文件(含任何运行时覆盖版本,如 opencode.lua) - BACKUP_DIR="/etc/llmsproxy/adapters.bak.$(date +%Y%m%d%H%M%S)" + BACKUP_DIR="/etc/modelrouter/adapters.bak.$(date +%Y%m%d%H%M%S)" if [[ -d "$TARGET_ADAPTERS" ]]; then mkdir -p "$BACKUP_DIR" cp -rf "$TARGET_ADAPTERS/"*.lua "$BACKUP_DIR/" 2>/dev/null || true @@ -425,7 +438,7 @@ sync_adapters() { # # 早期实现是「只补缺,已存在一律不动」,看着安全,实际造成一个功能性缺口: # 内置插件本身的**修复无法下发**。v2.0.3/v2.0.4/v2.0.5 三个渠道修复就这样 -# 全部卡在仓库里 —— 二进制换了、/etc/llmsproxy/plugins/channels.lua 还是旧版, +# 全部卡在仓库里 —— 二进制换了、/etc/modelrouter/plugins/channels.lua 还是旧版, # 生产行为毫无变化。内置源码的正确性由仓库保证,部署必须与二进制内容对齐。 # # 「只补缺」原本想保护的是「运维改过 / 运行时新增的插件」,但那些都是**非内置 @@ -439,7 +452,7 @@ sync_plugins() { mkdir -p "$TARGET_PLUGINS" # 备份将被覆盖/新增的内置插件(非内置名字不进入循环,天然不动) - BACKUP_DIR="/etc/llmsproxy/plugins.bak.$(date +%Y%m%d%H%M%S)" + BACKUP_DIR="/etc/modelrouter/plugins.bak.$(date +%Y%m%d%H%M%S)" mkdir -p "$BACKUP_DIR" local added=0 updated=0 same=0 for src in "$SRC_PLUGINS"/*.lua; do @@ -481,8 +494,8 @@ sync_plugins() { # ---------- 重启服务 ---------- restart_service() { log "重启服务(中断 <500ms)" - systemctl restart llmsproxy.service || fail "systemd restart 失败" - log "OK llmsproxy.service restarted" + systemctl restart modelrouter.service || fail "systemd restart 失败" + log "OK modelrouter.service restarted" } # ---------- 健康检查 ---------- @@ -505,7 +518,7 @@ healthcheck() { (( attempt++ )) done - warn "健康检查未通过,请检查服务状态: systemctl status llmsproxy.service" + warn "健康检查未通过,请检查服务状态: systemctl status modelrouter.service" return 1 } @@ -541,7 +554,7 @@ rollback() { if (( restored_any == 0 )); then err "无任何回滚点,无法自动恢复" - err "请手动检查: systemctl status llmsproxy.service && journalctl -u llmsproxy -n 50" + err "请手动检查: systemctl status modelrouter.service && journalctl -u modelrouter -n 50" return 1 fi @@ -555,9 +568,9 @@ rollback() { fi log "重启服务(旧版本)" - systemctl restart llmsproxy.service || { + systemctl restart modelrouter.service || { err "重启失败,服务可能完全宕机" - err "请手动检查: systemctl status llmsproxy.service" + err "请手动检查: systemctl status modelrouter.service" return 1 } @@ -592,7 +605,7 @@ print_summary() { # 于是坏配置在服务仍然健康时就被拦下,根本不会触发重启。 main() { parse_args "$@" - log "═══ llmsproxy 原子部署开始 ═══" + log "═══ modelrouter 原子部署开始 ═══" precheck # 密钥校验放在 build/替换之前:它的失败后果最重(配置里的密文永久解不开), # 必须在动任何文件之前就挡住。此阶段用的是**当前线上二进制**,它已经 diff --git a/docs/channels-parity.md b/docs/channels-parity.md index 1c96352..8e87b32 100644 --- a/docs/channels-parity.md +++ b/docs/channels-parity.md @@ -389,10 +389,10 @@ flow 类型,不是"登录成功"。 channels 插件用 `storeDir()` 决定存储位置,它有多级兜底: ``` -env LLMSPROXY_PLUGIN_DIR → settings.store_dir → 读 /etc/llmsproxy/config.yaml 的 plugin_dir → 硬编码 +env MODELROUTER_PLUGIN_DIR → settings.store_dir → 读 /etc/llmsproxy/config.yaml 的 plugin_dir → 硬编码 ``` -测试台没设 `LLMSPROXY_PLUGIN_DIR`,**后两级直接命中生产**: +测试台没设 `MODELROUTER_PLUGIN_DIR`,**后两级直接命中生产**: ``` 测试插件目录 = /tmp/TestXXX/002/plugins @@ -404,7 +404,7 @@ storeDir 结果 = /etc/llmsproxy/plugins ← 探针实测输出 `.channels-store.json` 从 1730 覆盖成 1986 字节、账号池清空(inode 都换了)。 只有 `internal/lua` 复现,`internal/core` 不复现 —— 因为 core 的测试台 -(`kernelTestCore`)设了 `LLMSPROXY_PLUGIN_DIR`。**这是隔离做得不一致**的代价。 +(`kernelTestCore`)设了 `MODELROUTER_PLUGIN_DIR`。**这是隔离做得不一致**的代价。 同类前科:曾把 26 个测试账号写进生产 store。两次同一类错误。 diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index ba48b53..49df4c1 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -1,4 +1,4 @@ -// Package e2e runs the real llmsproxy binary against mocked upstreams over +// Package e2e runs the real modelrouter binary against mocked upstreams over // real HTTP: startup, auth, chat, streaming SSE, image generation, and // AUTO failover when a higher-priority source fails. package e2e @@ -100,15 +100,15 @@ type gatewayUnderTest struct { func buildBinary(t *testing.T) string { t.Helper() dir := t.TempDir() - bin := filepath.Join(dir, "llmsproxy") + bin := filepath.Join(dir, "modelrouter") if runtime.GOOS == "windows" { bin += ".exe" // go build writes the exact name; Windows needs the suffix to exec } - out, err := exec.Command("go", "build", "-tags", "luajit", "-o", bin, "llmsproxy/cmd/llmsproxy").CombinedOutput() + out, err := exec.Command("go", "build", "-tags", "luajit", "-o", bin, "modelrouter/cmd/modelrouter").CombinedOutput() if err != nil && runtime.GOOS == "windows" { // local Windows dev box may lack LuaJIT: fall back to the bundled // Lua runtime (the e2e adapters used here are passthrough-only) - out, err = exec.Command("go", "build", "-o", bin, "llmsproxy/cmd/llmsproxy").CombinedOutput() + out, err = exec.Command("go", "build", "-o", bin, "modelrouter/cmd/modelrouter").CombinedOutput() } if err != nil { t.Fatalf("build: %v\n%s", err, out) diff --git a/go.mod b/go.mod index 290a1f5..0e7326c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module llmsproxy +module modelrouter go 1.25.0 diff --git a/internal/config/config.go b/internal/config/config.go index 11df0c7..6e75c3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -161,7 +161,7 @@ func (r *RetryPolicy) DelayDuration() time.Duration { // Load reads and validates a config file. When the file does not exist yet a // default config is generated at that path first (first-run bootstrap), so a -// fresh binary just works: `llmsproxy -config /path/to/config.yaml`. +// fresh binary just works: `modelrouter -config /path/to/config.yaml`. func Load(path string) (*Config, error) { if _, err := EnsureDefault(path); err != nil { return nil, err diff --git a/internal/config/secret.go b/internal/config/secret.go index a4485a6..17e1145 100644 --- a/internal/config/secret.go +++ b/internal/config/secret.go @@ -27,8 +27,9 @@ func NewGatewayKey() (string, error) { // SecretBox encrypts and decrypts sensitive values (upstream API keys, // custom header values, gateway keys) for persist-time protection. The -// master key comes from the LLMS_PROXY_MASTER_KEY environment variable -// (hex, 64 chars) or from a generated /master.key file. +// master key comes from the MODELROUTER_MASTER_KEY environment variable +// (hex, 64 chars; the pre-rename LLMS_PROXY_MASTER_KEY is still accepted) or +// from a generated /master.key file. type SecretBox struct { aead cipher.AEAD } @@ -37,10 +38,18 @@ type SecretBox struct { // file next to the runtime file. If neither exists, a fresh random key is // generated and written to master.key with mode 0600. func NewSecretBox(runtimeFile string) (*SecretBox, error) { - if v := os.Getenv("LLMS_PROXY_MASTER_KEY"); v != "" { - key, err := hex.DecodeString(strings.TrimSpace(v)) + // New name first, then the pre-rename name: an existing unit file that still + // exports LLMS_PROXY_MASTER_KEY must keep starting after an upgrade. + envKey := os.Getenv("MODELROUTER_MASTER_KEY") + envName := "MODELROUTER_MASTER_KEY" + if envKey == "" { + envKey = os.Getenv("LLMS_PROXY_MASTER_KEY") + envName = "LLMS_PROXY_MASTER_KEY" + } + if envKey != "" { + key, err := hex.DecodeString(strings.TrimSpace(envKey)) if err != nil || len(key) != 32 { - return nil, fmt.Errorf("LLMS_PROXY_MASTER_KEY: expected 64 hex chars") + return nil, fmt.Errorf("%s: expected 64 hex chars", envName) } return newBox(key) } diff --git a/internal/core/channels_authorize_test.go b/internal/core/channels_authorize_test.go index 5b7d1e6..072d6fb 100644 --- a/internal/core/channels_authorize_test.go +++ b/internal/core/channels_authorize_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、 diff --git a/internal/core/channels_callback_loopback_test.go b/internal/core/channels_callback_loopback_test.go index ba43d20..10ecde3 100644 --- a/internal/core/channels_callback_loopback_test.go +++ b/internal/core/channels_callback_loopback_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 三个 callback 平台的回调路径**各是注册值**,且校验规则不同: diff --git a/internal/core/channels_clear_credentialless_test.go b/internal/core/channels_clear_credentialless_test.go index 72d24c9..d9e234d 100644 --- a/internal/core/channels_clear_credentialless_test.go +++ b/internal/core/channels_clear_credentialless_test.go @@ -3,7 +3,7 @@ package core import ( "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ 无凭据账号的批量清理:**只删没有凭据的**,有凭据的一律不动。 diff --git a/internal/core/channels_codearts_catalog_test.go b/internal/core/channels_codearts_catalog_test.go index 152a2e1..f388c76 100644 --- a/internal/core/channels_codearts_catalog_test.go +++ b/internal/core/channels_codearts_catalog_test.go @@ -8,7 +8,7 @@ import ( "sync" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ codearts 的模型目录必须从**两个**签名端点拉取并合并。 diff --git a/internal/core/channels_codearts_oauth_flow_test.go b/internal/core/channels_codearts_oauth_flow_test.go index 5ac1257..f2851b0 100644 --- a/internal/core/channels_codearts_oauth_flow_test.go +++ b/internal/core/channels_codearts_oauth_flow_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // CodeArts 端到端:起真实回调服务器 → 模上游回 code → 走真实 STS 交换 diff --git a/internal/core/channels_codearts_port_test.go b/internal/core/channels_codearts_port_test.go index 23d49b8..ffcba95 100644 --- a/internal/core/channels_codearts_port_test.go +++ b/internal/core/channels_codearts_port_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // CodeArts 的 portal 拒绝 <10000 的回调端口(参考 listenOnCallbackPort 的 diff --git a/internal/core/channels_lobster_claim_test.go b/internal/core/channels_lobster_claim_test.go index 45d7acb..5384cb5 100644 --- a/internal/core/channels_lobster_claim_test.go +++ b/internal/core/channels_lobster_claim_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 有道每日活动领取是**三步流程**,参考注释点名:无可用活动是**正常业务状态**, diff --git a/internal/core/channels_lobster_exchange_test.go b/internal/core/channels_lobster_exchange_test.go index 26fe322..f7d65f7 100644 --- a/internal/core/channels_lobster_exchange_test.go +++ b/internal/core/channels_lobster_exchange_test.go @@ -2,7 +2,7 @@ package core import ( "encoding/json" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" "net/http" "net/http/httptest" "testing" diff --git a/internal/core/channels_lobster_loopback_test.go b/internal/core/channels_lobster_loopback_test.go index a86db57..073ab03 100644 --- a/internal/core/channels_lobster_loopback_test.go +++ b/internal/core/channels_lobster_loopback_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 有道登录闭环的判据:portal 会把授权重定向回 **本机 loopback 接收器** diff --git a/internal/core/channels_loomy_test.go b/internal/core/channels_loomy_test.go index 7ee15be..4158a89 100644 --- a/internal/core/channels_loomy_test.go +++ b/internal/core/channels_loomy_test.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // loomyHarness:Loomy 微信扫码长轮询的测试台。 diff --git a/internal/core/channels_models_remote_test.go b/internal/core/channels_models_remote_test.go index c4c560a..3f8b71e 100644 --- a/internal/core/channels_models_remote_test.go +++ b/internal/core/channels_models_remote_test.go @@ -8,7 +8,7 @@ import ( "sync" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 远端模型目录的判据:**远端为主 + 兜底补位**(照参考 reconcileWithFallback)。 diff --git a/internal/core/channels_more_test.go b/internal/core/channels_more_test.go index 21d8714..7ea5d58 100644 --- a/internal/core/channels_more_test.go +++ b/internal/core/channels_more_test.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // channelsMoreTest:新引入的三条真实流程 + 两个边界流的行为对齐测试。 diff --git a/internal/core/channels_orphan_callback_test.go b/internal/core/channels_orphan_callback_test.go index f1f896b..4ce57c5 100644 --- a/internal/core/channels_orphan_callback_test.go +++ b/internal/core/channels_orphan_callback_test.go @@ -5,12 +5,12 @@ import ( "path/filepath" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ 插件加载时必须清掉上一进程留下的孤儿回调文件。 // -// 真实事故:生产 `/etc/llmsproxy/plugins/` 攒了 **18 个 callback-*.port** +// 真实事故:生产 `/etc/modelrouter/plugins/` 攒了 **18 个 callback-*.port** // (对应的 .json 早已消失)。它们永远不会被清,因为: // // · loopbacks 追踪表只活在内存(plugin.state.loopbacks); @@ -25,7 +25,7 @@ func TestPluginLoadSweepsOrphanCallbackFiles(t *testing.T) { if err := os.MkdirAll(pluginDir, 0755); err != nil { t.Fatal(err) } - t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir) + t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir) // 摆上一轮的残留(模拟上一进程被强杀后的现场) orphans := []string{ diff --git a/internal/core/channels_plugin_test.go b/internal/core/channels_plugin_test.go index 3100b51..ad71574 100644 --- a/internal/core/channels_plugin_test.go +++ b/internal/core/channels_plugin_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // TestChannelsPluginEndToEnd 是「同类插件」的完整验收: diff --git a/internal/core/channels_pool_reason_test.go b/internal/core/channels_pool_reason_test.go index 5b487e4..20f35ad 100644 --- a/internal/core/channels_pool_reason_test.go +++ b/internal/core/channels_pool_reason_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ 「为什么没有可用账号」必须区分**真的用尽**与**查不到**。 diff --git a/internal/core/channels_pool_test.go b/internal/core/channels_pool_test.go index 67cfa25..7f6ff38 100644 --- a/internal/core/channels_pool_test.go +++ b/internal/core/channels_pool_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" "fmt" "net/http" diff --git a/internal/core/channels_qoder_adapter_test.go b/internal/core/channels_qoder_adapter_test.go index 5879094..43aa450 100644 --- a/internal/core/channels_qoder_adapter_test.go +++ b/internal/core/channels_qoder_adapter_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // qoderReason 调真实的 transform_error 钩子(不自己重算)。 diff --git a/internal/core/channels_qoder_claim_test.go b/internal/core/channels_qoder_claim_test.go index 583475c..5f8fc10 100644 --- a/internal/core/channels_qoder_claim_test.go +++ b/internal/core/channels_qoder_claim_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // Qoder 活动领取有三条参考用**抓包 + 用户报障**定位过的「会误报」陷阱, diff --git a/internal/core/channels_qoder_test.go b/internal/core/channels_qoder_test.go index 3b68f70..b0a8910 100644 --- a/internal/core/channels_qoder_test.go +++ b/internal/core/channels_qoder_test.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // qoderHarness:Qoder PKCE 设备码轮的测试台。 diff --git a/internal/core/channels_raccoon_test.go b/internal/core/channels_raccoon_test.go index 3b67a41..538c9f8 100644 --- a/internal/core/channels_raccoon_test.go +++ b/internal/core/channels_raccoon_test.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // raccoonHarness:Raccoon 内置二维码扫码登录的测试台。 diff --git a/internal/core/channels_remove_source_perm_test.go b/internal/core/channels_remove_source_perm_test.go index d811a10..ebb3f5f 100644 --- a/internal/core/channels_remove_source_perm_test.go +++ b/internal/core/channels_remove_source_perm_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ 删除账号必须**真的把源撤掉** —— 且撤不掉时不能静默。 @@ -190,8 +190,14 @@ func TestChannelsDeclaresEveryKernelActionItCalls(t *testing.T) { // ② 抽出实际调用的动作 called := map[string]bool{} - for _, m := range regexp.MustCompile(`__llmsproxy_kernel\.call\(\s*"([a-z_]+)"`).FindAllStringSubmatch(s, -1) { - called[m[1]] = true + // 插件只允许走公开 SDK(modelrouter.kernel);旧名 __llmsproxy_kernel 是 + // 兼容别名,新代码用它就说明插件又在依赖内核私有全局了。 + for _, m := range regexp.MustCompile(`\b(?:modelrouter|__llmsproxy_kernel)\.kernel\.call\(\s*"([a-z_]+)"|\b__llmsproxy_kernel\.call\(\s*"([a-z_]+)"`).FindAllStringSubmatch(s, -1) { + name := m[1] + if name == "" { + name = m[2] // 旧名的分支 + } + called[name] = true } if len(called) == 0 { t.Fatal("没扫描到任何内核调用(判据失效)") diff --git a/internal/core/channels_reorder_effect_test.go b/internal/core/channels_reorder_effect_test.go index 9c3e5bc..25683ec 100644 --- a/internal/core/channels_reorder_effect_test.go +++ b/internal/core/channels_reorder_effect_test.go @@ -3,7 +3,7 @@ package core import ( "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ account_reorder 必须**真的改变内核里的源顺序**。 diff --git a/internal/core/channels_store_atomic_test.go b/internal/core/channels_store_atomic_test.go index 7ac21e1..883fad3 100644 --- a/internal/core/channels_store_atomic_test.go +++ b/internal/core/channels_store_atomic_test.go @@ -33,8 +33,8 @@ func TestAccountPoolStoreIsAtomicAndConsistent(t *testing.T) { storePath := filepath.Join(dir, ".channels-store.json") t.Logf("插件存储目录: %s", dir) // ⚠️ 必须在**测试目录**里,绝不能是生产路径 - if _, err := os.Stat("/etc/llmsproxy/plugins/.channels-store.json"); err == nil { - if filepath.Clean(dir) == "/etc/llmsproxy/plugins" { + if _, err := os.Stat("/etc/modelrouter/plugins/.channels-store.json"); err == nil { + if filepath.Clean(dir) == "/etc/modelrouter/plugins" { t.Fatal("测试正在使用生产存储目录 —— 隔离失效(这会造成真实数据事故)") } } diff --git a/internal/core/channels_store_test.go b/internal/core/channels_store_test.go index 057f20a..758ba0b 100644 --- a/internal/core/channels_store_test.go +++ b/internal/core/channels_store_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // TestChannelsStoreIsPluginOwned 是「插件自持持久化」的回归。 @@ -23,7 +23,7 @@ func TestChannelsStoreIsPluginOwned(t *testing.T) { // 固定插件目录:两次「重启」必须共用同一目录,否则测不出持久化。 // 这也是防止测试写进生产插件目录的那道隔离(kernelTestCore 会 respect 它)。 dir := t.TempDir() - t.Setenv("LLMSPROXY_PLUGIN_DIR", dir) + t.Setenv("MODELROUTER_PLUGIN_DIR", dir) load := func() *lua.Plugins { c := kernelTestCore(t) ps := c.Plugins() @@ -128,6 +128,10 @@ func TestChannelsMigratesLegacyKernelState(t *testing.T) { c := kernelTestCore(t) dir = c.Config().PluginDir + // 网关不再自动创建 plugin_dir(插件不由内核安装),测试自己建。 + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } legacyPath := filepath.Join(dir, ".channels.state.json") if err := os.WriteFile(legacyPath, b, 0o600); err != nil { t.Fatal(err) @@ -179,7 +183,7 @@ func TestChannelsMigrationKeepsLegacyOnWriteFailure(t *testing.T) { // (/.channels-store.json.tmp)**占成一个目录**,io.open(tmp,"w") 必失败。 // 不用 chmod:测试以 root 跑,root 绕过权限位,chmod 拦不住。 sub := t.TempDir() - t.Setenv("LLMSPROXY_PLUGIN_DIR", sub) + t.Setenv("MODELROUTER_PLUGIN_DIR", sub) legacy := map[string]interface{}{ "version": 1, diff --git a/internal/core/channels_stress_test.go b/internal/core/channels_stress_test.go index 7f9f9eb..68b06ee 100644 --- a/internal/core/channels_stress_test.go +++ b/internal/core/channels_stress_test.go @@ -11,8 +11,8 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" + "modelrouter/internal/config" + "modelrouter/internal/lua" ) // mustBox 构造一个真 SecretBox(与运行期同一实现,密钥落 runtime dir)。 diff --git a/internal/core/channels_trae_catalog_test.go b/internal/core/channels_trae_catalog_test.go index 915668a..0e78e7c 100644 --- a/internal/core/channels_trae_catalog_test.go +++ b/internal/core/channels_trae_catalog_test.go @@ -9,7 +9,7 @@ import ( "sync" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ TRAE 的 batch 模型目录解析:两个**真实缺陷**必须在移植时一起带上 diff --git a/internal/core/channels_trae_chat_headers_test.go b/internal/core/channels_trae_chat_headers_test.go index 28620f4..b47405c 100644 --- a/internal/core/channels_trae_chat_headers_test.go +++ b/internal/core/channels_trae_chat_headers_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ TRAE 的**推理请求**必须用 Cloud-IDE-JWT 头族,不是 Bearer。 diff --git a/internal/core/channels_trae_checkin_test.go b/internal/core/channels_trae_checkin_test.go index 4bf1a1d..1e3ecd9 100644 --- a/internal/core/channels_trae_checkin_test.go +++ b/internal/core/channels_trae_checkin_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // Trae 签到是最容易被"看起来对"骗过的地方:请求能发出去、上游可能也返回 200, diff --git a/internal/core/channels_trae_derive_test.go b/internal/core/channels_trae_derive_test.go index e8527e6..585ba23 100644 --- a/internal/core/channels_trae_derive_test.go +++ b/internal/core/channels_trae_derive_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 手工移植的确定性派生必须与参考**逐字节一致**,否则签名的设备身份是错的 diff --git a/internal/core/channels_trae_exchange_shape_test.go b/internal/core/channels_trae_exchange_shape_test.go index 52cbc01..19104cc 100644 --- a/internal/core/channels_trae_exchange_shape_test.go +++ b/internal/core/channels_trae_exchange_shape_test.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ TRAE ExchangeToken 的凭据在 **Result 里**,不在顶层。 diff --git a/internal/core/channels_trae_pkce_test.go b/internal/core/channels_trae_pkce_test.go index 2c5bba6..5585e84 100644 --- a/internal/core/channels_trae_pkce_test.go +++ b/internal/core/channels_trae_pkce_test.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ TRAE 回调只带 code(PKCE 形态)时必须**明确报出**「暂不支持 PKCE」, diff --git a/internal/core/channels_trae_test.go b/internal/core/channels_trae_test.go index 24a323f..4f29fe4 100644 --- a/internal/core/channels_trae_test.go +++ b/internal/core/channels_trae_test.go @@ -10,7 +10,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // traeHarness:TRAE callback 流的测试台。假上游模拟 ExchangeToken; diff --git a/internal/core/channels_vnc_test.go b/internal/core/channels_vnc_test.go index 137b6cf..aec1ca4 100644 --- a/internal/core/channels_vnc_test.go +++ b/internal/core/channels_vnc_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // TestChannelsVncStackEndToEnd 验证 VNC 兜底链路在**真插件**上跑通: diff --git a/internal/core/channels_xdisplay_test.go b/internal/core/channels_xdisplay_test.go index 6eac7ef..6be4427 100644 --- a/internal/core/channels_xdisplay_test.go +++ b/internal/core/channels_xdisplay_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // 显示号泄漏是「测试单独跑绿、全量跑红」的根源,值得一条判据钉住它。 @@ -34,7 +34,7 @@ func TestXDisplaySelectionReclaimsStaleLocks(t *testing.T) { // (见 kernelTestCore);硬编码 90 段会让本判据造出**根本不会被扫描到**的 // 陈旧锁,于是"没复用"这个断言永远失败 —— 那是判据与被测对象脱节。 base := 300 - if v := os.Getenv("LLMSPROXY_XDISPLAY_BASE"); v != "" { + if v := os.Getenv("MODELROUTER_XDISPLAY_BASE"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { base = n } diff --git a/internal/core/channels_zcode_identity_test.go b/internal/core/channels_zcode_identity_test.go index a0aad61..b56a135 100644 --- a/internal/core/channels_zcode_identity_test.go +++ b/internal/core/channels_zcode_identity_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // ★ ZCode 的身份块是 3012 准入的**唯一开关**。 @@ -269,7 +269,7 @@ func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) { if err := os.MkdirAll(pluginDir, 0755); err != nil { t.Fatal(err) } - t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir) + t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir) c := kernelTestCore(t) ps := c.Plugins() diff --git a/internal/core/channels_zcode_test.go b/internal/core/channels_zcode_test.go index d973a34..1851e94 100644 --- a/internal/core/channels_zcode_test.go +++ b/internal/core/channels_zcode_test.go @@ -10,7 +10,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // zcodeHarness:ZCode 两段式服务端中介授权流的测试台。 diff --git a/internal/core/core_test.go b/internal/core/core_test.go index 9e8226c..0413b5d 100644 --- a/internal/core/core_test.go +++ b/internal/core/core_test.go @@ -5,7 +5,7 @@ import ( "sync" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // newTestConfig returns an in-memory config backed by temp files so diff --git a/internal/core/device_harness_test.go b/internal/core/device_harness_test.go index 0bed988..2447235 100644 --- a/internal/core/device_harness_test.go +++ b/internal/core/device_harness_test.go @@ -8,7 +8,7 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。 diff --git a/internal/core/kernel_actions.go b/internal/core/kernel_actions.go index ffe2722..2df666b 100644 --- a/internal/core/kernel_actions.go +++ b/internal/core/kernel_actions.go @@ -6,8 +6,8 @@ import ( "fmt" "strings" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" + "modelrouter/internal/config" + "modelrouter/internal/lua" ) // 内核动作实现(R4-c) diff --git a/internal/core/kernel_actions_test.go b/internal/core/kernel_actions_test.go index baff645..b016c19 100644 --- a/internal/core/kernel_actions_test.go +++ b/internal/core/kernel_actions_test.go @@ -6,8 +6,8 @@ import ( "path/filepath" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" + "modelrouter/internal/config" + "modelrouter/internal/lua" ) // 本文件验证 R4-c/R4-d 的闭环:**插件自己驱动外部源的链接认证,然后把源接进内核**。 @@ -22,12 +22,12 @@ func kernelTestCore(t *testing.T) *Core { dir := t.TempDir() pluginDir := filepath.Join(dir, "plugins") // 插件的自有存储位置由插件自己决定(读 config.yaml 的 plugin_dir,或 - // LLMSPROXY_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到 - // /etc/llmsproxy/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试 + // MODELROUTER_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到 + // /etc/modelrouter/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试 // 把 26 个测试账号写进了生产 .channels-store.json)。 // 调用方已显式设定时尊重它:多次「重启」要共用同一目录才能测持久化。 - if os.Getenv("LLMSPROXY_PLUGIN_DIR") == "" { - t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir) + if os.Getenv("MODELROUTER_PLUGIN_DIR") == "" { + t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir) } // ★ 显示器号与端口用**高段号**,避开生产。 // @@ -36,8 +36,8 @@ func kernelTestCore(t *testing.T) *Core { // 并去抢,结果 `Xvfb 未就绪(/tmp/.X11-unix/X91 未出现)`,而生产上正跑着 // 一套浏览器栈。判据本身没错,错的是**两个 /tmp 互不可见**而号段重叠。 // 故测试整体挪到 300+。 - if os.Getenv("LLMSPROXY_XDISPLAY_BASE") == "" { - t.Setenv("LLMSPROXY_XDISPLAY_BASE", "300") + if os.Getenv("MODELROUTER_XDISPLAY_BASE") == "" { + t.Setenv("MODELROUTER_XDISPLAY_BASE", "300") } cfg := &config.Config{ Path: filepath.Join(dir, "config.yaml"), diff --git a/internal/core/kernel_no_deadlock_test.go b/internal/core/kernel_no_deadlock_test.go index d521f02..ed800df 100644 --- a/internal/core/kernel_no_deadlock_test.go +++ b/internal/core/kernel_no_deadlock_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // TestNoKernelActionDeadlocksFromLua 对**每个**内核动作做同一条断言: diff --git a/internal/core/key_auto_concurrency_test.go b/internal/core/key_auto_concurrency_test.go index 06780e8..9870346 100644 --- a/internal/core/key_auto_concurrency_test.go +++ b/internal/core/key_auto_concurrency_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // AutoChainFor sits on the request hot path and reads keyAutoChains with no diff --git a/internal/core/key_auto_test.go b/internal/core/key_auto_test.go index 283bef3..0c7d1f6 100644 --- a/internal/core/key_auto_test.go +++ b/internal/core/key_auto_test.go @@ -4,8 +4,8 @@ import ( "path/filepath" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/scheduler" + "modelrouter/internal/config" + "modelrouter/internal/scheduler" ) // Per-key AUTO chains let an admin give one user their own scheduling chain diff --git a/internal/core/source_hosts_test.go b/internal/core/source_hosts_test.go index 90beb5a..58edd80 100644 --- a/internal/core/source_hosts_test.go +++ b/internal/core/source_hosts_test.go @@ -4,7 +4,7 @@ import ( "net/url" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // The plugin outbound allowlist is built from the hosts of configured sources. diff --git a/internal/core/startup_secrets_test.go b/internal/core/startup_secrets_test.go index f6317b1..337d638 100644 --- a/internal/core/startup_secrets_test.go +++ b/internal/core/startup_secrets_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // Startup must be idempotent, and every credential consumer must see PLAINTEXT. diff --git a/internal/gateway/api.go b/internal/gateway/api.go index 4dc75f0..9606eec 100644 --- a/internal/gateway/api.go +++ b/internal/gateway/api.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) type adapterPayload struct { @@ -488,7 +488,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) { if to == 0 { to = time.Now().UnixMilli() } - csvHeaders(w, "llmsproxy-requests.csv") + csvHeaders(w, "modelrouter-requests.csv") cw := csv.NewWriter(w) names := map[string]string{} for _, k := range g.core.ListKeys() { @@ -537,7 +537,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) { return } if r.URL.Query().Get("export") == "keys-csv" { - csvHeaders(w, "llmsproxy-keys.csv") + csvHeaders(w, "modelrouter-keys.csv") cw := csv.NewWriter(w) _ = cw.Write([]string{"key", "key_name", "role", "models", "total_requests", "success_requests", "failed_requests", "prompt_tokens", "completion_tokens", "total_tokens", "avg_latency_ms", "max_latency_ms", "created_at"}) keyFilter := exportKey(r) diff --git a/internal/gateway/apiv1.go b/internal/gateway/apiv1.go index 0c0bc90..ae53ad3 100644 --- a/internal/gateway/apiv1.go +++ b/internal/gateway/apiv1.go @@ -31,7 +31,7 @@ import ( "sort" "strings" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // apiV1Routes dispatches /api/v1/*. Kept separate from routes() so the original @@ -144,7 +144,7 @@ func (g *Gateway) apiV1Index(w http.ResponseWriter, r *http.Request) { } index := map[string]interface{}{ "api_version": "v1", - "description": "llmsproxy management API. Authenticate with a gateway key: " + + "description": "modelrouter management API. Authenticate with a gateway key: " + "'Authorization: Bearer ', '?api_key=', or the gw_key cookie. " + "Read endpoints accept any role; mutations require an admin key.", "discovery": []ep{ diff --git a/internal/gateway/apiv1_test.go b/internal/gateway/apiv1_test.go index c688ef5..e8a755d 100644 --- a/internal/gateway/apiv1_test.go +++ b/internal/gateway/apiv1_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // v1Gateway builds a gateway with one source and two keys, and returns the diff --git a/internal/gateway/chat.go b/internal/gateway/chat.go index d848911..e510187 100644 --- a/internal/gateway/chat.go +++ b/internal/gateway/chat.go @@ -13,11 +13,11 @@ import ( "sync/atomic" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" - "llmsproxy/internal/provider" - "llmsproxy/internal/scheduler" - "llmsproxy/internal/types" + "modelrouter/internal/config" + "modelrouter/internal/lua" + "modelrouter/internal/provider" + "modelrouter/internal/scheduler" + "modelrouter/internal/types" ) // chatRequest mirrors the OpenAI chat completions request the gateway accepts. diff --git a/internal/gateway/chat_cache_test.go b/internal/gateway/chat_cache_test.go index edc42c0..0d87015 100644 --- a/internal/gateway/chat_cache_test.go +++ b/internal/gateway/chat_cache_test.go @@ -3,7 +3,7 @@ package gateway import ( "testing" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // TestMergeUsageKeepsZeroCacheDetails locks the streaming counterpart of the diff --git a/internal/gateway/chat_reasoning_test.go b/internal/gateway/chat_reasoning_test.go index bc2ed4b..ab047de 100644 --- a/internal/gateway/chat_reasoning_test.go +++ b/internal/gateway/chat_reasoning_test.go @@ -3,7 +3,7 @@ package gateway import ( "testing" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // TestRecordChatUsageKeepsReasoningTokens pins the reasoning-token plumbing. diff --git a/internal/gateway/deploy_script_test.go b/internal/gateway/deploy_script_test.go index b3be2ea..4e87f94 100644 --- a/internal/gateway/deploy_script_test.go +++ b/internal/gateway/deploy_script_test.go @@ -11,7 +11,7 @@ import ( // deploy.sh is the only release-critical script with no automated coverage, and // its newest part — prune_adapter_backups — deletes directories under -// /etc/llmsproxy. This exercises the function in a throwaway directory with the +// /etc/modelrouter. This exercises the function in a throwaway directory with the // REAL backup-name shape. // // The shape matters and got this wrong twice: the guard is @@ -45,7 +45,7 @@ func TestDeployPruneAdapterBackups(t *testing.T) { // Redirect ONLY the base path. The logic under test must stay verbatim. body = strings.Replace(body, - `local base="/etc/llmsproxy"`, + `local base="/etc/modelrouter"`, `local base="`+base+`"`, 1) // KEEP_BACKUPS must come from deploy.sh itself, NOT from a literal // here: hardcoding 5 in the shim overrode whatever the script configured, so @@ -207,7 +207,7 @@ func TestDeployBacksUpTheLiveFileBeforeOverwriting(t *testing.T) { // // 事实:backup 清理最初只处理 `adapters.bak.*`,而 `sync_plugins` 同样每次部署 // 建一个 `plugins.bak.<时间戳>` —— 于是 adapters 的堆积被修好,plugins 继续涨。 -// 实测线上 plugins.bak 积累到 **36 个**、/etc/llmsproxy 涨到 192M。 +// 实测线上 plugins.bak 积累到 **36 个**、/etc/modelrouter 涨到 192M。 // // 这是「两处只修一处」的老形状。判据钉住:prune_all_backups 必须同时裁两个前缀, // 且新增任何 .bak.<时间戳> 前缀时都要在这里补上(否则又是一个泄漏点)。 @@ -264,7 +264,7 @@ func TestDeployPruneCoversBothBackupPrefixes(t *testing.T) { k := strings.Index(src, "prune_backups()") kj := strings.Index(src[k:], "\n}\n") fnBody := src[k : k+kj+3] - fnBody = strings.Replace(fnBody, `local base="/etc/llmsproxy"`, `local base="`+base+`"`, 1) + fnBody = strings.Replace(fnBody, `local base="/etc/modelrouter"`, `local base="`+base+`"`, 1) keepRe := regexp.MustCompile(`(?m)^KEEP_BACKUPS=(\d+)`) km := keepRe.FindStringSubmatch(src) if km == nil { diff --git a/internal/gateway/discover.go b/internal/gateway/discover.go index 1b7a6e9..b3a6e0c 100644 --- a/internal/gateway/discover.go +++ b/internal/gateway/discover.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // Model discovery: turn "here is a URL and a key" into a configured source. diff --git a/internal/gateway/discover_api.go b/internal/gateway/discover_api.go index 27f7e12..9d2f316 100644 --- a/internal/gateway/discover_api.go +++ b/internal/gateway/discover_api.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "io" - "llmsproxy/internal/config" + "modelrouter/internal/config" "net/http" "strings" "time" diff --git a/internal/gateway/discover_api_test.go b/internal/gateway/discover_api_test.go index 71d6f03..474283d 100644 --- a/internal/gateway/discover_api_test.go +++ b/internal/gateway/discover_api_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // upstream that answers /v1/models with the real captured catalog diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go index 8a146f8..d11eaff 100644 --- a/internal/gateway/gateway_test.go +++ b/internal/gateway/gateway_test.go @@ -14,8 +14,8 @@ import ( "testing" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) func mockUpstream() *httptest.Server { diff --git a/internal/gateway/key_auto_api_test.go b/internal/gateway/key_auto_api_test.go index 714822f..c631bf6 100644 --- a/internal/gateway/key_auto_api_test.go +++ b/internal/gateway/key_auto_api_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // Per-key AUTO chain admin API. The behaviours pinned here are the ones that diff --git a/internal/gateway/key_quota_api_test.go b/internal/gateway/key_quota_api_test.go index f5e1517..ffc881a 100644 --- a/internal/gateway/key_quota_api_test.go +++ b/internal/gateway/key_quota_api_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // adminGateway builds a gateway whose admin key can call /api/keys. diff --git a/internal/gateway/key_quota_test.go b/internal/gateway/key_quota_test.go index bf80f55..bfd8699 100644 --- a/internal/gateway/key_quota_test.go +++ b/internal/gateway/key_quota_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // ---- data layer: per-key isolation ---- diff --git a/internal/gateway/key_quota_wiring_test.go b/internal/gateway/key_quota_wiring_test.go index 24ea3eb..a600d23 100644 --- a/internal/gateway/key_quota_wiring_test.go +++ b/internal/gateway/key_quota_wiring_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // quotaGateway builds a gateway with two user keys that differ only in their diff --git a/internal/gateway/keys.go b/internal/gateway/keys.go index 587f421..54e1030 100644 --- a/internal/gateway/keys.go +++ b/internal/gateway/keys.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - "llmsproxy/internal/config" - "llmsproxy/internal/scheduler" + "modelrouter/internal/config" + "modelrouter/internal/scheduler" ) // handleKeysAPI manages gateway keys: GET /api/keys (admin: all keys), diff --git a/internal/gateway/messages.go b/internal/gateway/messages.go index 770341d..47401c6 100644 --- a/internal/gateway/messages.go +++ b/internal/gateway/messages.go @@ -10,9 +10,9 @@ import ( "strings" "time" - "llmsproxy/internal/provider" - "llmsproxy/internal/scheduler" - "llmsproxy/internal/types" + "modelrouter/internal/provider" + "modelrouter/internal/scheduler" + "modelrouter/internal/types" ) // handleMessages serves the Anthropic Messages protocol (POST /v1/messages). diff --git a/internal/gateway/messages_test.go b/internal/gateway/messages_test.go index 4ce1410..45b60da 100644 --- a/internal/gateway/messages_test.go +++ b/internal/gateway/messages_test.go @@ -11,8 +11,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // 本文件端到端验证 Phase 2 的核心主张:**对外协议由适配器决定,内核只调度**。 diff --git a/internal/gateway/mount.go b/internal/gateway/mount.go index d7d9635..2d57006 100644 --- a/internal/gateway/mount.go +++ b/internal/gateway/mount.go @@ -21,7 +21,7 @@ import ( "sort" "strings" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // allowedMountPrefixes are the only namespaces a plugin/adapters may claim. diff --git a/internal/gateway/mount_test.go b/internal/gateway/mount_test.go index dcded1b..8ce7fef 100644 --- a/internal/gateway/mount_test.go +++ b/internal/gateway/mount_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // 本文件锁定 R3 的两条不变量: diff --git a/internal/gateway/overflow_err_test.go b/internal/gateway/overflow_err_test.go index 632719a..b7512f8 100644 --- a/internal/gateway/overflow_err_test.go +++ b/internal/gateway/overflow_err_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" - "llmsproxy/internal/scheduler" - "llmsproxy/internal/types" + "modelrouter/internal/scheduler" + "modelrouter/internal/types" ) // piOverflowRe 是 @earendil-works/pi-ai 的 OVERFLOW_PATTERNS 中对应的一条。 diff --git a/internal/gateway/plugin_call_api_test.go b/internal/gateway/plugin_call_api_test.go index 7f4afa1..6622918 100644 --- a/internal/gateway/plugin_call_api_test.go +++ b/internal/gateway/plugin_call_api_test.go @@ -17,8 +17,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // callPlugin exposes exactly one export plus a function that is NOT exported. diff --git a/internal/gateway/plugin_http_routes.go b/internal/gateway/plugin_http_routes.go index d4114cf..8a7051c 100644 --- a/internal/gateway/plugin_http_routes.go +++ b/internal/gateway/plugin_http_routes.go @@ -21,7 +21,7 @@ import ( "strconv" "strings" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) // pluginHTTPPrefix is the mount point. It is deliberately narrow: a plugin's diff --git a/internal/gateway/plugin_wiring_test.go b/internal/gateway/plugin_wiring_test.go index 794606f..c8d62e4 100644 --- a/internal/gateway/plugin_wiring_test.go +++ b/internal/gateway/plugin_wiring_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // newFailingServer serves handler and returns its test server. diff --git a/internal/gateway/plugins_api.go b/internal/gateway/plugins_api.go index 6ea786c..890113f 100644 --- a/internal/gateway/plugins_api.go +++ b/internal/gateway/plugins_api.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - "llmsproxy/internal/lua" + "modelrouter/internal/lua" ) var ( diff --git a/internal/gateway/plugins_api_test.go b/internal/gateway/plugins_api_test.go index 8c4e69f..3223702 100644 --- a/internal/gateway/plugins_api_test.go +++ b/internal/gateway/plugins_api_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/core" - "llmsproxy/internal/lua" + "modelrouter/internal/config" + "modelrouter/internal/core" + "modelrouter/internal/lua" ) // gatewayWithBilling boots a gateway with the bundled billing plugin loaded, so diff --git a/internal/gateway/serializer.go b/internal/gateway/serializer.go index c36212f..4786625 100644 --- a/internal/gateway/serializer.go +++ b/internal/gateway/serializer.go @@ -29,9 +29,9 @@ import ( "sync/atomic" "time" - "llmsproxy/internal/lua" - "llmsproxy/internal/provider" - "llmsproxy/internal/types" + "modelrouter/internal/lua" + "modelrouter/internal/provider" + "modelrouter/internal/types" ) // StreamState 是一个流会话在序列化侧的累积状态(角色前导是否已发、已见 diff --git a/internal/gateway/serializer_test.go b/internal/gateway/serializer_test.go index ca49326..9701ef8 100644 --- a/internal/gateway/serializer_test.go +++ b/internal/gateway/serializer_test.go @@ -8,8 +8,8 @@ import ( "sync" "testing" - "llmsproxy/internal/lua" - "llmsproxy/internal/types" + "modelrouter/internal/lua" + "modelrouter/internal/types" ) // 本文件锁定对外序列化的**优先级契约**: diff --git a/internal/gateway/server.go b/internal/gateway/server.go index e70051b..b8fcb37 100644 --- a/internal/gateway/server.go +++ b/internal/gateway/server.go @@ -18,8 +18,8 @@ import ( "sync" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) //go:embed ui/* diff --git a/internal/gateway/source_fields_test.go b/internal/gateway/source_fields_test.go index c28b96a..aacb39a 100644 --- a/internal/gateway/source_fields_test.go +++ b/internal/gateway/source_fields_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/core" + "modelrouter/internal/config" + "modelrouter/internal/core" ) // This file covers the second round of the same class of defect bf0657b fixed. diff --git a/internal/gateway/sources_api_test.go b/internal/gateway/sources_api_test.go index c6d6695..537e7f5 100644 --- a/internal/gateway/sources_api_test.go +++ b/internal/gateway/sources_api_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // These cover the two write-path defects found while adding a model to a live diff --git a/internal/gateway/stats_unknown_key_test.go b/internal/gateway/stats_unknown_key_test.go index 62eff24..2bef6f1 100644 --- a/internal/gateway/stats_unknown_key_test.go +++ b/internal/gateway/stats_unknown_key_test.go @@ -22,7 +22,7 @@ import ( // /api/stats?period=day&limit=1&key=sk-gw-7ffe12c5cbacec40bd39a7291df9a9ed // → curl: (52) Empty reply from server // → log: http: panic serving …: invalid memory address or nil pointer dereference -// llmsproxy/internal/gateway.rows stats.go:771 +// modelrouter/internal/gateway.rows stats.go:771 // // A key with no recorded traffic is a legitimate question, and zero is its // answer — not a crash. diff --git a/internal/gateway/thinking_test.go b/internal/gateway/thinking_test.go index eb42dd1..8b9d3b9 100644 --- a/internal/gateway/thinking_test.go +++ b/internal/gateway/thinking_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // TestThinkingFromClient 把「客户端措辞 → 中立语义」这条翻译链钉死。 diff --git a/internal/gateway/token_units_test.go b/internal/gateway/token_units_test.go index b4274fe..4239219 100644 --- a/internal/gateway/token_units_test.go +++ b/internal/gateway/token_units_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // Token accounting must record TOKENS, not byte estimates and not image diff --git a/internal/gateway/ui_image_page_test.go b/internal/gateway/ui_image_page_test.go index 99e2d07..d356b0b 100644 --- a/internal/gateway/ui_image_page_test.go +++ b/internal/gateway/ui_image_page_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "llmsproxy/internal/provider" + "modelrouter/internal/provider" ) // TestUI_ChatPageGroupsImageModels is a UI-contract guard for the bug that diff --git a/internal/provider/gemini_endpoint_test.go b/internal/provider/gemini_endpoint_test.go index 2eb78ae..d22cccc 100644 --- a/internal/provider/gemini_endpoint_test.go +++ b/internal/provider/gemini_endpoint_test.go @@ -12,9 +12,9 @@ import ( "sync" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" - "llmsproxy/internal/types" + "modelrouter/internal/config" + "modelrouter/internal/lua" + "modelrouter/internal/types" ) // Gemini is the only adapter whose endpoint carries the model in the PATH diff --git a/internal/provider/model_kind_test.go b/internal/provider/model_kind_test.go index b271077..3c7cbbd 100644 --- a/internal/provider/model_kind_test.go +++ b/internal/provider/model_kind_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "llmsproxy/internal/config" + "modelrouter/internal/config" ) // TestSourceStatus_CarriesModelKind is the fix for a UI bug where the chat diff --git a/internal/provider/provider.go b/internal/provider/provider.go index cb2b5fc..faef2d8 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -20,9 +20,9 @@ import ( "sync/atomic" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" - "llmsproxy/internal/types" + "modelrouter/internal/config" + "modelrouter/internal/lua" + "modelrouter/internal/types" ) // proxyFor picks the transport proxy for one source. An explicit per-source diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index a9a4595..ddf455d 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -14,9 +14,9 @@ import ( "testing" "time" - "llmsproxy/internal/config" - "llmsproxy/internal/lua" - "llmsproxy/internal/types" + "modelrouter/internal/config" + "modelrouter/internal/lua" + "modelrouter/internal/types" ) func newTestProvider(t *testing.T, src config.Source) *Provider { diff --git a/internal/provider/retry_policy_test.go b/internal/provider/retry_policy_test.go index 9326fff..6da3abb 100644 --- a/internal/provider/retry_policy_test.go +++ b/internal/provider/retry_policy_test.go @@ -8,8 +8,8 @@ import ( "sync/atomic" "testing" - "llmsproxy/internal/config" - "llmsproxy/internal/types" + "modelrouter/internal/config" + "modelrouter/internal/types" ) // 上游重试是**内核的通用机制**,判据纯数据(status + body 子串), diff --git a/internal/scheduler/empty_response_test.go b/internal/scheduler/empty_response_test.go index 8fc90c9..00d0a52 100644 --- a/internal/scheduler/empty_response_test.go +++ b/internal/scheduler/empty_response_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // TestResultIsEmptyMatchesTheReasoningOnlyResponse guards the production bug diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 3f467ce..1154189 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -15,7 +15,7 @@ import ( "sync/atomic" "time" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // busyWait is how long a fully-busy tier is polled for a free slot before the diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go index 96811fc..b4ea212 100644 --- a/internal/scheduler/scheduler_test.go +++ b/internal/scheduler/scheduler_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "llmsproxy/internal/types" + "modelrouter/internal/types" ) // fakeProvider is an in-memory Provider used to exercise chain scheduling diff --git a/packaging/config.example.yaml b/packaging/config.example.yaml index ad9441b..99e992f 100644 --- a/packaging/config.example.yaml +++ b/packaging/config.example.yaml @@ -1,4 +1,4 @@ -# llmsproxy — 统一 OpenAI 兼容网关配置(示例模板) +# modelrouter — 统一 OpenAI 兼容网关配置(示例模板) # # 这个是打包进发行包、供首次安装使用的模板。它故意不含任何可用的凭据: # 首次启动时程序会生成一个随机 admin key 并写入实际配置(WebUI「密钥」页可见)。 @@ -20,14 +20,10 @@ default_model: AUTO # Lua 适配器目录(默认 adapters/,首次启动自动写入内置适配器) adapter_dir: adapters -# 插件目录(可选)。设置后: -# - 首次启动会把随核心发布的示例插件(billing:按源/模型/密钥计费 + 仪表盘) -# 写入本目录,它会在请求流水线上挂钩子,并向 WebUI 注入一个「Billing」页面 -# 和状态页上的一块总开销组件; -# - 之后 WebUI「插件」页与 Electron 壳的设置面板可安装/禁用/删除/编辑。 -# 目录一旦存在即以目录为准:删除或改写内置插件都是真实生效的操作。 -# 留空 = 插件功能完全关闭(不影响网关其它功能)。 -# plugin_dir: /etc/llmsproxy/plugins +# Plugin directory (optional). Plugins are independent Lua files managed by the +# operator. Release packages do not include or install plugins automatically. +# Place desired .lua files in this directory before starting the gateway. +# plugin_dir: /etc/modelrouter/plugins # 运行时文件:存放 WebUI 管理的源模板、已删除标记、预置模板名单。 # diff --git a/packaging/core-dist.sh b/packaging/core-dist.sh index 5b4e26d..005197c 100755 --- a/packaging/core-dist.sh +++ b/packaging/core-dist.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash -# Build the llmsproxy CORE distribution packages (deb + rpm + tar.gz). +# Build the modelrouter CORE distribution packages (deb + rpm + tar.gz). # # Unlike the GUI packages (Electron), the core is a single static-ish binary # that server admins install with dpkg/rpm. The systemd unit bakes in the # memory tuning (GOGC=50, MALLOC_ARENA_MAX=2) measured on the production box. # # Requires: go, nfpm (PATH), dpkg-deb/rpmbuild for verification -# Outputs: cmd/build/dist/llmsproxy__amd64.deb -# cmd/build/dist/llmsproxy-.x86_64.rpm -# cmd/build/dist/llmsproxy--linux-amd64.tar.gz +# Outputs: cmd/build/dist/modelrouter__amd64.deb +# cmd/build/dist/modelrouter-.x86_64.rpm +# cmd/build/dist/modelrouter--linux-amd64.tar.gz set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -16,7 +16,7 @@ cd "$ROOT" VERSION="${1:-$(git describe --tags --dirty 2>/dev/null | sed 's/^v//' || echo 0.1.0)}" DIST="$ROOT/cmd/build/dist" -BUILD_BIN="$ROOT/cmd/build/llmsproxy" +BUILD_BIN="$ROOT/cmd/build/modelrouter" log() { printf '\033[1;34m[core-dist]\033[0m %s\n' "$*"; } @@ -35,7 +35,7 @@ mkdir -p "$DIST" # 1) build the core binary (luajit tag, -s -w like deploy.sh) log "building core binary (luajit, stripped)..." -CGO_ENABLED=1 go build -tags luajit -trimpath -ldflags="-s -w" -o "$BUILD_BIN" ./cmd/llmsproxy +CGO_ENABLED=1 go build -tags luajit -trimpath -ldflags="-s -w" -o "$BUILD_BIN" ./cmd/modelrouter # 2) sanity gates: binary must contain the expected symbols and not be degenerate. # NOTE under `set -o pipefail`, `strings | grep -q` is BROKEN: grep -q exits @@ -65,41 +65,34 @@ rm -f "$NFPM_CFG" # 4) tar.gz log "packaging tar.gz..." -TAR_DIR="$DIST/llmsproxy-$VERSION-linux-amd64" +TAR_DIR="$DIST/modelrouter-$VERSION-linux-amd64" rm -rf "$TAR_DIR"; mkdir -p "$TAR_DIR" -cp "$BUILD_BIN" "$TAR_DIR/llmsproxy" +cp "$BUILD_BIN" "$TAR_DIR/modelrouter" # Use the tracked sanitised template, NOT the local config.yaml: that file is # gitignored and carries the operator's real gateway_keys / upstream keys. It # was previously copied verbatim as config.example.yaml, which shipped a real, # working admin key (sk-gw-local-0001) to every install — and postinst.sh copies # the example to /etc when none exists, so it ended up live in production. cp "$ROOT/packaging/config.example.yaml" "$TAR_DIR/config.example.yaml" -cp "$ROOT/packaging/llmsproxy.service" "$TAR_DIR/llmsproxy.service" +cp "$ROOT/packaging/modelrouter.service" "$TAR_DIR/modelrouter.service" +# 改名迁移脚本随包分发:升级自 llmsproxy 时代的机器需要它(dry-run 默认)。 +cp "$ROOT/packaging/migrate-to-modelrouter.sh" "$TAR_DIR/migrate-to-modelrouter.sh" mkdir -p "$TAR_DIR/adapters" cp "$ROOT"/internal/lua/adapters/*.lua "$TAR_DIR/adapters/" -# The shipped plugins go into the archive too, not just into the binary's -# embedded FS. -# -# Seeding works without them (internal/lua/vm.go embeds plugins/*.lua and -# writeBundledPlugins materialises them on first start — verified: a fresh -# plugin_dir gains billing.lua, 70031 bytes, and the plugin loads), so this is -# not a functional fix. It is a contents/claim mismatch: the v1.8.0 release -# notes tell the operator the billing plugin ships with the core, and this -# archive — the artefact they unpack and inspect — did not contain it. Shipping -# the file also means an operator can read and adapt it before installing, -# instead of only discovering it after the first boot writes it out. -mkdir -p "$TAR_DIR/plugins" -cp "$ROOT"/internal/lua/plugins/*.lua "$TAR_DIR/plugins/" +# The core distribution contains only the gateway and protocol adapters. +# Optional plugins remain in the source repository, but are neither embedded in +# the binary nor copied into release archives. Operators install selected Lua +# plugins into plugin_dir separately. -tar -C "$DIST" -czf "$DIST/llmsproxy-$VERSION-linux-amd64.tar.gz" \ +tar -C "$DIST" -czf "$DIST/modelrouter-$VERSION-linux-amd64.tar.gz" \ "$(basename "$TAR_DIR")" rm -rf "$TAR_DIR" # 5) verify artifacts are healthy (the same gate that would have caught the # 264KB exe fiasco) log "verifying packages..." -for f in "$DIST"/llmsproxy_*.deb "$DIST"/llmsproxy-*.rpm "$DIST"/*.tar.gz; do +for f in "$DIST"/modelrouter_*.deb "$DIST"/modelrouter-*.rpm "$DIST"/*.tar.gz; do [ -f "$f" ] || continue sz=$(stat -c%s "$f") [ "$sz" -gt 100000 ] || { echo "[core-dist] FATAL: $f suspiciously small ($sz B)"; exit 1; } @@ -111,7 +104,7 @@ done # `set -e` that aborted the script right here, so the "done" line never printed # and NO content verification below ever ran. The size gate above was the only # check that ever executed. Pick the newest deb explicitly. -newest_deb=$(ls -t "$DIST"/llmsproxy_*.deb 2>/dev/null | head -1 || true) +newest_deb=$(ls -t "$DIST"/modelrouter_*.deb 2>/dev/null | head -1 || true) if [ -n "$newest_deb" ]; then dpkg-deb -I "$newest_deb" 2>/dev/null | grep -E 'Package|Version' | head -2 || true fi @@ -123,22 +116,24 @@ fi # behaviour hides it at runtime — the operator sees a working billing plugin and # concludes the package is complete. Assert the file is in the archive. log "verifying archive contents..." -TARBALL="$DIST/llmsproxy-$VERSION-linux-amd64.tar.gz" +TARBALL="$DIST/modelrouter-$VERSION-linux-amd64.tar.gz" [ -f "$TARBALL" ] || { echo "[core-dist] FATAL: $TARBALL missing"; exit 1; } listing=$(tar tzf "$TARBALL" 2>/dev/null || true) [ -n "$listing" ] || { echo "[core-dist] FATAL: cannot list $TARBALL"; exit 1; } -for want in llmsproxy config.example.yaml llmsproxy.service plugins/billing.lua; do +for want in modelrouter config.example.yaml modelrouter.service migrate-to-modelrouter.sh; do if ! printf '%s\n' "$listing" | grep -q "/$want\$"; then echo "[core-dist] FATAL: archive is missing $want" - echo " (release notes claim the billing plugin ships with the core)" + echo " (the archive must carry the binary, config template, unit and migration script)" exit 1 fi log " OK archive contains $want" done -# Directories are matched by their CONTENTS, not by name: tar written with -# `tar -czf` from a populated tree lists "…/adapters/openai.lua" and never a bare -# "…/adapters" entry, so an earlier version of this check that grepped for -# "/adapters$" reported a complete archive as broken. +n_plugins=$(printf '%s\n' "$listing" | grep -c "/plugins/.*\.lua\$" || true) +if [ "$n_plugins" -ne 0 ]; then + echo "[core-dist] FATAL: archive unexpectedly contains $n_plugins plugins" + exit 1 +fi +log " OK archive excludes optional plugins" n_adapters=$(printf '%s\n' "$listing" | grep -c "/adapters/.*\.lua\$" || true) if [ "$n_adapters" -lt 10 ]; then echo "[core-dist] FATAL: archive has only $n_adapters adapters (expected >= 10)" diff --git a/packaging/install-browser-stack.sh b/packaging/install-browser-stack.sh index 601b0f5..e89e967 100755 --- a/packaging/install-browser-stack.sh +++ b/packaging/install-browser-stack.sh @@ -115,7 +115,7 @@ python3 -c 'import websocket' >/dev/null 2>&1 && echo " OK python3 websocket-cl if [ "$ok" = 1 ]; then echo - echo "OK 浏览器栈就绪。重启 llmsproxy 后,flow=vnc 的平台即可用 VNC 兜底登录。" + echo "OK 浏览器栈就绪。重启 modelrouter 后,flow=vnc 的平台即可用 VNC 兜底登录。" else echo echo "WARN: 仍有缺失项,VNC 兜底在该机器上不可用。插件的 deps/status 端点会一样报告。" >&2 diff --git a/packaging/migrate-to-modelrouter.sh b/packaging/migrate-to-modelrouter.sh new file mode 100755 index 0000000..6fb0b13 --- /dev/null +++ b/packaging/migrate-to-modelrouter.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# migrate-to-modelrouter.sh — 把 llmsproxy 时代的部署迁到 modelrouter 命名。 +# +# 背景:产品名早已是 ModelRouter,但二进制/配置目录/服务名/插件目录仍沿用 +# 仓库旧名 llmsproxy。这次改名不留"半新半旧"的机器:脚本复制(不是移动) +# 旧目录到新路径、改写 systemd unit、安装新二进制,并在最后做一遍校验。 +# +# 安全约定: +# * **默认 dry-run**,只打印将要做什么;加 --apply 才真正执行。 +# * 旧目录**永不删除**,迁移结束仍可 `--rollback` 回到旧布局。 +# * 已存在的新路径不会被静默覆盖:先备份成 .bak-<时间戳>,再写入。 +# * 不自动重启正在运行的服务,除非显式 --restart。 +# +# 用法: +# sudo ./packaging/migrate-to-modelrouter.sh # 预演 +# sudo ./packaging/migrate-to-modelrouter.sh --apply # 执行 +# sudo ./packaging/migrate-to-modelrouter.sh --apply --restart +# sudo ./packaging/migrate-to-modelrouter.sh --rollback --apply +set -euo pipefail + +OLD_NAME="llmsproxy" +NEW_NAME="modelrouter" +OLD_ETC="/etc/${OLD_NAME}" +NEW_ETC="/etc/${NEW_NAME}" +OLD_BIN="/usr/local/bin/${OLD_NAME}" +NEW_BIN="/usr/local/bin/${NEW_NAME}" +OLD_UNIT="/etc/systemd/system/${OLD_NAME}.service" +NEW_UNIT="/etc/systemd/system/${NEW_NAME}.service" +OLD_SHARE="/usr/share/${OLD_NAME}" +NEW_SHARE="/usr/share/${NEW_NAME}" + +APPLY=0 +RESTART=0 +ROLLBACK=0 +for arg in "$@"; do + case "$arg" in + --apply) APPLY=1 ;; + --restart) RESTART=1 ;; + --rollback) ROLLBACK=1 ;; + -h|--help) sed -n '2,30p' "$0"; exit 0 ;; + *) echo "unknown argument: $arg" >&2; exit 2 ;; + esac +done + +ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } +skip() { printf ' \033[90m·\033[0m %s\n' "$*"; } +plan() { printf ' \033[33m→\033[0m %s\n' "$*"; } +die() { printf '\033[31mfail\033[0m %s\n' "$*" >&2; exit 1; } + +run() { + if [ "$APPLY" -eq 1 ]; then + "$@" + else + printf ' [dry-run] %s\n' "$*" + fi +} + +backup_if_exists() { + local p="$1" + if [ -e "$p" ]; then + local b="${p}.bak-$(date +%Y%m%d%H%M%S)" + plan "备份已存在的 $p → $b" + run cp -a "$p" "$b" + fi +} + +stamp() { date +%Y%m%d%H%M%S; } + +if [ "$(id -u)" -ne 0 ] && [ "$APPLY" -eq 1 ]; then + die "--apply 需要 root(会写 /etc 与 /usr/local/bin)" +fi + +if [ "$ROLLBACK" -eq 1 ]; then + echo "== 回滚到 ${OLD_NAME} 布局 ==" + [ -d "$NEW_ETC" ] || die "$NEW_ETC 不存在,没有可回滚的内容" + if [ -e "$OLD_ETC" ]; then + plan "$OLD_ETC 已存在,保留它(回滚包在 ${NEW_ETC} 原地)" + plan "如需覆盖请先手工比对:diff -r $OLD_ETC $NEW_ETC" + else + plan "复制 $NEW_ETC → $OLD_ETC(不删除新目录)" + run cp -a "$NEW_ETC" "$OLD_ETC" + fi + if [ -e "$NEW_UNIT" ]; then + plan "恢复 $OLD_UNIT" + run cp "$NEW_UNIT" "$OLD_UNIT" + run sed -i "s#${NEW_NAME}#${OLD_NAME}#g" "$OLD_UNIT" + fi + if [ "$RESTART" -eq 1 ]; then + run systemctl daemon-reload + run systemctl enable --now "${OLD_NAME}.service" + fi + echo + echo "回滚$( [ "$APPLY" -eq 1 ] && echo '完成' || echo '预演完成(未改动任何文件)' )" + exit 0 +fi + +echo "== 迁移 ${OLD_NAME} → ${NEW_NAME}$( [ "$APPLY" -eq 1 ] && echo '' || echo '(dry-run,加 --apply 执行)' ) ==" +echo + +# ---- 1. 状态目录 ------------------------------------------------------- +echo "[1/5] 配置与状态目录" +if [ -d "$OLD_ETC" ]; then + if [ -d "$NEW_ETC" ]; then + ok "$NEW_ETC 已存在,保持不动(旧目录仍保留在 $OLD_ETC)" + plan "合并前请人工比对:diff -r $OLD_ETC $NEW_ETC" + else + plan "复制 $OLD_ETC → $NEW_ETC(保留原目录,可回滚)" + run cp -a "$OLD_ETC" "$NEW_ETC" + fi +else + skip "$OLD_ETC 不存在,跳过(新安装无需迁移)" +fi + +# 配置内的绝对路径也要跟着改;只改这些键,避免误伤注释里的示例。 +if [ -d "$NEW_ETC" ] && [ -f "$NEW_ETC/config.yaml" ]; then + if grep -q "$OLD_ETC" "$NEW_ETC/config.yaml"; then + plan "把 $NEW_ETC/config.yaml 中的 $OLD_ETC 改为 $NEW_ETC" + backup_if_exists "$NEW_ETC/config.yaml" + run sed -i "s#${OLD_ETC}#${NEW_ETC}#g" "$NEW_ETC/config.yaml" + else + ok "config.yaml 内无旧路径" + fi + # master.key 的权限是安全边界,迁移后复核一次。 + if [ -f "$NEW_ETC/master.key" ]; then + plan "复核 master.key 权限为 0600" + run chmod 0600 "$NEW_ETC/master.key" + fi +fi + +# ---- 2. 二进制 --------------------------------------------------------- +echo "[2/5] 二进制" +if [ -x "$NEW_BIN" ]; then + ok "$NEW_BIN 已存在(安装包已就位)" +elif [ -x "$OLD_BIN" ]; then + plan "复制 $OLD_BIN → $NEW_BIN 并保留旧文件" + run cp -a "$OLD_BIN" "$NEW_BIN" + run chmod 0755 "$NEW_BIN" +else + plan "尚无二进制:从发行包或 'make build' 得到 $NEW_BIN 后重跑本脚本" +fi + +# ---- 3. systemd ------------------------------------------------------ +echo "[3/5] systemd unit" +if [ -f "$NEW_UNIT" ]; then + ok "$NEW_UNIT 已存在" +elif [ -f "$OLD_UNIT" ]; then + plan "生成 $NEW_UNIT(改写 ExecStart/WorkingDirectory/ReadWritePaths)" + if [ "$APPLY" -eq 1 ]; then + sed "s#/usr/local/bin/${OLD_NAME}#${NEW_BIN}#g; s#${OLD_ETC}#${NEW_ETC}#g" \ + "$OLD_UNIT" > "$NEW_UNIT" + chmod 0644 "$NEW_UNIT" + else + printf ' [dry-run] sed ... %s > %s\n' "$OLD_UNIT" "$NEW_UNIT" + fi + plan "旧 unit 保留:$OLD_UNIT(确认新服务可用后再 disable)" +else + skip "没有 systemd unit,跳过" +fi + +# ---- 4. 分享目录(安装脚本/示例配置)---------------------------------- +echo "[4/5] /usr/share 资源" +if [ -d "$OLD_SHARE" ] && [ ! -d "$NEW_SHARE" ]; then + plan "复制 $OLD_SHARE → $NEW_SHARE" + run cp -a "$OLD_SHARE" "$NEW_SHARE" +else + skip "$OLD_SHARE → $NEW_SHARE 无需处理" +fi + +# ---- 5. 校验 --------------------------------------------------------- +echo "[5/5] 校验" +if [ "$APPLY" -eq 1 ]; then + [ -x "$NEW_BIN" ] || die "$NEW_BIN 不可执行" + ok "$NEW_BIN 可执行" + if [ -f "$NEW_ETC/config.yaml" ]; then + "$NEW_BIN" -config "$NEW_ETC/config.yaml" -check >/dev/null || die "配置校验失败" + ok "配置校验通过($NEW_ETC/config.yaml)" + fi + if [ -f "$NEW_UNIT" ]; then + systemctl daemon-reload + ok "systemd 已 reload" + fi + if [ "$RESTART" -eq 1 ] && [ -f "$NEW_UNIT" ]; then + systemctl enable --now "${NEW_NAME}.service" + ok "${NEW_NAME}.service 已启动" + else + skip "未重启服务(需要时加 --restart)" + fi + + echo + echo "迁移完成。旧布局原样保留,回滚:" + echo " sudo $0 --rollback --apply" + echo + echo "确认无误后再手工清理(本脚本不会删):" + echo " systemctl disable --now ${OLD_NAME}.service 2>/dev/null || true" + echo " rm -f $OLD_UNIT" + echo " rm -rf $OLD_BIN $OLD_ETC $OLD_SHARE" +else + echo + echo "以上为预演。确认后执行:" + echo " sudo $0 --apply" +fi diff --git a/packaging/llmsproxy.service b/packaging/modelrouter.service similarity index 88% rename from packaging/llmsproxy.service rename to packaging/modelrouter.service index 93c757f..d781fe0 100644 --- a/packaging/llmsproxy.service +++ b/packaging/modelrouter.service @@ -1,5 +1,5 @@ [Unit] -Description=LLMSProxy - unified OpenAI-compatible multi-source LLM gateway +Description=ModelRouter - unified OpenAI-compatible multi-source LLM gateway After=network.target [Service] @@ -15,19 +15,19 @@ Type=simple # the extra GC cycles are free. Environment=GOGC=50 Environment=MALLOC_ARENA_MAX=2 -ExecStart=/usr/local/bin/llmsproxy -config /etc/llmsproxy/config.yaml -WorkingDirectory=/etc/llmsproxy +ExecStart=/usr/local/bin/modelrouter -config /etc/modelrouter/config.yaml +WorkingDirectory=/etc/modelrouter Restart=always RestartSec=5 # ---- 加固(2026-10-01 逐条实测后加入,不是照抄文档)---- # -# 为什么仍然以 root 运行:master.key 是 0600 root。加 User=llmsproxy 实测直接 +# 为什么仍然以 root 运行:master.key 是 0600 root。加 User=modelrouter 实测直接 # 起不来,而且失败方式很隐蔽—— -# [config] secrets disabled: open /etc/llmsproxy/master.key: permission denied +# [config] secrets disabled: open /etc/modelrouter/master.key: permission denied # 只是**一行日志**,服务会带着"敏感值将以明文落盘"继续跑起来。 # 也就是说降权在当前文件权限下不是加固,而是把密钥降级。要降权必须先把 -# master.key 交给服务用户并统一 /etc/llmsproxy 的属主,那是一次独立的、有回滚 +# master.key 交给服务用户并统一 /etc/modelrouter 的属主,那是一次独立的、有回滚 # 需求的变更,不该和加固混在一起。 # # 下面每一条都在一个独立探针单元(临时端口 + 独立 runtime_file/adapter_dir) @@ -36,11 +36,11 @@ RestartSec=5 # kill -9 后行为符合预期。systemd 对非法指令值不报错只"忽略",逐条实测是唯一 # 可靠做法。 NoNewPrivileges=yes -# 读路径全部落在 /etc/llmsproxy;写路径经核对只有 config.yaml / runtime.json / +# 读路径全部落在 /etc/modelrouter;写路径经核对只有 config.yaml / runtime.json / # audit.jsonl / adapters/*.lua / master.key,全在该目录下(internal/{config,gateway, # core,lua} 里的 WriteFile|Rename|Remove 调用点)。 ProtectSystem=strict -ReadWritePaths=/etc/llmsproxy +ReadWritePaths=/etc/modelrouter ProtectHome=yes PrivateTmp=yes ProtectKernelTunables=yes @@ -80,7 +80,7 @@ CapabilityBoundingSet= # 仍然保留的加固(见下):NoNewPrivileges / ProtectSystem=strict / # ReadWritePaths / ProtectHome / PrivateTmp / CapabilityBoundingSet=(空)/ # ProtectKernel* / RestrictSUIDSGID / RestrictRealtime / LockPersonality。 -# 这些**都不影响**浏览器栈(已逐条实测),并把可写面收窄到 /etc/llmsproxy。 +# 这些**都不影响**浏览器栈(已逐条实测),并把可写面收窄到 /etc/modelrouter。 [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml index 86af09b..6953004 100644 --- a/packaging/nfpm.yaml +++ b/packaging/nfpm.yaml @@ -1,8 +1,8 @@ -# nfpm config for the llmsproxy core gateway binary. +# nfpm config for the modelrouter core gateway binary. # The version placeholder below is substituted by packaging/core-dist.sh (nfpm v2 # here does not honour {{ .Env.VERSION }} template rendering), so do not edit it # by hand. -name: llmsproxy +name: modelrouter arch: amd64 platform: linux version: __VERSION__ @@ -19,36 +19,36 @@ homepage: https://gitcode.com/JianFeeeee/ModelRouter license: MIT contents: # binary - - src: ./cmd/build/llmsproxy - dst: /usr/bin/llmsproxy + - src: ./cmd/build/modelrouter + dst: /usr/bin/modelrouter file_info: mode: 0755 # systemd unit (memory tuning baked in) - - src: ./packaging/llmsproxy.service - dst: /etc/systemd/system/llmsproxy.service + - src: ./packaging/modelrouter.service + dst: /etc/systemd/system/modelrouter.service file_info: mode: 0644 - # example config (seeded to /etc/llmsproxy on first install by postinst). + # example config (seeded to /etc/modelrouter on first install by postinst). # Must be the tracked sanitised template: ./config.yaml is the operator's own # gitignored config and shipping it would leak real credentials — it shipped a # working admin key (sk-gw-local-0001) to every install for a while. - src: ./packaging/config.example.yaml - dst: /usr/share/llmsproxy/config.example.yaml + dst: /usr/share/modelrouter/config.example.yaml file_info: mode: 0644 - # built-in Lua adapters (deployed to /etc/llmsproxy/adapters) + # built-in Lua adapters (deployed to /etc/modelrouter/adapters) - src: ./internal/lua/adapters/*.lua - dst: /usr/share/llmsproxy/adapters/ + dst: /usr/share/modelrouter/adapters/ file_info: mode: 0644 # 服务端浏览器栈依赖安装脚本(VNC 兜底登录用;需 root,运维显式执行) - src: ./packaging/install-browser-stack.sh - dst: /usr/share/llmsproxy/install-browser-stack.sh + dst: /usr/share/modelrouter/install-browser-stack.sh file_info: mode: 0755 # README pointer - src: ./README.md - dst: /usr/share/doc/llmsproxy/README.md + dst: /usr/share/doc/modelrouter/README.md file_info: mode: 0644 diff --git a/packaging/postinst.sh b/packaging/postinst.sh index 63768d3..b452eb5 100755 --- a/packaging/postinst.sh +++ b/packaging/postinst.sh @@ -1,13 +1,13 @@ #!/bin/sh -# llmsproxy core package postinst +# modelrouter core package postinst set -e -# Runtime state belongs under /etc/llmsproxy just like the production layout: +# Runtime state belongs under /etc/modelrouter just like the production layout: # runtime.json holds keys/created sources and is NOT touched on reinstall. -mkdir -p /etc/llmsproxy/adapters +mkdir -p /etc/modelrouter/adapters -if [ -f /etc/llmsproxy/config.yaml ]; then - echo "llmsproxy: keeping existing /etc/llmsproxy/config.yaml" +if [ -f /etc/modelrouter/config.yaml ]; then + echo "modelrouter: keeping existing /etc/modelrouter/config.yaml" else # Do NOT seed a starter config here. Packaging/installing one used to hand # everyone the same file, and while it was copied from the maintainer's @@ -21,12 +21,12 @@ else # # (Seeding it with gateway_keys: [] would be worse: the gateway would start # but reject every request, with no way in except editing the file.) - echo "llmsproxy: no /etc/llmsproxy/config.yaml — a random admin key will be generated on first start" + echo "modelrouter: no /etc/modelrouter/config.yaml — a random admin key will be generated on first start" fi # systemd if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true - echo "llmsproxy: use 'systemctl enable --now llmsproxy' to start" + echo "modelrouter: use 'systemctl enable --now modelrouter' to start" fi exit 0 \ No newline at end of file diff --git a/packaging/prerm.sh b/packaging/prerm.sh index 5183f47..d0ded1b 100755 --- a/packaging/prerm.sh +++ b/packaging/prerm.sh @@ -1,13 +1,13 @@ #!/bin/sh -# llmsproxy core package prerm +# modelrouter core package prerm set -e if command -v systemctl >/dev/null 2>&1; then # stop the service if this package's unit is the one installed - if systemctl list-unit-files llmsproxy.service >/dev/null 2>&1 && \ - systemctl is-active llmsproxy.service >/dev/null 2>&1; then - systemctl stop llmsproxy.service >/dev/null 2>&1 || true - systemctl disable llmsproxy.service >/dev/null 2>&1 || true + if systemctl list-unit-files modelrouter.service >/dev/null 2>&1 && \ + systemctl is-active modelrouter.service >/dev/null 2>&1; then + systemctl stop modelrouter.service >/dev/null 2>&1 || true + systemctl disable modelrouter.service >/dev/null 2>&1 || true fi systemctl daemon-reload >/dev/null 2>&1 || true fi