omniroute 3.4.3 → 3.4.4

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.
Files changed (109) hide show
  1. package/README.md +4 -0
  2. package/app/.next/BUILD_ID +1 -1
  3. package/app/.next/build-manifest.json +3 -3
  4. package/app/.next/prerender-manifest.json +3 -3
  5. package/app/.next/server/app/(dashboard)/dashboard/agents/page_client-reference-manifest.js +1 -1
  6. package/app/.next/server/app/(dashboard)/dashboard/analytics/page_client-reference-manifest.js +1 -1
  7. package/app/.next/server/app/(dashboard)/dashboard/api-manager/page_client-reference-manifest.js +1 -1
  8. package/app/.next/server/app/(dashboard)/dashboard/audit/page_client-reference-manifest.js +1 -1
  9. package/app/.next/server/app/(dashboard)/dashboard/auto-combo/page_client-reference-manifest.js +1 -1
  10. package/app/.next/server/app/(dashboard)/dashboard/cache/page_client-reference-manifest.js +1 -1
  11. package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  12. package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  13. package/app/.next/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
  14. package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  15. package/app/.next/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
  16. package/app/.next/server/app/(dashboard)/dashboard/limits/page_client-reference-manifest.js +1 -1
  17. package/app/.next/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
  18. package/app/.next/server/app/(dashboard)/dashboard/media/page_client-reference-manifest.js +1 -1
  19. package/app/.next/server/app/(dashboard)/dashboard/memory/page_client-reference-manifest.js +1 -1
  20. package/app/.next/server/app/(dashboard)/dashboard/onboarding/page_client-reference-manifest.js +1 -1
  21. package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  22. package/app/.next/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
  23. package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  24. package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  25. package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  26. package/app/.next/server/app/(dashboard)/dashboard/search-tools/page_client-reference-manifest.js +1 -1
  27. package/app/.next/server/app/(dashboard)/dashboard/settings/page_client-reference-manifest.js +1 -1
  28. package/app/.next/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  29. package/app/.next/server/app/(dashboard)/dashboard/skills/page_client-reference-manifest.js +1 -1
  30. package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
  31. package/app/.next/server/app/400/page_client-reference-manifest.js +1 -1
  32. package/app/.next/server/app/401/page_client-reference-manifest.js +1 -1
  33. package/app/.next/server/app/403/page_client-reference-manifest.js +1 -1
  34. package/app/.next/server/app/408/page_client-reference-manifest.js +1 -1
  35. package/app/.next/server/app/429/page_client-reference-manifest.js +1 -1
  36. package/app/.next/server/app/500/page_client-reference-manifest.js +1 -1
  37. package/app/.next/server/app/502/page_client-reference-manifest.js +1 -1
  38. package/app/.next/server/app/503/page_client-reference-manifest.js +1 -1
  39. package/app/.next/server/app/_global-error.html +1 -1
  40. package/app/.next/server/app/_global-error.rsc +1 -1
  41. package/app/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  42. package/app/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  43. package/app/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  44. package/app/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  45. package/app/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  46. package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  47. package/app/.next/server/app/api/system/version/route.js.nft.json +1 -1
  48. package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  49. package/app/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  50. package/app/.next/server/app/forbidden/page_client-reference-manifest.js +1 -1
  51. package/app/.next/server/app/forgot-password/page_client-reference-manifest.js +1 -1
  52. package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  53. package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
  54. package/app/.next/server/app/maintenance/page_client-reference-manifest.js +1 -1
  55. package/app/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  56. package/app/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/app/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  58. package/app/.next/server/app/status/page_client-reference-manifest.js +1 -1
  59. package/app/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  60. package/app/.next/server/chunks/[root-of-the-server]__02uu1us._.js +1 -1
  61. package/app/.next/server/chunks/[root-of-the-server]__0c721a-._.js +1 -1
  62. package/app/.next/server/chunks/[root-of-the-server]__0ex164m._.js +1 -1
  63. package/app/.next/server/chunks/[root-of-the-server]__0qf7ta~._.js +1 -1
  64. package/app/.next/server/chunks/[root-of-the-server]__0s1dq3.._.js +1 -1
  65. package/app/.next/server/chunks/[root-of-the-server]__0tsl88m._.js +1 -1
  66. package/app/.next/server/chunks/[root-of-the-server]__0x_~cf-._.js +1 -1
  67. package/app/.next/server/chunks/[root-of-the-server]__111mvd5._.js +1 -1
  68. package/app/.next/server/chunks/_00.pgsp._.js +1 -1
  69. package/app/.next/server/chunks/_013gowh._.js +1 -1
  70. package/app/.next/server/chunks/_036lxbr._.js +1 -1
  71. package/app/.next/server/chunks/_05reh6o._.js +1 -1
  72. package/app/.next/server/chunks/_0a3.3sc._.js +1 -1
  73. package/app/.next/server/chunks/_0c.abwr._.js +1 -1
  74. package/app/.next/server/chunks/_0h-j8c2._.js +1 -1
  75. package/app/.next/server/chunks/_0k43pd6._.js +5 -5
  76. package/app/.next/server/chunks/_10.rw9f._.js +1 -1
  77. package/app/.next/server/chunks/src_lib_0j-wze8._.js +1 -1
  78. package/app/.next/server/chunks/src_lib_db_core_ts_0aifyrs._.js +2 -2
  79. package/app/.next/server/chunks/src_lib_db_core_ts_0naqwwu._.js +6 -6
  80. package/app/.next/server/chunks/src_lib_db_core_ts_0~nrtni._.js +6 -6
  81. package/app/.next/server/chunks/ssr/_008ht2n._.js +1 -1
  82. package/app/.next/server/chunks/ssr/_0oo1f90._.js +1 -1
  83. package/app/.next/server/chunks/ssr/src_lib_db_core_ts_0a4mjaw._.js +1 -1
  84. package/app/.next/server/middleware-build-manifest.js +3 -3
  85. package/app/.next/server/pages/500.html +1 -1
  86. package/app/.next/server/server-reference-manifest.js +1 -1
  87. package/app/.next/server/server-reference-manifest.json +1 -1
  88. package/app/.next/static/chunks/{0z77gqz0h9srz.js → 0vau7j_kpzwkc.js} +1 -1
  89. package/app/CHANGELOG.md +17 -0
  90. package/app/README.md +4 -0
  91. package/app/docker-compose.prod.yml +1 -0
  92. package/app/docker-compose.yml +1 -0
  93. package/app/docs/i18n/zh-CN/README.md +4 -0
  94. package/app/docs/openapi.yaml +1 -1
  95. package/app/open-sse/package.json +1 -1
  96. package/app/open-sse/translator/index.ts +9 -0
  97. package/app/open-sse/translator/response/openai-responses.ts +17 -4
  98. package/app/package-lock.json +3 -3
  99. package/app/package.json +1 -1
  100. package/app/src/app/api/restart/route.ts +2 -2
  101. package/app/src/app/api/shutdown/route.ts +1 -1
  102. package/app/src/lib/db/core.ts +36 -5
  103. package/app/src/lib/gracefulShutdown.ts +3 -5
  104. package/app/tests/unit/fixes-p1.test.mjs +76 -0
  105. package/app/tests/unit/responses-translation-fixes.test.mjs +3 -1
  106. package/package.json +1 -1
  107. /package/app/.next/static/{FON4TJvqiYocduWLr9KDp → KbjOFkfScJgP8s3aOWBvF}/_buildManifest.js +0 -0
  108. /package/app/.next/static/{FON4TJvqiYocduWLr9KDp → KbjOFkfScJgP8s3aOWBvF}/_clientMiddlewareManifest.js +0 -0
  109. /package/app/.next/static/{FON4TJvqiYocduWLr9KDp → KbjOFkfScJgP8s3aOWBvF}/_ssgManifest.js +0 -0
