@khanglvm/llm-router 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.2] - 2026-04-23
11
+
12
+ ### Fixed
13
+ - `yarn dev` now force-reclaims stale dev web-console listeners on startup and restarts matching stale dev routers so the next dev session takes over the sandbox cleanly instead of inheriting the old process.
14
+
10
15
  ## [2.5.1] - 2026-04-23
11
16
 
12
17
  ### Fixed
package/README.md CHANGED
@@ -29,7 +29,7 @@ llr ai-help # agent-oriented setup brief
29
29
  - **Model aliases with routing** — group models into stable alias names with weighted round-robin, quota-aware balancing, and automatic fallback
30
30
  - **Rate limiting** — set request caps per model or across all models over configurable time windows
31
31
  - **Coding tool routing** — one-click routing config for Codex CLI, Claude Code, Factory Droid, and AMP
32
- - **Dev sandbox** — `yarn dev` runs the console against a dedicated dev config/router port, highlights dev mode in terminal + UI, and can clone the production config into the sandbox for quick iteration
32
+ - **Dev sandbox** — `yarn dev` runs the console against a dedicated dev config/router port, highlights dev mode in terminal + UI, can clone the production config into the sandbox for quick iteration, and automatically reclaims stale dev listeners before the next session starts
33
33
  - **Claude native web tools** — local handling for Claude web search and page fetch requests, with selectable Claude Code web-search providers from the shared Web Search config
34
34
  - **Seamless local updates** — `llr update` keeps the fixed local router endpoint online, drains in-flight requests, and automatically retries through backend restart windows
35
35
  - **Web search** — built-in web search for AMP and other router-managed tools
@@ -60,7 +60,7 @@ That means `llr update` can install a new package version and gracefully swap th
60
60
  yarn dev
61
61
  ```
62
62
 
63
- Development mode uses the dedicated `~/.llm-router-dev.json` config and its own local router port so it can run alongside a startup-managed or manually started production router. The terminal and Web UI both show a dev-mode indicator, and the dev Web UI includes a one-click sync action to copy the current production config into the sandbox without changing the dev router binding.
63
+ Development mode uses the dedicated `~/.llm-router-dev.json` config and its own local router port so it can run alongside a startup-managed or manually started production router. The terminal and Web UI both show a dev-mode indicator, the dev Web UI includes a one-click sync action to copy the current production config into the sandbox without changing the dev router binding, and each new `yarn dev` run automatically takes over any stale dev web-console/router listeners from a prior session.
64
64
 
65
65
  ## Web UI
66
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/llm-router",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "LLM Router: single gateway endpoint for multi-provider LLMs with unified OpenAI+Anthropic format and seamless fallback",
5
5
  "keywords": [
6
6
  "llm-router",
@@ -0,0 +1,114 @@
1
+ import { getActiveRuntimeState } from "./instance-state.js";
2
+ import { reclaimPort } from "./port-reclaim.js";
3
+ import { startWebConsoleServer } from "./web-console-server.js";
4
+
5
+ const DEV_ROUTER_STOP_REASON = "Stopping the dev router because the dev web console exited.";
6
+
7
+ function normalizeHost(value) {
8
+ return String(value || "127.0.0.1").trim() || "127.0.0.1";
9
+ }
10
+
11
+ function shouldRestartStaleDevRouter(runtimeBeforeStart, runtimeAfterStart, snapshot) {
12
+ if (!runtimeBeforeStart || !runtimeAfterStart || !snapshot?.router?.running) return false;
13
+ if (Number(runtimeBeforeStart.pid) !== Number(runtimeAfterStart.pid)) return false;
14
+ if (snapshot?.config?.parseError) return false;
15
+ if (!Number(snapshot?.config?.providerCount)) return false;
16
+
17
+ const localServer = snapshot?.config?.localServer || {};
18
+ return Number(runtimeAfterStart.port) === Number(localServer.port)
19
+ && normalizeHost(runtimeAfterStart.host) === normalizeHost(localServer.host);
20
+ }
21
+
22
+ async function stopDevRouterAfterExit(server, onError) {
23
+ if (!server || typeof server.stopRouter !== "function") return;
24
+
25
+ try {
26
+ await server.stopRouter({
27
+ reason: DEV_ROUTER_STOP_REASON,
28
+ reclaimPortIfStopped: true
29
+ });
30
+ } catch (error) {
31
+ onError(`Failed stopping the dev router during shutdown: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ }
34
+
35
+ export async function startManagedDevWebConsole(options = {}, deps = {}) {
36
+ const line = typeof deps.line === "function" ? deps.line : console.log;
37
+ const error = typeof deps.error === "function" ? deps.error : console.error;
38
+ const startWebConsoleServerFn = typeof deps.startWebConsoleServer === "function"
39
+ ? deps.startWebConsoleServer
40
+ : startWebConsoleServer;
41
+ const getActiveRuntimeStateFn = typeof deps.getActiveRuntimeState === "function"
42
+ ? deps.getActiveRuntimeState
43
+ : getActiveRuntimeState;
44
+ const reclaimPortFn = typeof deps.reclaimPort === "function"
45
+ ? deps.reclaimPort
46
+ : (args) => reclaimPort(args, deps);
47
+ const serverOptions = {
48
+ ...options,
49
+ devMode: true
50
+ };
51
+ const runtimeBeforeStart = await getActiveRuntimeStateFn().catch(() => null);
52
+
53
+ let server;
54
+ try {
55
+ server = await startWebConsoleServerFn(serverOptions);
56
+ } catch (startError) {
57
+ if (startError?.code !== "EADDRINUSE") throw startError;
58
+
59
+ const reclaimed = await reclaimPortFn({
60
+ port: serverOptions.port,
61
+ line,
62
+ error
63
+ });
64
+ if (!reclaimed?.ok) {
65
+ throw new Error(reclaimed?.errorMessage || `Failed to reclaim port ${serverOptions.port}.`);
66
+ }
67
+
68
+ line(`Port ${serverOptions.port} reclaimed successfully.`);
69
+ server = await startWebConsoleServerFn(serverOptions);
70
+ }
71
+
72
+ const startupSnapshot = typeof server.getSnapshot === "function"
73
+ ? await server.getSnapshot().catch(() => null)
74
+ : null;
75
+ const runtimeAfterStart = await getActiveRuntimeStateFn().catch(() => null);
76
+ if (shouldRestartStaleDevRouter(runtimeBeforeStart, runtimeAfterStart, startupSnapshot)
77
+ && typeof server.restartRouter === "function") {
78
+ await server.restartRouter(startupSnapshot.config.localServer);
79
+ }
80
+
81
+ let stopRouterPromise = null;
82
+ const ensureDevRouterStopped = () => {
83
+ if (stopRouterPromise) return stopRouterPromise;
84
+ stopRouterPromise = stopDevRouterAfterExit(server, error);
85
+ return stopRouterPromise;
86
+ };
87
+
88
+ const done = (async () => {
89
+ let result;
90
+ try {
91
+ result = await server.done;
92
+ } finally {
93
+ await ensureDevRouterStopped();
94
+ }
95
+ return result;
96
+ })();
97
+
98
+ let shutdownPromise = null;
99
+ const shutdown = async (reason = "dev-console-closed") => {
100
+ if (shutdownPromise) return shutdownPromise;
101
+ shutdownPromise = (async () => {
102
+ await ensureDevRouterStopped();
103
+ await server.close(reason);
104
+ return done;
105
+ })();
106
+ return shutdownPromise;
107
+ };
108
+
109
+ return {
110
+ ...server,
111
+ done,
112
+ shutdown
113
+ };
114
+ }