refactor(rename)!: llmsproxy → modelrouter(module / 二进制 / 路径 / env)

产品名早已是 ModelRouter,但仓库标识符仍沿用旧名。这次统一,读写两侧都做兼容,
已部署的机器不会因为顺序问题立刻失效。

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

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

本提交未包含:`adapters/opencodezen.lua`、`adapter_opencodezen_test.go`、
`adapter_opencodezen_live_test.go`(OpenCode UA/免费池的既有改动,另批提交)。
This commit is contained in:
JianFeeeee
2026-10-26 22:28:08 +08:00
parent de7553eaa2
commit 688dce88d1
107 changed files with 548 additions and 308 deletions

View File

@ -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) -----

View File

@ -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) {

View File

@ -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",

View File

@ -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",

View File

@ -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")
}

View File

@ -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 <file>]
--config <file> 投放这份配置到 /etc/llmsproxy/config.yaml(原子替换)。
--config <file> 投放这份配置到 /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() {
# ---------- 备份目录保留策略 ----------
# 每次部署都新建一个 <prefix>.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/替换之前:它的失败后果最重(配置里的密文永久解不开),
# 必须在动任何文件之前就挡住。此阶段用的是**当前线上二进制**,它已经

View File

@ -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。两次同一类错误。

View File

@ -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)

2
go.mod
View File

@ -1,4 +1,4 @@
module llmsproxy
module modelrouter
go 1.25.0

View File

@ -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

View File

@ -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 <runtime-dir>/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 <runtime-dir>/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)
}

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 三个 callback 平台的回调路径**各是注册值**,且校验规则不同:

View File

