mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
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:
4
Makefile
4
Makefile
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core)
|
# ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core)
|
||||||
export BINARY
|
export BINARY
|
||||||
BINARY=llmsproxy
|
BINARY=modelrouter
|
||||||
export BUILD_DIR
|
export BUILD_DIR
|
||||||
BUILD_DIR=build
|
BUILD_DIR=build
|
||||||
export GUI_DIR
|
export GUI_DIR
|
||||||
@ -20,7 +20,7 @@ all: build
|
|||||||
# ---- core binary (server users / embedded core) ----
|
# ---- core binary (server users / embedded core) ----
|
||||||
build:
|
build:
|
||||||
@mkdir -p $(BUILD_DIR)
|
@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))"
|
@echo "Built: $(BUILD_DIR)/$(BINARY) ($(VERSION))"
|
||||||
|
|
||||||
# ---- core distribution packages (deb + rpm + tar.gz) -----
|
# ---- core distribution packages (deb + rpm + tar.gz) -----
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// ModelRouter Desktop GUI — main process
|
// 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,
|
// 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.
|
// supports launch-at-login. Server-side users keep using the pure Go binary.
|
||||||
const {
|
const {
|
||||||
@ -50,11 +50,27 @@ const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml");
|
|||||||
const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins");
|
const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins");
|
||||||
// Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable).
|
// Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable).
|
||||||
// Dev: cmd/gui/bin, populated by prepare-core.js.
|
// 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,
|
app.isPackaged ? process.resourcesPath : __dirname,
|
||||||
"bin",
|
"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",
|
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
|
// Packaged window icon: must be a REAL file on disk (extraResource), not an
|
||||||
// asar path — Electron/GTK pulls BrowserWindow icons from the filesystem and
|
// asar path — Electron/GTK pulls BrowserWindow icons from the filesystem and
|
||||||
// crashes (SIGSEGV) when given an asar virtual path on Linux.
|
// crashes (SIGSEGV) when given an asar virtual path on Linux.
|
||||||
@ -163,7 +179,7 @@ function unsealViaCore() {
|
|||||||
let key = "";
|
let key = "";
|
||||||
try {
|
try {
|
||||||
const out = execFileSync(
|
const out = execFileSync(
|
||||||
CORE_EXE,
|
resolveCoreExe(),
|
||||||
["-config", CONFIG_FILE, "-show-secrets"],
|
["-config", CONFIG_FILE, "-show-secrets"],
|
||||||
{
|
{
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
@ -215,7 +231,7 @@ function coreStarted() {
|
|||||||
function writeDefaultEmbeddedConfig() {
|
function writeDefaultEmbeddedConfig() {
|
||||||
// Generate a self-contained embedded profile with a fresh random admin key.
|
// Generate a self-contained embedded profile with a fresh random admin key.
|
||||||
// No upstream sources are preconfigured — the desktop user adds them via
|
// 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 a config already exists we do NOT overwrite it (keep user's edits).
|
||||||
if (fs.existsSync(CONFIG_FILE)) return;
|
if (fs.existsSync(CONFIG_FILE)) return;
|
||||||
const key = "sk-gw-" + crypto.randomBytes(16).toString("hex");
|
const key = "sk-gw-" + crypto.randomBytes(16).toString("hex");
|
||||||
@ -272,8 +288,9 @@ function startCore() {
|
|||||||
console.log("core already running");
|
console.log("core already running");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(CORE_EXE)) {
|
const coreExe = resolveCoreExe();
|
||||||
console.error("embedded core binary missing:", CORE_EXE);
|
if (!fs.existsSync(coreExe)) {
|
||||||
|
console.error("embedded core binary missing:", coreExe);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ensure profile dir + config + local adapters exist
|
// ensure profile dir + config + local adapters exist
|
||||||
@ -281,9 +298,8 @@ function startCore() {
|
|||||||
if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig();
|
if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig();
|
||||||
pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log);
|
pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log);
|
||||||
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
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,
|
cwd: PROFILE_DIR,
|
||||||
env: Object.assign({}, process.env, {
|
env: Object.assign({}, process.env, {
|
||||||
LLMS_PROXY_GUI: "1",
|
LLMS_PROXY_GUI: "1",
|
||||||
@ -686,7 +702,7 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Silent start: `--silent` CLI flag or settings.silentStart keeps the app
|
// 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.
|
// always shows the window so the user sees the dashboard.
|
||||||
mainWindow.once("ready-to-show", () => {
|
mainWindow.once("ready-to-show", () => {
|
||||||
const firstRun = !fs.existsSync(
|
const firstRun = !fs.existsSync(
|
||||||
@ -810,8 +826,8 @@ ipcMain.handle("core:state", () => ({
|
|||||||
port: settings.port || DEFAULT_PORT,
|
port: settings.port || DEFAULT_PORT,
|
||||||
profileDir: PROFILE_DIR,
|
profileDir: PROFILE_DIR,
|
||||||
configFile: CONFIG_FILE,
|
configFile: CONFIG_FILE,
|
||||||
coreExe: CORE_EXE,
|
coreExe: resolveCoreExe(),
|
||||||
coreExists: fs.existsSync(CORE_EXE),
|
coreExists: fs.existsSync(resolveCoreExe()),
|
||||||
}));
|
}));
|
||||||
ipcMain.handle("core:start", () => {
|
ipcMain.handle("core:start", () => {
|
||||||
app.isQuitting = false;
|
app.isQuitting = false;
|
||||||
@ -883,7 +899,7 @@ if (gotLock) {
|
|||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
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 () => {
|
setTimeout(async () => {
|
||||||
const p = await probeEmbedded();
|
const p = await probeEmbedded();
|
||||||
if (!p.ok) {
|
if (!p.ok) {
|
||||||
|
|||||||
@ -39,8 +39,8 @@
|
|||||||
},
|
},
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "bin/llmsproxy",
|
"from": "bin/modelrouter",
|
||||||
"to": "bin/llmsproxy"
|
"to": "bin/modelrouter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build/icon.png",
|
"from": "build/icon.png",
|
||||||
@ -66,8 +66,8 @@
|
|||||||
"icon": "build/icon.ico",
|
"icon": "build/icon.ico",
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "bin/llmsproxy.exe",
|
"from": "bin/modelrouter.exe",
|
||||||
"to": "bin/llmsproxy.exe"
|
"to": "bin/modelrouter.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "bin/lua51.dll",
|
"from": "bin/lua51.dll",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/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 (native, luajit) + win (cross, best effort)
|
||||||
// node prepare-core.js linux -> linux only
|
// node prepare-core.js linux -> linux only
|
||||||
// node prepare-core.js win -> win only
|
// node prepare-core.js win -> win only
|
||||||
@ -12,21 +12,21 @@ const binDir = path.join(guiDir, "bin");
|
|||||||
const projectRoot = path.join(guiDir, "..", "..");
|
const projectRoot = path.join(guiDir, "..", "..");
|
||||||
|
|
||||||
function buildLinux() {
|
function buildLinux() {
|
||||||
const out = path.join(binDir, "llmsproxy");
|
const out = path.join(binDir, "modelrouter");
|
||||||
console.log("[prepare-core] building llmsproxy (linux/luajit)...");
|
console.log("[prepare-core] building modelrouter (linux/luajit)...");
|
||||||
fs.mkdirSync(binDir, { recursive: true });
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
execFileSync(
|
execFileSync(
|
||||||
"go",
|
"go",
|
||||||
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/llmsproxy"],
|
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"],
|
||||||
{ cwd: projectRoot, stdio: "inherit" },
|
{ cwd: projectRoot, stdio: "inherit" },
|
||||||
);
|
);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWin() {
|
function buildWin() {
|
||||||
const out = path.join(binDir, "llmsproxy.exe");
|
const out = path.join(binDir, "modelrouter.exe");
|
||||||
console.log(
|
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 });
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
const cc = process.env.MINGW_CC || "x86_64-w64-mingw32-gcc";
|
const cc = process.env.MINGW_CC || "x86_64-w64-mingw32-gcc";
|
||||||
@ -43,7 +43,7 @@ function buildWin() {
|
|||||||
}
|
}
|
||||||
execFileSync(
|
execFileSync(
|
||||||
"go",
|
"go",
|
||||||
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/llmsproxy"],
|
["build", "-tags", "luajit", "-trimpath", "-o", out, "./cmd/modelrouter"],
|
||||||
{
|
{
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|||||||
@ -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
|
// sources behind a unified OpenAI-compatible HTTP API. Protocol differences are
|
||||||
// handled by Lua adapters (per source), optionally signing outgoing requests.
|
// handled by Lua adapters (per source), optionally signing outgoing requests.
|
||||||
// It supports explicit model selection or AUTO routing by priority, image
|
// It supports explicit model selection or AUTO routing by priority, image
|
||||||
@ -16,9 +16,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
"llmsproxy/internal/gateway"
|
"modelrouter/internal/gateway"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -33,12 +33,12 @@ func main() {
|
|||||||
// error here instead of being silently created.
|
// error here instead of being silently created.
|
||||||
if *checkOnly {
|
if *checkOnly {
|
||||||
if _, err := os.Stat(*cfgPath); err != nil {
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,34 +48,34 @@ func main() {
|
|||||||
// program prints a credential, and this path never logs to a file.
|
// program prints a credential, and this path never logs to a file.
|
||||||
if *showSecrets {
|
if *showSecrets {
|
||||||
if _, err := os.Stat(*cfgPath); err != nil {
|
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 {
|
if err := config.PrintSecrets(*cfgPath); err != nil {
|
||||||
log.Fatalf("[llmsproxy] show-secrets: %v", err)
|
log.Fatalf("[modelrouter] show-secrets: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := config.EnsureDefault(*cfgPath)
|
created, err := config.EnsureDefault(*cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[llmsproxy] config: %v", err)
|
log.Fatalf("[modelrouter] config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := core.New(*cfgPath)
|
c, err := core.New(*cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[llmsproxy] core: %v", err)
|
log.Fatalf("[modelrouter] core: %v", err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
if created {
|
if created {
|
||||||
for _, k := range c.GatewayKeys() {
|
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)
|
gw, err := gateway.New(c)
|
||||||
if err != nil {
|
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
|
// 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
|
// (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.
|
// rejected" while seven working keys, one of them admin, were in service.
|
||||||
if keys := c.ListKeys(); len(keys) == 0 {
|
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 {
|
} else {
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if k.Key == "sk-gw-local-0001" || k.Key == "sk-local-0001" {
|
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, "::") {
|
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{
|
srv := &http.Server{
|
||||||
@ -108,22 +108,22 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
cert, key := c.TLS()
|
cert, key := c.TLS()
|
||||||
if cert != "" && key != "" {
|
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()))
|
c.Listen(), cert, c.DefaultModel(), c.Registry().ModelList(), len(c.ListAdapters()))
|
||||||
if err := srv.ListenAndServeTLS(cert, key); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServeTLS(cert, key); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("[llmsproxy] server: %v", err)
|
log.Fatalf("[modelrouter] server: %v", err)
|
||||||
}
|
}
|
||||||
return
|
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()))
|
c.Listen(), c.DefaultModel(), c.Registry().ModelList(), len(c.ListAdapters()))
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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)
|
stop := make(chan os.Signal, 1)
|
||||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-stop
|
<-stop
|
||||||
log.Printf("[llmsproxy] shutting down")
|
log.Printf("[modelrouter] shutting down")
|
||||||
}
|
}
|
||||||
73
deploy.sh
73
deploy.sh
@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# llmsproxy 原子化滚动部署脚本
|
# modelrouter 原子化滚动部署脚本
|
||||||
#
|
#
|
||||||
# 策略:
|
# 策略:
|
||||||
# 1. /tmp (tmpfs) 构建,/usr/local/bin (ext4) 部署
|
# 1. /tmp (tmpfs) 构建,/usr/local/bin (ext4) 部署
|
||||||
# 2. 构建后先 copy 到目标文件系统(.llmsproxy.tmp),
|
# 2. 构建后先 copy 到目标文件系统(.modelrouter.tmp),
|
||||||
# 再 rename(2) 做原子替换(同一文件系统内 mv 是原子的)
|
# 再 rename(2) 做原子替换(同一文件系统内 mv 是原子的)
|
||||||
# 3. runtime.json 不替换(保留运行时密钥、审计状态)
|
# 3. runtime.json 不替换(保留运行时密钥、审计状态)
|
||||||
# 4. systemctl restart → 服务中断仅在 Go 进程重启瞬间 (<500ms)
|
# 4. systemctl restart → 服务中断仅在 Go 进程重启瞬间 (<500ms)
|
||||||
@ -28,20 +28,21 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ---------- 配置 ----------
|
# ---------- 配置 ----------
|
||||||
readonly TARGET_BIN="/usr/local/bin/llmsproxy"
|
readonly TARGET_BIN="/usr/local/bin/modelrouter"
|
||||||
readonly TARGET_CONFIG="/etc/llmsproxy/config.yaml"
|
readonly TARGET_CONFIG="/etc/modelrouter/config.yaml"
|
||||||
readonly TARGET_ADAPTERS="/etc/llmsproxy/adapters"
|
readonly TARGET_ADAPTERS="/etc/modelrouter/adapters"
|
||||||
readonly TARGET_PLUGINS="/etc/llmsproxy/plugins"
|
readonly TARGET_PLUGINS="/etc/modelrouter/plugins"
|
||||||
readonly REPO_DIR="/home/program/llmsproxy"
|
# 仓库所在目录不等于产品名:这里保持真实路径,除非用 MODELROUTER_REPO_DIR 覆盖。
|
||||||
|
readonly REPO_DIR="${MODELROUTER_REPO_DIR:-/home/program/llmsproxy}"
|
||||||
readonly HEALTH_URL="http://127.0.0.1:8081/v1/models"
|
readonly HEALTH_URL="http://127.0.0.1:8081/v1/models"
|
||||||
readonly BIN_NAME="llmsproxy"
|
readonly BIN_NAME="modelrouter"
|
||||||
|
|
||||||
# 构建目录。默认落在仓库下的构建工作区,而不是 /tmp:本机 /tmp 是 8.8G
|
# 构建目录。默认落在仓库下的构建工作区,而不是 /tmp:本机 /tmp 是 8.8G
|
||||||
# tmpfs,实测被 Go 构建缓存占满后会让 go test/build 在链接阶段直接失败。
|
# tmpfs,实测被 Go 构建缓存占满后会让 go test/build 在链接阶段直接失败。
|
||||||
# 可用 LLMSPROXY_BUILD_DIR 覆盖;同一目录也被测试用作 GOTMPDIR。
|
# 可用 MODELROUTER_BUILD_DIR 覆盖;同一目录也被测试用作 GOTMPDIR。
|
||||||
readonly BUILD_SUBDIR=".build-work"
|
readonly BUILD_SUBDIR=".build-work"
|
||||||
if [[ -n "${LLMSPROXY_BUILD_DIR:-}" ]]; then
|
if [[ -n "${MODELROUTER_BUILD_DIR:-}" ]]; then
|
||||||
readonly TMP_DIR="${LLMSPROXY_BUILD_DIR}"
|
readonly TMP_DIR="${MODELROUTER_BUILD_DIR}"
|
||||||
else
|
else
|
||||||
readonly TMP_DIR="${REPO_DIR}/${BUILD_SUBDIR}"
|
readonly TMP_DIR="${REPO_DIR}/${BUILD_SUBDIR}"
|
||||||
fi
|
fi
|
||||||
@ -59,7 +60,7 @@ usage() {
|
|||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
用法: ./deploy.sh [--config <file>]
|
用法: ./deploy.sh [--config <file>]
|
||||||
|
|
||||||
--config <file> 投放这份配置到 /etc/llmsproxy/config.yaml(原子替换)。
|
--config <file> 投放这份配置到 /etc/modelrouter/config.yaml(原子替换)。
|
||||||
省略时不改动线上配置,只部署二进制与适配器。
|
省略时不改动线上配置,只部署二进制与适配器。
|
||||||
无论是否指定,重启前都会用新二进制 -check 预检线上配置。
|
无论是否指定,重启前都会用新二进制 -check 预检线上配置。
|
||||||
|
|
||||||
@ -103,6 +104,17 @@ precheck() {
|
|||||||
command -v systemctl &>/dev/null || fail "未找到 systemctl"
|
command -v systemctl &>/dev/null || fail "未找到 systemctl"
|
||||||
[[ $(id -u) -eq 0 ]] || fail "需要 root 权限"
|
[[ $(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
|
if [[ -n "$NEW_CONFIG_SRC" ]]; then
|
||||||
[[ -f "$NEW_CONFIG_SRC" ]] || fail "待部署配置不存在: $NEW_CONFIG_SRC"
|
[[ -f "$NEW_CONFIG_SRC" ]] || fail "待部署配置不存在: $NEW_CONFIG_SRC"
|
||||||
# 同一文件比对:--config 指向线上文件本身时没有"投放"动作可做,
|
# 同一文件比对:--config 指向线上文件本身时没有"投放"动作可做,
|
||||||
@ -153,7 +165,7 @@ build() {
|
|||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w" \
|
-ldflags="-s -w" \
|
||||||
-o "$TMP_DIR/bin/$BIN_NAME" \
|
-o "$TMP_DIR/bin/$BIN_NAME" \
|
||||||
./cmd/llmsproxy
|
./cmd/modelrouter
|
||||||
|
|
||||||
NEW_BIN="$TMP_DIR/bin/$BIN_NAME"
|
NEW_BIN="$TMP_DIR/bin/$BIN_NAME"
|
||||||
NEW_SIZE=$(stat -c%s "$NEW_BIN")
|
NEW_SIZE=$(stat -c%s "$NEW_BIN")
|
||||||
@ -254,7 +266,7 @@ verify_config() {
|
|||||||
|
|
||||||
# ---------- 密钥可解封校验 ----------
|
# ---------- 密钥可解封校验 ----------
|
||||||
# config.yaml 里的凭据以 enc:v1: 密文落盘,解密钥匙是 master.key(或
|
# 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"
|
[[ -n "$runtime_file" ]] || runtime_file="$(dirname "$TARGET_CONFIG")/runtime.json"
|
||||||
key_file="$(dirname "$runtime_file")/master.key"
|
key_file="$(dirname "$runtime_file")/master.key"
|
||||||
|
|
||||||
if [[ -n "${LLMS_PROXY_MASTER_KEY:-}" ]]; then
|
MASTER_KEY_ENV="${MODELROUTER_MASTER_KEY:-${LLMS_PROXY_MASTER_KEY:-}}"
|
||||||
log " 钥匙来源: 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
|
if out=$("$TARGET_BIN" -show-secrets -config "$TARGET_CONFIG" 2>&1); then
|
||||||
log "OK 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
log "OK 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
err "凭据无法用 LLMS_PROXY_MASTER_KEY 解封,未替换任何文件:"
|
err "凭据无法用 MODELROUTER_MASTER_KEY 解封,未替换任何文件:"
|
||||||
printf '%s\n' "$out" >&2
|
printf '%s\n' "$out" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$key_file" ]]; then
|
if [[ ! -f "$key_file" ]]; then
|
||||||
err "配置含 $sealed 处密文,但钥匙文件不存在: $key_file"
|
err "配置含 $sealed 处密文,但钥匙文件不存在: $key_file"
|
||||||
err " 没有它,部署后服务将无法启动。恢复 master.key 或设置 LLMS_PROXY_MASTER_KEY 后重试。"
|
err " 没有它,部署后服务将无法启动。恢复 master.key 或设置 MODELROUTER_MASTER_KEY 后重试。"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
log " 钥匙文件: $key_file ($(stat -c '%A' "$key_file" 2>/dev/null || echo '?'),$(wc -c < "$key_file") 字节)"
|
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.<时间戳> 目录,而回滚只用到最近一次
|
# 每次部署都新建一个 <prefix>.bak.<时间戳> 目录,而回滚只用到最近一次
|
||||||
# ($BACKUP_BIN / $BACKUP_CONFIG 都是单文件覆盖)。多出来的目录从没人清理,
|
# ($BACKUP_BIN / $BACKUP_CONFIG 都是单文件覆盖)。多出来的目录从没人清理,
|
||||||
# 实测线上已积累 71 个,/etc/llmsproxy 因此涨到 122M。
|
# 实测线上已积累 71 个,/etc/modelrouter 因此涨到 122M。
|
||||||
#
|
#
|
||||||
# 保留最近 KEEP_BACKUPS 份足够回滚,同时给目录数设上限——否则一次误配置
|
# 保留最近 KEEP_BACKUPS 份足够回滚,同时给目录数设上限——否则一次误配置
|
||||||
# 就可能在几秒内造出成百上千个目录。
|
# 就可能在几秒内造出成百上千个目录。
|
||||||
@ -327,7 +340,7 @@ KEEP_BACKUPS=5
|
|||||||
|
|
||||||
prune_backups() {
|
prune_backups() {
|
||||||
local prefix="$1"
|
local prefix="$1"
|
||||||
local base="/etc/llmsproxy"
|
local base="/etc/modelrouter"
|
||||||
[[ -d "$base" ]] || return 0
|
[[ -d "$base" ]] || return 0
|
||||||
# 防误用:前缀必须是已知的两个(避免有人传空串删掉整个目录)
|
# 防误用:前缀必须是已知的两个(避免有人传空串删掉整个目录)
|
||||||
case "$prefix" in
|
case "$prefix" in
|
||||||
@ -393,7 +406,7 @@ sync_adapters() {
|
|||||||
[[ -d "$SRC_ADAPTERS" ]] || fail "内嵌适配器目录不存在: $SRC_ADAPTERS"
|
[[ -d "$SRC_ADAPTERS" ]] || fail "内嵌适配器目录不存在: $SRC_ADAPTERS"
|
||||||
|
|
||||||
# 备份旧的适配器文件(含任何运行时覆盖版本,如 opencode.lua)
|
# 备份旧的适配器文件(含任何运行时覆盖版本,如 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
|
if [[ -d "$TARGET_ADAPTERS" ]]; then
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
cp -rf "$TARGET_ADAPTERS/"*.lua "$BACKUP_DIR/" 2>/dev/null || true
|
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 三个渠道修复就这样
|
# 内置插件本身的**修复无法下发**。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"
|
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"
|
mkdir -p "$BACKUP_DIR"
|
||||||
local added=0 updated=0 same=0
|
local added=0 updated=0 same=0
|
||||||
for src in "$SRC_PLUGINS"/*.lua; do
|
for src in "$SRC_PLUGINS"/*.lua; do
|
||||||
@ -481,8 +494,8 @@ sync_plugins() {
|
|||||||
# ---------- 重启服务 ----------
|
# ---------- 重启服务 ----------
|
||||||
restart_service() {
|
restart_service() {
|
||||||
log "重启服务(中断 <500ms)"
|
log "重启服务(中断 <500ms)"
|
||||||
systemctl restart llmsproxy.service || fail "systemd restart 失败"
|
systemctl restart modelrouter.service || fail "systemd restart 失败"
|
||||||
log "OK llmsproxy.service restarted"
|
log "OK modelrouter.service restarted"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- 健康检查 ----------
|
# ---------- 健康检查 ----------
|
||||||
@ -505,7 +518,7 @@ healthcheck() {
|
|||||||
(( attempt++ ))
|
(( attempt++ ))
|
||||||
done
|
done
|
||||||
|
|
||||||
warn "健康检查未通过,请检查服务状态: systemctl status llmsproxy.service"
|
warn "健康检查未通过,请检查服务状态: systemctl status modelrouter.service"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,7 +554,7 @@ rollback() {
|
|||||||
|
|
||||||
if (( restored_any == 0 )); then
|
if (( restored_any == 0 )); then
|
||||||
err "无任何回滚点,无法自动恢复"
|
err "无任何回滚点,无法自动恢复"
|
||||||
err "请手动检查: systemctl status llmsproxy.service && journalctl -u llmsproxy -n 50"
|
err "请手动检查: systemctl status modelrouter.service && journalctl -u modelrouter -n 50"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -555,9 +568,9 @@ rollback() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log "重启服务(旧版本)"
|
log "重启服务(旧版本)"
|
||||||
systemctl restart llmsproxy.service || {
|
systemctl restart modelrouter.service || {
|
||||||
err "重启失败,服务可能完全宕机"
|
err "重启失败,服务可能完全宕机"
|
||||||
err "请手动检查: systemctl status llmsproxy.service"
|
err "请手动检查: systemctl status modelrouter.service"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +605,7 @@ print_summary() {
|
|||||||
# 于是坏配置在服务仍然健康时就被拦下,根本不会触发重启。
|
# 于是坏配置在服务仍然健康时就被拦下,根本不会触发重启。
|
||||||
main() {
|
main() {
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
log "═══ llmsproxy 原子部署开始 ═══"
|
log "═══ modelrouter 原子部署开始 ═══"
|
||||||
precheck
|
precheck
|
||||||
# 密钥校验放在 build/替换之前:它的失败后果最重(配置里的密文永久解不开),
|
# 密钥校验放在 build/替换之前:它的失败后果最重(配置里的密文永久解不开),
|
||||||
# 必须在动任何文件之前就挡住。此阶段用的是**当前线上二进制**,它已经
|
# 必须在动任何文件之前就挡住。此阶段用的是**当前线上二进制**,它已经
|
||||||
|
|||||||
@ -389,10 +389,10 @@ flow 类型,不是"登录成功"。
|
|||||||
channels 插件用 `storeDir()` 决定存储位置,它有多级兜底:
|
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
|
测试插件目录 = /tmp/TestXXX/002/plugins
|
||||||
@ -404,7 +404,7 @@ storeDir 结果 = /etc/llmsproxy/plugins ← 探针实测输出
|
|||||||
`.channels-store.json` 从 1730 覆盖成 1986 字节、账号池清空(inode 都换了)。
|
`.channels-store.json` 从 1730 覆盖成 1986 字节、账号池清空(inode 都换了)。
|
||||||
|
|
||||||
只有 `internal/lua` 复现,`internal/core` 不复现 —— 因为 core 的测试台
|
只有 `internal/lua` 复现,`internal/core` 不复现 —— 因为 core 的测试台
|
||||||
(`kernelTestCore`)设了 `LLMSPROXY_PLUGIN_DIR`。**这是隔离做得不一致**的代价。
|
(`kernelTestCore`)设了 `MODELROUTER_PLUGIN_DIR`。**这是隔离做得不一致**的代价。
|
||||||
|
|
||||||
同类前科:曾把 26 个测试账号写进生产 store。两次同一类错误。
|
同类前科:曾把 26 个测试账号写进生产 store。两次同一类错误。
|
||||||
|
|
||||||
|
|||||||
@ -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
|
// real HTTP: startup, auth, chat, streaming SSE, image generation, and
|
||||||
// AUTO failover when a higher-priority source fails.
|
// AUTO failover when a higher-priority source fails.
|
||||||
package e2e
|
package e2e
|
||||||
@ -100,15 +100,15 @@ type gatewayUnderTest struct {
|
|||||||
func buildBinary(t *testing.T) string {
|
func buildBinary(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
bin := filepath.Join(dir, "llmsproxy")
|
bin := filepath.Join(dir, "modelrouter")
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
bin += ".exe" // go build writes the exact name; Windows needs the suffix to exec
|
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" {
|
if err != nil && runtime.GOOS == "windows" {
|
||||||
// local Windows dev box may lack LuaJIT: fall back to the bundled
|
// local Windows dev box may lack LuaJIT: fall back to the bundled
|
||||||
// Lua runtime (the e2e adapters used here are passthrough-only)
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("build: %v\n%s", err, out)
|
t.Fatalf("build: %v\n%s", err, out)
|
||||||
|
|||||||
@ -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
|
// 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
|
// 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) {
|
func Load(path string) (*Config, error) {
|
||||||
if _, err := EnsureDefault(path); err != nil {
|
if _, err := EnsureDefault(path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -27,8 +27,9 @@ func NewGatewayKey() (string, error) {
|
|||||||
|
|
||||||
// SecretBox encrypts and decrypts sensitive values (upstream API keys,
|
// SecretBox encrypts and decrypts sensitive values (upstream API keys,
|
||||||
// custom header values, gateway keys) for persist-time protection. The
|
// custom header values, gateway keys) for persist-time protection. The
|
||||||
// master key comes from the LLMS_PROXY_MASTER_KEY environment variable
|
// master key comes from the MODELROUTER_MASTER_KEY environment variable
|
||||||
// (hex, 64 chars) or from a generated <runtime-dir>/master.key file.
|
// (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 {
|
type SecretBox struct {
|
||||||
aead cipher.AEAD
|
aead cipher.AEAD
|
||||||
}
|
}
|
||||||
@ -37,10 +38,18 @@ type SecretBox struct {
|
|||||||
// file next to the runtime file. If neither exists, a fresh random key is
|
// file next to the runtime file. If neither exists, a fresh random key is
|
||||||
// generated and written to master.key with mode 0600.
|
// generated and written to master.key with mode 0600.
|
||||||
func NewSecretBox(runtimeFile string) (*SecretBox, error) {
|
func NewSecretBox(runtimeFile string) (*SecretBox, error) {
|
||||||
if v := os.Getenv("LLMS_PROXY_MASTER_KEY"); v != "" {
|
// New name first, then the pre-rename name: an existing unit file that still
|
||||||
key, err := hex.DecodeString(strings.TrimSpace(v))
|
// 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 {
|
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)
|
return newBox(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、
|
// callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 三个 callback 平台的回调路径**各是注册值**,且校验规则不同:
|
// 三个 callback 平台的回调路径**各是注册值**,且校验规则不同:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ 无凭据账号的批量清理:**只删没有凭据的**,有凭据的一律不动。
|
// ★ 无凭据账号的批量清理:**只删没有凭据的**,有凭据的一律不动。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ codearts 的模型目录必须从**两个**签名端点拉取并合并。
|
// ★ codearts 的模型目录必须从**两个**签名端点拉取并合并。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CodeArts 端到端:起真实回调服务器 → 模上游回 code → 走真实 STS 交换
|
// CodeArts 端到端:起真实回调服务器 → 模上游回 code → 走真实 STS 交换
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CodeArts 的 portal 拒绝 <10000 的回调端口(参考 listenOnCallbackPort 的
|
// CodeArts 的 portal 拒绝 <10000 的回调端口(参考 listenOnCallbackPort 的
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 有道每日活动领取是**三步流程**,参考注释点名:无可用活动是**正常业务状态**,
|
// 有道每日活动领取是**三步流程**,参考注释点名:无可用活动是**正常业务状态**,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 有道登录闭环的判据:portal 会把授权重定向回 **本机 loopback 接收器**
|
// 有道登录闭环的判据:portal 会把授权重定向回 **本机 loopback 接收器**
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loomyHarness:Loomy 微信扫码长轮询的测试台。
|
// loomyHarness:Loomy 微信扫码长轮询的测试台。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 远端模型目录的判据:**远端为主 + 兜底补位**(照参考 reconcileWithFallback)。
|
// 远端模型目录的判据:**远端为主 + 兜底补位**(照参考 reconcileWithFallback)。
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// channelsMoreTest:新引入的三条真实流程 + 两个边界流的行为对齐测试。
|
// channelsMoreTest:新引入的三条真实流程 + 两个边界流的行为对齐测试。
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ 插件加载时必须清掉上一进程留下的孤儿回调文件。
|
// ★ 插件加载时必须清掉上一进程留下的孤儿回调文件。
|
||||||
//
|
//
|
||||||
// 真实事故:生产 `/etc/llmsproxy/plugins/` 攒了 **18 个 callback-*.port**
|
// 真实事故:生产 `/etc/modelrouter/plugins/` 攒了 **18 个 callback-*.port**
|
||||||
// (对应的 .json 早已消失)。它们永远不会被清,因为:
|
// (对应的 .json 早已消失)。它们永远不会被清,因为:
|
||||||
//
|
//
|
||||||
// · loopbacks 追踪表只活在内存(plugin.state.loopbacks);
|
// · loopbacks 追踪表只活在内存(plugin.state.loopbacks);
|
||||||
@ -25,7 +25,7 @@ func TestPluginLoadSweepsOrphanCallbackFiles(t *testing.T) {
|
|||||||
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
|
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
|
||||||
|
|
||||||
// 摆上一轮的残留(模拟上一进程被强杀后的现场)
|
// 摆上一轮的残留(模拟上一进程被强杀后的现场)
|
||||||
orphans := []string{
|
orphans := []string{
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestChannelsPluginEndToEnd 是「同类插件」的完整验收:
|
// TestChannelsPluginEndToEnd 是「同类插件」的完整验收:
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ 「为什么没有可用账号」必须区分**真的用尽**与**查不到**。
|
// ★ 「为什么没有可用账号」必须区分**真的用尽**与**查不到**。
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// qoderReason 调真实的 transform_error 钩子(不自己重算)。
|
// qoderReason 调真实的 transform_error 钩子(不自己重算)。
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Qoder 活动领取有三条参考用**抓包 + 用户报障**定位过的「会误报」陷阱,
|
// Qoder 活动领取有三条参考用**抓包 + 用户报障**定位过的「会误报」陷阱,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// qoderHarness:Qoder PKCE 设备码轮的测试台。
|
// qoderHarness:Qoder PKCE 设备码轮的测试台。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// raccoonHarness:Raccoon 内置二维码扫码登录的测试台。
|
// raccoonHarness:Raccoon 内置二维码扫码登录的测试台。
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ 删除账号必须**真的把源撤掉** —— 且撤不掉时不能静默。
|
// ★ 删除账号必须**真的把源撤掉** —— 且撤不掉时不能静默。
|
||||||
@ -190,8 +190,14 @@ func TestChannelsDeclaresEveryKernelActionItCalls(t *testing.T) {
|
|||||||
|
|
||||||
// ② 抽出实际调用的动作
|
// ② 抽出实际调用的动作
|
||||||
called := map[string]bool{}
|
called := map[string]bool{}
|
||||||
for _, m := range regexp.MustCompile(`__llmsproxy_kernel\.call\(\s*"([a-z_]+)"`).FindAllStringSubmatch(s, -1) {
|
// 插件只允许走公开 SDK(modelrouter.kernel);旧名 __llmsproxy_kernel 是
|
||||||
called[m[1]] = true
|
// 兼容别名,新代码用它就说明插件又在依赖内核私有全局了。
|
||||||
|
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 {
|
if len(called) == 0 {
|
||||||
t.Fatal("没扫描到任何内核调用(判据失效)")
|
t.Fatal("没扫描到任何内核调用(判据失效)")
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ account_reorder 必须**真的改变内核里的源顺序**。
|
// ★ account_reorder 必须**真的改变内核里的源顺序**。
|
||||||
|
|||||||
@ -33,8 +33,8 @@ func TestAccountPoolStoreIsAtomicAndConsistent(t *testing.T) {
|
|||||||
storePath := filepath.Join(dir, ".channels-store.json")
|
storePath := filepath.Join(dir, ".channels-store.json")
|
||||||
t.Logf("插件存储目录: %s", dir)
|
t.Logf("插件存储目录: %s", dir)
|
||||||
// ⚠️ 必须在**测试目录**里,绝不能是生产路径
|
// ⚠️ 必须在**测试目录**里,绝不能是生产路径
|
||||||
if _, err := os.Stat("/etc/llmsproxy/plugins/.channels-store.json"); err == nil {
|
if _, err := os.Stat("/etc/modelrouter/plugins/.channels-store.json"); err == nil {
|
||||||
if filepath.Clean(dir) == "/etc/llmsproxy/plugins" {
|
if filepath.Clean(dir) == "/etc/modelrouter/plugins" {
|
||||||
t.Fatal("测试正在使用生产存储目录 —— 隔离失效(这会造成真实数据事故)")
|
t.Fatal("测试正在使用生产存储目录 —— 隔离失效(这会造成真实数据事故)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestChannelsStoreIsPluginOwned 是「插件自持持久化」的回归。
|
// TestChannelsStoreIsPluginOwned 是「插件自持持久化」的回归。
|
||||||
@ -23,7 +23,7 @@ func TestChannelsStoreIsPluginOwned(t *testing.T) {
|
|||||||
// 固定插件目录:两次「重启」必须共用同一目录,否则测不出持久化。
|
// 固定插件目录:两次「重启」必须共用同一目录,否则测不出持久化。
|
||||||
// 这也是防止测试写进生产插件目录的那道隔离(kernelTestCore 会 respect 它)。
|
// 这也是防止测试写进生产插件目录的那道隔离(kernelTestCore 会 respect 它)。
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
t.Setenv("LLMSPROXY_PLUGIN_DIR", dir)
|
t.Setenv("MODELROUTER_PLUGIN_DIR", dir)
|
||||||
load := func() *lua.Plugins {
|
load := func() *lua.Plugins {
|
||||||
c := kernelTestCore(t)
|
c := kernelTestCore(t)
|
||||||
ps := c.Plugins()
|
ps := c.Plugins()
|
||||||
@ -128,6 +128,10 @@ func TestChannelsMigratesLegacyKernelState(t *testing.T) {
|
|||||||
|
|
||||||
c := kernelTestCore(t)
|
c := kernelTestCore(t)
|
||||||
dir = c.Config().PluginDir
|
dir = c.Config().PluginDir
|
||||||
|
// 网关不再自动创建 plugin_dir(插件不由内核安装),测试自己建。
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
legacyPath := filepath.Join(dir, ".channels.state.json")
|
legacyPath := filepath.Join(dir, ".channels.state.json")
|
||||||
if err := os.WriteFile(legacyPath, b, 0o600); err != nil {
|
if err := os.WriteFile(legacyPath, b, 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -179,7 +183,7 @@ func TestChannelsMigrationKeepsLegacyOnWriteFailure(t *testing.T) {
|
|||||||
// (<dir>/.channels-store.json.tmp)**占成一个目录**,io.open(tmp,"w") 必失败。
|
// (<dir>/.channels-store.json.tmp)**占成一个目录**,io.open(tmp,"w") 必失败。
|
||||||
// 不用 chmod:测试以 root 跑,root 绕过权限位,chmod 拦不住。
|
// 不用 chmod:测试以 root 跑,root 绕过权限位,chmod 拦不住。
|
||||||
sub := t.TempDir()
|
sub := t.TempDir()
|
||||||
t.Setenv("LLMSPROXY_PLUGIN_DIR", sub)
|
t.Setenv("MODELROUTER_PLUGIN_DIR", sub)
|
||||||
|
|
||||||
legacy := map[string]interface{}{
|
legacy := map[string]interface{}{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mustBox 构造一个真 SecretBox(与运行期同一实现,密钥落 runtime dir)。
|
// mustBox 构造一个真 SecretBox(与运行期同一实现,密钥落 runtime dir)。
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ TRAE 的 batch 模型目录解析:两个**真实缺陷**必须在移植时一起带上
|
// ★ TRAE 的 batch 模型目录解析:两个**真实缺陷**必须在移植时一起带上
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ TRAE 的**推理请求**必须用 Cloud-IDE-JWT 头族,不是 Bearer。
|
// ★ TRAE 的**推理请求**必须用 Cloud-IDE-JWT 头族,不是 Bearer。
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Trae 签到是最容易被"看起来对"骗过的地方:请求能发出去、上游可能也返回 200,
|
// Trae 签到是最容易被"看起来对"骗过的地方:请求能发出去、上游可能也返回 200,
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 手工移植的确定性派生必须与参考**逐字节一致**,否则签名的设备身份是错的
|
// 手工移植的确定性派生必须与参考**逐字节一致**,否则签名的设备身份是错的
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ TRAE ExchangeToken 的凭据在 **Result 里**,不在顶层。
|
// ★ TRAE ExchangeToken 的凭据在 **Result 里**,不在顶层。
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ TRAE 回调只带 code(PKCE 形态)时必须**明确报出**「暂不支持 PKCE」,
|
// ★ TRAE 回调只带 code(PKCE 形态)时必须**明确报出**「暂不支持 PKCE」,
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// traeHarness:TRAE callback 流的测试台。假上游模拟 ExchangeToken;
|
// traeHarness:TRAE callback 流的测试台。假上游模拟 ExchangeToken;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestChannelsVncStackEndToEnd 验证 VNC 兜底链路在**真插件**上跑通:
|
// TestChannelsVncStackEndToEnd 验证 VNC 兜底链路在**真插件**上跑通:
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 显示号泄漏是「测试单独跑绿、全量跑红」的根源,值得一条判据钉住它。
|
// 显示号泄漏是「测试单独跑绿、全量跑红」的根源,值得一条判据钉住它。
|
||||||
@ -34,7 +34,7 @@ func TestXDisplaySelectionReclaimsStaleLocks(t *testing.T) {
|
|||||||
// (见 kernelTestCore);硬编码 90 段会让本判据造出**根本不会被扫描到**的
|
// (见 kernelTestCore);硬编码 90 段会让本判据造出**根本不会被扫描到**的
|
||||||
// 陈旧锁,于是"没复用"这个断言永远失败 —— 那是判据与被测对象脱节。
|
// 陈旧锁,于是"没复用"这个断言永远失败 —— 那是判据与被测对象脱节。
|
||||||
base := 300
|
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 {
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||||
base = n
|
base = n
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ★ ZCode 的身份块是 3012 准入的**唯一开关**。
|
// ★ ZCode 的身份块是 3012 准入的**唯一开关**。
|
||||||
@ -269,7 +269,7 @@ func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) {
|
|||||||
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
|
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
|
||||||
|
|
||||||
c := kernelTestCore(t)
|
c := kernelTestCore(t)
|
||||||
ps := c.Plugins()
|
ps := c.Plugins()
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// zcodeHarness:ZCode 两段式服务端中介授权流的测试台。
|
// zcodeHarness:ZCode 两段式服务端中介授权流的测试台。
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestConfig returns an in-memory config backed by temp files so
|
// newTestConfig returns an in-memory config backed by temp files so
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。
|
// readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 内核动作实现(R4-c)
|
// 内核动作实现(R4-c)
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 本文件验证 R4-c/R4-d 的闭环:**插件自己驱动外部源的链接认证,然后把源接进内核**。
|
// 本文件验证 R4-c/R4-d 的闭环:**插件自己驱动外部源的链接认证,然后把源接进内核**。
|
||||||
@ -22,12 +22,12 @@ func kernelTestCore(t *testing.T) *Core {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pluginDir := filepath.Join(dir, "plugins")
|
pluginDir := filepath.Join(dir, "plugins")
|
||||||
// 插件的自有存储位置由插件自己决定(读 config.yaml 的 plugin_dir,或
|
// 插件的自有存储位置由插件自己决定(读 config.yaml 的 plugin_dir,或
|
||||||
// LLMSPROXY_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到
|
// MODELROUTER_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到
|
||||||
// /etc/llmsproxy/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试
|
// /etc/modelrouter/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试
|
||||||
// 把 26 个测试账号写进了生产 .channels-store.json)。
|
// 把 26 个测试账号写进了生产 .channels-store.json)。
|
||||||
// 调用方已显式设定时尊重它:多次「重启」要共用同一目录才能测持久化。
|
// 调用方已显式设定时尊重它:多次「重启」要共用同一目录才能测持久化。
|
||||||
if os.Getenv("LLMSPROXY_PLUGIN_DIR") == "" {
|
if os.Getenv("MODELROUTER_PLUGIN_DIR") == "" {
|
||||||
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
|
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
|
||||||
}
|
}
|
||||||
// ★ 显示器号与端口用**高段号**,避开生产。
|
// ★ 显示器号与端口用**高段号**,避开生产。
|
||||||
//
|
//
|
||||||
@ -36,8 +36,8 @@ func kernelTestCore(t *testing.T) *Core {
|
|||||||
// 并去抢,结果 `Xvfb 未就绪(/tmp/.X11-unix/X91 未出现)`,而生产上正跑着
|
// 并去抢,结果 `Xvfb 未就绪(/tmp/.X11-unix/X91 未出现)`,而生产上正跑着
|
||||||
// 一套浏览器栈。判据本身没错,错的是**两个 /tmp 互不可见**而号段重叠。
|
// 一套浏览器栈。判据本身没错,错的是**两个 /tmp 互不可见**而号段重叠。
|
||||||
// 故测试整体挪到 300+。
|
// 故测试整体挪到 300+。
|
||||||
if os.Getenv("LLMSPROXY_XDISPLAY_BASE") == "" {
|
if os.Getenv("MODELROUTER_XDISPLAY_BASE") == "" {
|
||||||
t.Setenv("LLMSPROXY_XDISPLAY_BASE", "300")
|
t.Setenv("MODELROUTER_XDISPLAY_BASE", "300")
|
||||||
}
|
}
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Path: filepath.Join(dir, "config.yaml"),
|
Path: filepath.Join(dir, "config.yaml"),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestNoKernelActionDeadlocksFromLua 对**每个**内核动作做同一条断言:
|
// TestNoKernelActionDeadlocksFromLua 对**每个**内核动作做同一条断言:
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoChainFor sits on the request hot path and reads keyAutoChains with no
|
// AutoChainFor sits on the request hot path and reads keyAutoChains with no
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/scheduler"
|
"modelrouter/internal/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Per-key AUTO chains let an admin give one user their own scheduling chain
|
// Per-key AUTO chains let an admin give one user their own scheduling chain
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The plugin outbound allowlist is built from the hosts of configured sources.
|
// The plugin outbound allowlist is built from the hosts of configured sources.
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Startup must be idempotent, and every credential consumer must see PLAINTEXT.
|
// Startup must be idempotent, and every credential consumer must see PLAINTEXT.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adapterPayload struct {
|
type adapterPayload struct {
|
||||||
@ -488,7 +488,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
if to == 0 {
|
if to == 0 {
|
||||||
to = time.Now().UnixMilli()
|
to = time.Now().UnixMilli()
|
||||||
}
|
}
|
||||||
csvHeaders(w, "llmsproxy-requests.csv")
|
csvHeaders(w, "modelrouter-requests.csv")
|
||||||
cw := csv.NewWriter(w)
|
cw := csv.NewWriter(w)
|
||||||
names := map[string]string{}
|
names := map[string]string{}
|
||||||
for _, k := range g.core.ListKeys() {
|
for _, k := range g.core.ListKeys() {
|
||||||
@ -537,7 +537,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.URL.Query().Get("export") == "keys-csv" {
|
if r.URL.Query().Get("export") == "keys-csv" {
|
||||||
csvHeaders(w, "llmsproxy-keys.csv")
|
csvHeaders(w, "modelrouter-keys.csv")
|
||||||
cw := csv.NewWriter(w)
|
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"})
|
_ = 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)
|
keyFilter := exportKey(r)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// apiV1Routes dispatches /api/v1/*. Kept separate from routes() so the original
|
// 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{}{
|
index := map[string]interface{}{
|
||||||
"api_version": "v1",
|
"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. " +
|
"'Authorization: Bearer <key>', '?api_key=<key>', or the gw_key cookie. " +
|
||||||
"Read endpoints accept any role; mutations require an admin key.",
|
"Read endpoints accept any role; mutations require an admin key.",
|
||||||
"discovery": []ep{
|
"discovery": []ep{
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// v1Gateway builds a gateway with one source and two keys, and returns the
|
// v1Gateway builds a gateway with one source and two keys, and returns the
|
||||||
|
|||||||
@ -13,11 +13,11 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/provider"
|
"modelrouter/internal/provider"
|
||||||
"llmsproxy/internal/scheduler"
|
"modelrouter/internal/scheduler"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chatRequest mirrors the OpenAI chat completions request the gateway accepts.
|
// chatRequest mirrors the OpenAI chat completions request the gateway accepts.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestMergeUsageKeepsZeroCacheDetails locks the streaming counterpart of the
|
// TestMergeUsageKeepsZeroCacheDetails locks the streaming counterpart of the
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRecordChatUsageKeepsReasoningTokens pins the reasoning-token plumbing.
|
// TestRecordChatUsageKeepsReasoningTokens pins the reasoning-token plumbing.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// deploy.sh is the only release-critical script with no automated coverage, and
|
// deploy.sh is the only release-critical script with no automated coverage, and
|
||||||
// its newest part — prune_adapter_backups — deletes directories under
|
// 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.
|
// REAL backup-name shape.
|
||||||
//
|
//
|
||||||
// The shape matters and got this wrong twice: the guard is
|
// 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.
|
// Redirect ONLY the base path. The logic under test must stay verbatim.
|
||||||
body = strings.Replace(body,
|
body = strings.Replace(body,
|
||||||
`local base="/etc/llmsproxy"`,
|
`local base="/etc/modelrouter"`,
|
||||||
`local base="`+base+`"`, 1)
|
`local base="`+base+`"`, 1)
|
||||||
// KEEP_BACKUPS must come from deploy.sh itself, NOT from a literal
|
// KEEP_BACKUPS must come from deploy.sh itself, NOT from a literal
|
||||||
// here: hardcoding 5 in the shim overrode whatever the script configured, so
|
// 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` 同样每次部署
|
// 事实:backup 清理最初只处理 `adapters.bak.*`,而 `sync_plugins` 同样每次部署
|
||||||
// 建一个 `plugins.bak.<时间戳>` —— 于是 adapters 的堆积被修好,plugins 继续涨。
|
// 建一个 `plugins.bak.<时间戳>` —— 于是 adapters 的堆积被修好,plugins 继续涨。
|
||||||
// 实测线上 plugins.bak 积累到 **36 个**、/etc/llmsproxy 涨到 192M。
|
// 实测线上 plugins.bak 积累到 **36 个**、/etc/modelrouter 涨到 192M。
|
||||||
//
|
//
|
||||||
// 这是「两处只修一处」的老形状。判据钉住:prune_all_backups 必须同时裁两个前缀,
|
// 这是「两处只修一处」的老形状。判据钉住:prune_all_backups 必须同时裁两个前缀,
|
||||||
// 且新增任何 .bak.<时间戳> 前缀时都要在这里补上(否则又是一个泄漏点)。
|
// 且新增任何 .bak.<时间戳> 前缀时都要在这里补上(否则又是一个泄漏点)。
|
||||||
@ -264,7 +264,7 @@ func TestDeployPruneCoversBothBackupPrefixes(t *testing.T) {
|
|||||||
k := strings.Index(src, "prune_backups()")
|
k := strings.Index(src, "prune_backups()")
|
||||||
kj := strings.Index(src[k:], "\n}\n")
|
kj := strings.Index(src[k:], "\n}\n")
|
||||||
fnBody := src[k : k+kj+3]
|
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+)`)
|
keepRe := regexp.MustCompile(`(?m)^KEEP_BACKUPS=(\d+)`)
|
||||||
km := keepRe.FindStringSubmatch(src)
|
km := keepRe.FindStringSubmatch(src)
|
||||||
if km == nil {
|
if km == nil {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model discovery: turn "here is a URL and a key" into a configured source.
|
// Model discovery: turn "here is a URL and a key" into a configured source.
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// upstream that answers /v1/models with the real captured catalog
|
// upstream that answers /v1/models with the real captured catalog
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mockUpstream() *httptest.Server {
|
func mockUpstream() *httptest.Server {
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Per-key AUTO chain admin API. The behaviours pinned here are the ones that
|
// Per-key AUTO chain admin API. The behaviours pinned here are the ones that
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminGateway builds a gateway whose admin key can call /api/keys.
|
// adminGateway builds a gateway whose admin key can call /api/keys.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- data layer: per-key isolation ----
|
// ---- data layer: per-key isolation ----
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// quotaGateway builds a gateway with two user keys that differ only in their
|
// quotaGateway builds a gateway with two user keys that differ only in their
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/scheduler"
|
"modelrouter/internal/scheduler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleKeysAPI manages gateway keys: GET /api/keys (admin: all keys),
|
// handleKeysAPI manages gateway keys: GET /api/keys (admin: all keys),
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/provider"
|
"modelrouter/internal/provider"
|
||||||
"llmsproxy/internal/scheduler"
|
"modelrouter/internal/scheduler"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleMessages serves the Anthropic Messages protocol (POST /v1/messages).
|
// handleMessages serves the Anthropic Messages protocol (POST /v1/messages).
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 本文件端到端验证 Phase 2 的核心主张:**对外协议由适配器决定,内核只调度**。
|
// 本文件端到端验证 Phase 2 的核心主张:**对外协议由适配器决定,内核只调度**。
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// allowedMountPrefixes are the only namespaces a plugin/adapters may claim.
|
// allowedMountPrefixes are the only namespaces a plugin/adapters may claim.
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 本文件锁定 R3 的两条不变量:
|
// 本文件锁定 R3 的两条不变量:
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/scheduler"
|
"modelrouter/internal/scheduler"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// piOverflowRe 是 @earendil-works/pi-ai 的 OVERFLOW_PATTERNS 中对应的一条。
|
// piOverflowRe 是 @earendil-works/pi-ai 的 OVERFLOW_PATTERNS 中对应的一条。
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// callPlugin exposes exactly one export plus a function that is NOT exported.
|
// callPlugin exposes exactly one export plus a function that is NOT exported.
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pluginHTTPPrefix is the mount point. It is deliberately narrow: a plugin's
|
// pluginHTTPPrefix is the mount point. It is deliberately narrow: a plugin's
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newFailingServer serves handler and returns its test server.
|
// newFailingServer serves handler and returns its test server.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
// gatewayWithBilling boots a gateway with the bundled billing plugin loaded, so
|
// gatewayWithBilling boots a gateway with the bundled billing plugin loaded, so
|
||||||
|
|||||||
@ -29,9 +29,9 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/provider"
|
"modelrouter/internal/provider"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StreamState 是一个流会话在序列化侧的累积状态(角色前导是否已发、已见
|
// StreamState 是一个流会话在序列化侧的累积状态(角色前导是否已发、已见
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 本文件锁定对外序列化的**优先级契约**:
|
// 本文件锁定对外序列化的**优先级契约**:
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed ui/*
|
//go:embed ui/*
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/core"
|
"modelrouter/internal/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file covers the second round of the same class of defect bf0657b fixed.
|
// This file covers the second round of the same class of defect bf0657b fixed.
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These cover the two write-path defects found while adding a model to a live
|
// These cover the two write-path defects found while adding a model to a live
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
// /api/stats?period=day&limit=1&key=sk-gw-7ffe12c5cbacec40bd39a7291df9a9ed
|
// /api/stats?period=day&limit=1&key=sk-gw-7ffe12c5cbacec40bd39a7291df9a9ed
|
||||||
// → curl: (52) Empty reply from server
|
// → curl: (52) Empty reply from server
|
||||||
// → log: http: panic serving …: invalid memory address or nil pointer dereference
|
// → 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
|
// A key with no recorded traffic is a legitimate question, and zero is its
|
||||||
// answer — not a crash.
|
// answer — not a crash.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestThinkingFromClient 把「客户端措辞 → 中立语义」这条翻译链钉死。
|
// TestThinkingFromClient 把「客户端措辞 → 中立语义」这条翻译链钉死。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Token accounting must record TOKENS, not byte estimates and not image
|
// Token accounting must record TOKENS, not byte estimates and not image
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/provider"
|
"modelrouter/internal/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestUI_ChatPageGroupsImageModels is a UI-contract guard for the bug that
|
// TestUI_ChatPageGroupsImageModels is a UI-contract guard for the bug that
|
||||||
|
|||||||
@ -12,9 +12,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gemini is the only adapter whose endpoint carries the model in the PATH
|
// Gemini is the only adapter whose endpoint carries the model in the PATH
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSourceStatus_CarriesModelKind is the fix for a UI bug where the chat
|
// TestSourceStatus_CarriesModelKind is the fix for a UI bug where the chat
|
||||||
|
|||||||
@ -20,9 +20,9 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// proxyFor picks the transport proxy for one source. An explicit per-source
|
// proxyFor picks the transport proxy for one source. An explicit per-source
|
||||||
|
|||||||
@ -14,9 +14,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/lua"
|
"modelrouter/internal/lua"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestProvider(t *testing.T, src config.Source) *Provider {
|
func newTestProvider(t *testing.T, src config.Source) *Provider {
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"llmsproxy/internal/config"
|
"modelrouter/internal/config"
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 上游重试是**内核的通用机制**,判据纯数据(status + body 子串),
|
// 上游重试是**内核的通用机制**,判据纯数据(status + body 子串),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestResultIsEmptyMatchesTheReasoningOnlyResponse guards the production bug
|
// TestResultIsEmptyMatchesTheReasoningOnlyResponse guards the production bug
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// busyWait is how long a fully-busy tier is polled for a free slot before the
|
// busyWait is how long a fully-busy tier is polled for a free slot before the
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"llmsproxy/internal/types"
|
"modelrouter/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fakeProvider is an in-memory Provider used to exercise chain scheduling
|
// fakeProvider is an in-memory Provider used to exercise chain scheduling
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# llmsproxy — 统一 OpenAI 兼容网关配置(示例模板)
|
# modelrouter — 统一 OpenAI 兼容网关配置(示例模板)
|
||||||
#
|
#
|
||||||
# 这个是打包进发行包、供首次安装使用的模板。它故意不含任何可用的凭据:
|
# 这个是打包进发行包、供首次安装使用的模板。它故意不含任何可用的凭据:
|
||||||
# 首次启动时程序会生成一个随机 admin key 并写入实际配置(WebUI「密钥」页可见)。
|
# 首次启动时程序会生成一个随机 admin key 并写入实际配置(WebUI「密钥」页可见)。
|
||||||
@ -20,14 +20,10 @@ default_model: AUTO
|
|||||||
# Lua 适配器目录(默认 adapters/,首次启动自动写入内置适配器)
|
# Lua 适配器目录(默认 adapters/,首次启动自动写入内置适配器)
|
||||||
adapter_dir: adapters
|
adapter_dir: adapters
|
||||||
|
|
||||||
# 插件目录(可选)。设置后:
|
# Plugin directory (optional). Plugins are independent Lua files managed by the
|
||||||
# - 首次启动会把随核心发布的示例插件(billing:按源/模型/密钥计费 + 仪表盘)
|
# operator. Release packages do not include or install plugins automatically.
|
||||||
# 写入本目录,它会在请求流水线上挂钩子,并向 WebUI 注入一个「Billing」页面
|
# Place desired .lua files in this directory before starting the gateway.
|
||||||
# 和状态页上的一块总开销组件;
|
# plugin_dir: /etc/modelrouter/plugins
|
||||||
# - 之后 WebUI「插件」页与 Electron 壳的设置面板可安装/禁用/删除/编辑。
|
|
||||||
# 目录一旦存在即以目录为准:删除或改写内置插件都是真实生效的操作。
|
|
||||||
# 留空 = 插件功能完全关闭(不影响网关其它功能)。
|
|
||||||
# plugin_dir: /etc/llmsproxy/plugins
|
|
||||||
|
|
||||||
# 运行时文件:存放 WebUI 管理的源模板、已删除标记、预置模板名单。
|
# 运行时文件:存放 WebUI 管理的源模板、已删除标记、预置模板名单。
|
||||||
#
|
#
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user