package/app/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## [3.4.4] - 2026-04-02
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - **Responses API Token Reporting:** Emit `response.completed` with correct `input_tokens`/`output_tokens` fields for Codex CLI clients, fixing token usage display (#909 — thanks @christopher-s).
12
+ - **SQLite WAL Checkpoint on Shutdown:** Flush WAL changes into the primary database file during graceful shutdown/restart, preventing data loss on Docker container stops (#905 — thanks @rdself).
13
+ - **Graceful Shutdown Signal:** Changed `/api/restart` and `/api/shutdown` routes from `process.exit(0)` to `process.kill(SIGTERM)`, ensuring the shutdown handler runs before exit.
14
+ - **Docker Stop Grace Period:** Added `stop_grace_period: 40s` to Docker Compose files and `--stop-timeout 40` to Docker run examples.
15
+
16
+ ### 🛠️ Maintenance
17
+
18
+ - Closed 5 resolved/not-a-bug issues (#872, #814, #816, #890, #877).
19
+ - Triaged 6 issues with needs-info requests (#892, #887, #886, #865, #895, #870).
20
+ - Responded to CLI detection tracking issue (#863) with contributor guidance.
21
+
22
+ ---
23
+
7
24
  ## [3.4.3] - 2026-04-02
8
25
 
9
26
  ### ✨ New Features
package/app/README.md CHANGED
@@ -979,6 +979,7 @@ OmniRoute is available as a public Docker image on [Docker Hub](https://hub.dock
979
979
  docker run -d \
980
980
  --name omniroute \
981
981
  --restart unless-stopped \
982
+ --stop-timeout 40 \
982
983
  -p 20128:20128 \
983
984
  -v omniroute-data:/app/data \
984
985
  diegosouzapw/omniroute:latest
@@ -993,6 +994,7 @@ cp .env.example .env
993
994
  docker run -d \
994
995
  --name omniroute \
995
996
  --restart unless-stopped \
997
+ --stop-timeout 40 \
996
998
  --env-file .env \
997
999
  -p 20128:20128 \
998
1000
  -v omniroute-data:/app/data \
@@ -1016,6 +1018,8 @@ Notes:
1016
1018
  - Quick Tunnel URLs are temporary and change after every restart.
1017
1019
  - Managed install currently supports Linux, macOS, and Windows on `x64` / `arm64`.
1018
1020
  - Docker images bundle system CA roots and pass them to managed `cloudflared`, which avoids TLS trust failures when the tunnel bootstraps inside the container.
1021
+ - SQLite runs in WAL mode. `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`.
1022
+ - The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40` (or similar) so manual stops do not cut off shutdown cleanup.
1019
1023
  - Set `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` if you want OmniRoute to use an existing binary instead of downloading one.
1020
1024
 
1021
1025
  **Using Docker Compose with Caddy (HTTPS Auto-TLS):**
@@ -19,6 +19,7 @@ services:
19
19
  target: runner-cli
20
20
  image: omniroute:prod
21
21
  restart: unless-stopped
22
+ stop_grace_period: 40s
22
23
  env_file: .env
23
24
  environment:
24
25
  - NODE_ENV=production
@@ -17,6 +17,7 @@
17
17
 
18
18
  x-common: &common
19
19
  restart: unless-stopped
20
+ stop_grace_period: 40s
20
21
  env_file: .env
21
22
  environment:
22
23
  - DATA_DIR=/app/data # Must match the volume mount below
@@ -983,6 +983,7 @@ OmniRoute is available as a public Docker image on [Docker Hub](https://hub.dock
983
983
  docker run -d \
984
984
  --name omniroute \
985
985
  --restart unless-stopped \
986
+ --stop-timeout 40 \
986
987
  -p 20128:20128 \
987
988
  -v omniroute-data:/app/data \
988
989
  diegosouzapw/omniroute:latest
@@ -997,6 +998,7 @@ cp .env.example .env
997
998
  docker run -d \
998
999
  --name omniroute \
999
1000
  --restart unless-stopped \
1001
+ --stop-timeout 40 \
1000
1002
  --env-file .env \
1001
1003
  -p 20128:20128 \
1002
1004
  -v omniroute-data:/app/data \
@@ -1020,6 +1022,8 @@ Notes:
1020
1022
  - Quick Tunnel URLs are temporary and change after every restart.
1021
1023
  - Managed install currently supports Linux, macOS, and Windows on `x64` / `arm64`.
1022
1024
  - Docker images bundle system CA roots and pass them to managed `cloudflared`, which avoids TLS trust failures when the tunnel bootstraps inside the container.
1025
+ - SQLite uses WAL mode. Let `docker stop` finish cleanly so OmniRoute can checkpoint the latest changes back into `storage.sqlite`.
1026
+ - The bundled Compose files already use a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40` (or similar) so manual stops do not interrupt shutdown cleanup.
1023
1027
  - Set `CLOUDFLARED_BIN=/absolute/path/to/cloudflared` if you want OmniRoute to use an existing binary instead of downloading one.
1024
1028
 
1025
1029
  **Using Docker Compose with Caddy (HTTPS Auto-TLS):**
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: OmniRoute API
4
- version: 3.4.3
4
+ version: 3.4.4
5
5
  description: |
6
6
  OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
7
7
  endpoint that routes requests to multiple AI providers with load balancing,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omniroute/open-sse",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -267,6 +267,15 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
267
267
  finalResults.push(...(Array.isArray(converted) ? converted : [converted]));
268
268
  }
269
269
  }
270
+ // Flush: pass null to source-format translator even when Step 1 produced no output.
271
+ // This is critical for formats like openai-responses that emit terminal events
272
+ // (e.g., response.completed with total_tokens) in their flush handler.
273
+ if (chunk === null && results.length === 0) {
274
+ const converted = fromOpenAI(null, state);
275
+ if (converted) {
276
+ finalResults.push(...(Array.isArray(converted) ? converted : [converted]));
277
+ }
278
+ }
270
279
  results = finalResults;
271
280
  }
272
281
  }
@@ -14,11 +14,24 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
14
14
  return flushEvents(state);
15
15
  }
16
16
 
17
- if (!chunk.choices?.length) {
18
- // Capture usage from usage-only chunks (stream_options.include_usage)
19
- if (chunk.usage) {
20
- state.usage = chunk.usage;
17
+ // Capture usage from any chunk that carries it (usage-only chunks OR final chunks with finish_reason)
18
+ // Normalize Chat Completions format (prompt_tokens/completion_tokens) to Responses API format
19
+ // (input_tokens/output_tokens) so response.completed always has the fields Codex expects.
20
+ if (chunk.usage) {
21
+ const u = chunk.usage;
22
+ const input_tokens = u.input_tokens ?? u.prompt_tokens ?? 0;
23
+ const output_tokens = u.output_tokens ?? u.completion_tokens ?? 0;
24
+ state.usage = {
25
+ input_tokens,
26
+ output_tokens,
27
+ total_tokens: u.total_tokens ?? input_tokens + output_tokens,
28
+ };
29
+ if (u.prompt_tokens_details?.cached_tokens) {
30
+ state.usage.input_tokens_details = { cached_tokens: u.prompt_tokens_details.cached_tokens };
21
31
  }
32
+ }
33
+
34
+ if (!chunk.choices?.length) {
22
35
  return [];
23
36
  }
24
37
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omniroute",
9
- "version": "3.4.3",
9
+ "version": "3.4.4",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "workspaces": [
@@ -20907,7 +20907,7 @@
20907
20907
  },
20908
20908
  "open-sse": {
20909
20909
  "name": "@omniroute/open-sse",
20910
- "version": "3.4.3"
20910
+ "version": "3.4.4"
20911
20911
  }
20912
20912
  }
20913
20913
  }
package/app/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
 
3
3
  export async function POST() {
4
- // Graceful restart: exit with code 0 so the process manager (pm2/systemd) restarts
4
+ // Graceful restart: SIGTERM flows through the shutdown handler before the process manager restarts
5
5
  setTimeout(() => {
6
- process.exit(0);
6
+ process.kill(process.pid, "SIGTERM");
7
7
  }, 500);
8
8
 
9
9
  return NextResponse.json({ status: "restarting" });
@@ -4,7 +4,7 @@ export async function POST() {
4
4
  const response = NextResponse.json({ success: true, message: "Shutting down..." });
5
5
 
6
6
  setTimeout(() => {
7
- process.exit(0);
7
+ process.kill(process.pid, "SIGTERM");
8
8
  }, 500);
9
9
 
10
10
  return response;
@@ -12,6 +12,7 @@ import { runMigrations } from "./migrationRunner";
12
12
 
13
13
  type SqliteDatabase = import("better-sqlite3").Database;
14
14
  type JsonRecord = Record<string, unknown>;
15
+ type CheckpointMode = "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE";
15
16
 
16
17
  // ──────────────── Environment Detection ────────────────
17
18
 
@@ -323,6 +324,12 @@ function setDb(db: SqliteDatabase | null): void {
323
324
  }
324
325
  }
325
326
 
327
+ function checkpointDb(db: SqliteDatabase, mode: CheckpointMode = "TRUNCATE"): boolean {
328
+ if (isCloud || isBuildPhase || !SQLITE_FILE) return false;
329
+ db.pragma(`wal_checkpoint(${mode})`);
330
+ return true;
331
+ }
332
+
326
333
  function ensureProviderConnectionsColumns(db: SqliteDatabase) {
327
334
  try {
328
335
  const columns = db.prepare("PRAGMA table_info(provider_connections)").all() as Array<{
@@ -523,15 +530,39 @@ export function getDbInstance(): SqliteDatabase {
523
530
  return db;
524
531
  }
525
532
 
533
+ export function closeDbInstance(options?: { checkpointMode?: CheckpointMode | null }): boolean {
534
+ const db = getDb();
535
+ if (!db) return false;
536
+
537
+ const checkpointMode = options?.checkpointMode ?? "TRUNCATE";
538
+
539
+ try {
540
+ if (checkpointMode) {
541
+ try {
542
+ if (checkpointDb(db, checkpointMode)) {
543
+ console.log(`[DB] SQLite WAL checkpoint completed (${checkpointMode}).`);
544
+ }
545
+ } catch (error: unknown) {
546
+ const message = error instanceof Error ? error.message : String(error);
547
+ console.warn(`[DB] WAL checkpoint failed during close (${checkpointMode}):`, message);
548
+ }
549
+ }
550
+ } finally {
551
+ try {
552
+ if (db.open) db.close();
553
+ } finally {
554
+ setDb(null);
555
+ }
556
+ }
557
+
558
+ return true;
559
+ }
560
+
526
561
  /**
527
562
  * Reset the singleton (used by restore).
528
563
  */
529
564
  export function resetDbInstance() {
530
- const db = getDb();
531
- if (db) {
532
- db.close();
533
- setDb(null);
534
- }
565
+ closeDbInstance();
535
566
  }
536
567
 
537
568
  // ──────────────── JSON → SQLite Migration ────────────────
@@ -96,11 +96,9 @@ async function waitForDrain(): Promise<void> {
96
96
  */
97
97
  async function cleanup(): Promise<void> {
98
98
  try {
99
- const { getDbInstance } = await import("@/lib/db/core");
100
- const db = getDbInstance();
101
- if (db && typeof db.close === "function") {
102
- db.close();
103
- console.log("[Shutdown] SQLite database closed.");
99
+ const { closeDbInstance } = await import("@/lib/db/core");
100
+ if (closeDbInstance()) {
101
+ console.log("[Shutdown] SQLite database checkpointed and closed.");
104
102
  }
105
103
  } catch (err) {
106
104
  console.error("[Shutdown] Error during cleanup:", (err as Error).message);
@@ -19,6 +19,8 @@ const proxyFetch = await import("../../open-sse/utils/proxyFetch.ts");
19
19
  const proxyDispatcher = await import("../../open-sse/utils/proxyDispatcher.ts");
20
20
  const proxySettingsRoute = await import("../../src/app/api/settings/proxy/route.ts");
21
21
  const proxyTestRoute = await import("../../src/app/api/settings/proxy/test/route.ts");
22
+ const shutdownRoute = await import("../../src/app/api/shutdown/route.ts");
23
+ const restartRoute = await import("../../src/app/api/restart/route.ts");
22
24
 
23
25
  async function withEnv(name, value, fn) {
24
26
  const previous = process.env[name];
@@ -141,6 +143,80 @@ test(
141
143
  }
142
144
  );
143
145
 
146
+ test("closeDbInstance checkpoints WAL changes into the primary SQLite file", async () => {
147
+ await resetStorage();
148
+
149
+ const db = core.getDbInstance();
150
+ const now = new Date().toISOString();
151
+ db.prepare(
152
+ "INSERT INTO provider_connections (id, provider, auth_type, name, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
153
+ ).run("checkpoint-test-conn", "openai", "apikey", "checkpoint-test", 1, now, now);
154
+
155
+ core.closeDbInstance();
156
+
157
+ const snapshotPath = path.join(TEST_DATA_DIR, "storage-snapshot.sqlite");
158
+ fs.copyFileSync(core.SQLITE_FILE, snapshotPath);
159
+
160
+ const Database = (await import("better-sqlite3")).default;
161
+ const snapshotDb = new Database(snapshotPath, { readonly: true });
162
+ try {
163
+ const row = snapshotDb
164
+ .prepare("SELECT name FROM provider_connections WHERE id = ?")
165
+ .get("checkpoint-test-conn");
166
+ assert.equal(row?.name, "checkpoint-test");
167
+ } finally {
168
+ snapshotDb.close();
169
+ }
170
+ });
171
+
172
+ test("shutdown route uses SIGTERM for graceful shutdown", async () => {
173
+ const originalKill = process.kill;
174
+ const originalSetTimeout = globalThis.setTimeout;
175
+ const calls = [];
176
+
177
+ process.kill = (pid, signal) => {
178
+ calls.push({ pid, signal });
179
+ return true;
180
+ };
181
+ globalThis.setTimeout = (callback) => {
182
+ callback();
183
+ return 0;
184
+ };
185
+
186
+ try {
187
+ const response = await shutdownRoute.POST();
188
+ assert.equal(response.status, 200);
189
+ assert.deepEqual(calls, [{ pid: process.pid, signal: "SIGTERM" }]);
190
+ } finally {
191
+ process.kill = originalKill;
192
+ globalThis.setTimeout = originalSetTimeout;
193
+ }
194
+ });
195
+
196
+ test("restart route uses SIGTERM for graceful restart", async () => {
197
+ const originalKill = process.kill;
198
+ const originalSetTimeout = globalThis.setTimeout;
199
+ const calls = [];
200
+
201
+ process.kill = (pid, signal) => {
202
+ calls.push({ pid, signal });
203
+ return true;
204
+ };
205
+ globalThis.setTimeout = (callback) => {
206
+ callback();
207
+ return 0;
208
+ };
209
+
210
+ try {
211
+ const response = await restartRoute.POST();
212
+ assert.equal(response.status, 200);
213
+ assert.deepEqual(calls, [{ pid: process.pid, signal: "SIGTERM" }]);
214
+ } finally {
215
+ process.kill = originalKill;
216
+ globalThis.setTimeout = originalSetTimeout;
217
+ }
218
+ });
219
+
144
220
  test("unlinkFileWithRetry retries EBUSY/EPERM and eventually succeeds", async () => {
145
221
  const target = path.join(TEST_DATA_DIR, "retry-target.tmp");
146
222
  fs.writeFileSync(target, "retry-me");
@@ -369,7 +369,9 @@ test("Chat→Responses streaming: usage-only chunk is captured (not dropped)", (
369
369
  const completedEvent = finishEvents.find((e) => e.event === "response.completed");
370
370
  assert.ok(completedEvent, "should have completed event");
371
371
  assert.ok(completedEvent.data.response.usage, "completed event should include usage");
372
- assert.equal(completedEvent.data.response.usage.prompt_tokens, 10);
372
+ assert.equal(completedEvent.data.response.usage.input_tokens, 10);
373
+ assert.equal(completedEvent.data.response.usage.output_tokens, 5);
374
+ assert.equal(completedEvent.data.response.usage.total_tokens, 15);
373
375
  });
374
376
 
375
377
  test("Chat→Responses streaming: completed event includes accumulated output", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omniroute",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
5
5
  "type": "module",
6
6
  "bin": {