@ -3,7 +3,7 @@ package core
import (
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 无凭据账号的批量清理:**只删没有凭据的**,有凭据的一律不动。

View File

@ -8,7 +8,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ codearts 的模型目录必须从**两个**签名端点拉取并合并。

View File

@ -8,7 +8,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// CodeArts 端到端:起真实回调服务器 → 模上游回 code → 走真实 STS 交换

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// CodeArts 的 portal 拒绝 <10000 的回调端口(参考 listenOnCallbackPort 的

View File

@ -7,7 +7,7 @@ import (
"net/http/httptest"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 有道每日活动领取是**三步流程**,参考注释点名:无可用活动是**正常业务状态**,

View File

@ -2,7 +2,7 @@ package core
import (
"encoding/json"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
"net/http"
"net/http/httptest"
"testing"

View File

@ -10,7 +10,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 有道登录闭环的判据:portal 会把授权重定向回 **本机 loopback 接收器**

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// loomyHarness:Loomy 微信扫码长轮询的测试台。

View File

@ -8,7 +8,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 远端模型目录的判据:**远端为主 + 兜底补位**(照参考 reconcileWithFallback)。

View File

@ -9,7 +9,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// channelsMoreTest:新引入的三条真实流程 + 两个边界流的行为对齐测试。

View File

@ -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{

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestChannelsPluginEndToEnd 是「同类插件」的完整验收:

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 「为什么没有可用账号」必须区分**真的用尽**与**查不到**。

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
"fmt"
"net/http"

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// qoderReason 调真实的 transform_error 钩子(不自己重算)。

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// Qoder 活动领取有三条参考用**抓包 + 用户报障**定位过的「会误报」陷阱,

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// qoderHarness:Qoder PKCE 设备码轮的测试台。

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// raccoonHarness:Raccoon 内置二维码扫码登录的测试台。

View File

@ -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("没扫描到任何内核调用(判据失效)")

View File

@ -3,7 +3,7 @@ package core
import (
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ account_reorder 必须**真的改变内核里的源顺序**。

View File

@ -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("测试正在使用生产存储目录 —— 隔离失效(这会造成真实数据事故)")
}
}

View File

@ -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) {
// (<dir>/.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,

View File

@ -11,8 +11,8 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/lua"
"modelrouter/internal/config"
"modelrouter/internal/lua"
)
// mustBox 构造一个真 SecretBox(与运行期同一实现,密钥落 runtime dir)。

View File

@ -9,7 +9,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 的 batch 模型目录解析:两个**真实缺陷**必须在移植时一起带上

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 的**推理请求**必须用 Cloud-IDE-JWT 头族,不是 Bearer。

View File

@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// Trae 签到是最容易被"看起来对"骗过的地方:请求能发出去、上游可能也返回 200,

View File

@ -6,7 +6,7 @@ import (
"os"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 手工移植的确定性派生必须与参考**逐字节一致**,否则签名的设备身份是错的

View File

@ -6,7 +6,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE ExchangeToken 的凭据在 **Result 里**,不在顶层。

View File

@ -9,7 +9,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 回调只带 code(PKCE 形态)时必须**明确报出**「暂不支持 PKCE」,

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// traeHarness:TRAE callback 流的测试台。假上游模拟 ExchangeToken;

View File

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestChannelsVncStackEndToEnd 验证 VNC 兜底链路在**真插件**上跑通:

View File

@ -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
}

View File

@ -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()

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// zcodeHarness:ZCode 两段式服务端中介授权流的测试台。

View File

@ -5,7 +5,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// newTestConfig returns an in-memory config backed by temp files so

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。

View File

@ -6,8 +6,8 @@ import (
"fmt"
"strings"
"llmsproxy/internal/config"
"llmsproxy/internal/lua"
"modelrouter/internal/config"
"modelrouter/internal/lua"
)
// 内核动作实现(R4-c)

View File

@ -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"),

View File

@ -4,7 +4,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestNoKernelActionDeadlocksFromLua 对**每个**内核动作做同一条断言:

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// Startup must be idempotent, and every credential consumer must see PLAINTEXT.

View File

@ -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)

View File

@ -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 <key>', '?api_key=<key>', or the gw_key cookie. " +
"Read endpoints accept any role; mutations require an admin key.",
"discovery": []ep{

View File

@ -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

View File

@ -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.

View File

@ -3,7 +3,7 @@ package gateway
import (
"testing"
"llmsproxy/internal/types"
"modelrouter/internal/types"
)
// TestMergeUsageKeepsZeroCacheDetails locks the streaming counterpart of the

View File

@ -3,7 +3,7 @@ package gateway
import (
"testing"
"llmsproxy/internal/types"
"modelrouter/internal/types"
)
// TestRecordChatUsageKeepsReasoningTokens pins the reasoning-token plumbing.

View File

@ -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 {

View File

@ -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.

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"io"
"llmsproxy/internal/config"
"modelrouter/internal/config"
"net/http"
"strings"
"time"

View File

@ -9,7 +9,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// upstream that answers /v1/models with the real captured catalog

View File

@ -14,8 +14,8 @@ import (
"testing"
"time"
"llmsproxy/internal/config"
"llmsproxy/internal/core"
"modelrouter/internal/config"
"modelrouter/internal/core"
)
func mockUpstream() *httptest.Server {

View File

@ -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

View File

@ -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.

View File

@ -4,7 +4,7 @@ import (
"testing"
"time"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// ---- data layer: per-key isolation ----

View File

@ -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

View File

@ -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),

View File

@ -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).

View File

@ -11,8 +11,8 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/core"
"modelrouter/internal/config"
"modelrouter/internal/core"
)
// 本文件端到端验证 Phase 2 的核心主张:**对外协议由适配器决定,内核只调度**。

View File

@ -21,7 +21,7 @@ import (
"sort"
"strings"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// allowedMountPrefixes are the only namespaces a plugin/adapters may claim.

View File

@ -10,8 +10,8 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/core"
"modelrouter/internal/config"
"modelrouter/internal/core"
)
// 本文件锁定 R3 的两条不变量:

View File

@ -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 中对应的一条。

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
var (

View File

@ -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

View File

@ -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 是一个流会话在序列化侧的累积状态(角色前导是否已发、已见

View File

@ -8,8 +8,8 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"llmsproxy/internal/types"
"modelrouter/internal/lua"
"modelrouter/internal/types"
)
// 本文件锁定对外序列化的**优先级契约**:

View File

@ -18,8 +18,8 @@ import (
"sync"
"time"
"llmsproxy/internal/config"
"llmsproxy/internal/core"
"modelrouter/internal/config"
"modelrouter/internal/core"
)
//go:embed ui/*

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"llmsproxy/internal/types"
"modelrouter/internal/types"
)
// TestThinkingFromClient 把「客户端措辞 → 中立语义」这条翻译链钉死。

View File

@ -8,7 +8,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// Token accounting must record TOKENS, not byte estimates and not image

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/provider"
"modelrouter/internal/provider"
)
// TestUI_ChatPageGroupsImageModels is a UI-contract guard for the bug that

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -8,8 +8,8 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/types"
"modelrouter/internal/config"
"modelrouter/internal/types"
)
// 上游重试是**内核的通用机制**,判据纯数据(status + body 子串),

View File

@ -7,7 +7,7 @@ import (
"testing"
"time"
"llmsproxy/internal/types"
"modelrouter/internal/types"
)
// TestResultIsEmptyMatchesTheReasoningOnlyResponse guards the production bug

View File

@ -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

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/types"
"modelrouter/internal/types"
)
// fakeProvider is an in-memory Provider used to exercise chain scheduling

View File

@ -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 管理的源模板、已删除标记、预置模板名单。
#

Some files were not shown because too many files have changed in this diff Show More