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.
- package/README.md +4 -0
- package/app/.next/BUILD_ID +1 -1
- package/app/.next/build-manifest.json +3 -3
- package/app/.next/prerender-manifest.json +3 -3
- package/app/.next/server/app/(dashboard)/dashboard/agents/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/analytics/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/api-manager/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/audit/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/auto-combo/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/cache/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/costs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/health/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/limits/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/logs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/media/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/memory/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/onboarding/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/playground/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/search-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/settings/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/skills/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/400/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/401/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/403/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/408/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/429/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/500/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/502/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/503/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_global-error.html +1 -1
- package/app/.next/server/app/_global-error.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/system/version/route.js.nft.json +1 -1
- package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/forbidden/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/forgot-password/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/maintenance/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/status/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/terms/page_client-reference-manifest.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__02uu1us._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0c721a-._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0ex164m._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0qf7ta~._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0s1dq3.._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0tsl88m._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__0x_~cf-._.js +1 -1
- package/app/.next/server/chunks/[root-of-the-server]__111mvd5._.js +1 -1
- package/app/.next/server/chunks/_00.pgsp._.js +1 -1
- package/app/.next/server/chunks/_013gowh._.js +1 -1
- package/app/.next/server/chunks/_036lxbr._.js +1 -1
- package/app/.next/server/chunks/_05reh6o._.js +1 -1
- package/app/.next/server/chunks/_0a3.3sc._.js +1 -1
- package/app/.next/server/chunks/_0c.abwr._.js +1 -1
- package/app/.next/server/chunks/_0h-j8c2._.js +1 -1
- package/app/.next/server/chunks/_0k43pd6._.js +5 -5
- package/app/.next/server/chunks/_10.rw9f._.js +1 -1
- package/app/.next/server/chunks/src_lib_0j-wze8._.js +1 -1
- package/app/.next/server/chunks/src_lib_db_core_ts_0aifyrs._.js +2 -2
- package/app/.next/server/chunks/src_lib_db_core_ts_0naqwwu._.js +6 -6
- package/app/.next/server/chunks/src_lib_db_core_ts_0~nrtni._.js +6 -6
- package/app/.next/server/chunks/ssr/_008ht2n._.js +1 -1
- package/app/.next/server/chunks/ssr/_0oo1f90._.js +1 -1
- package/app/.next/server/chunks/ssr/src_lib_db_core_ts_0a4mjaw._.js +1 -1
- package/app/.next/server/middleware-build-manifest.js +3 -3
- package/app/.next/server/pages/500.html +1 -1
- package/app/.next/server/server-reference-manifest.js +1 -1
- package/app/.next/server/server-reference-manifest.json +1 -1
- package/app/.next/static/chunks/{0z77gqz0h9srz.js → 0vau7j_kpzwkc.js} +1 -1
- package/app/CHANGELOG.md +17 -0
- package/app/README.md +4 -0
- package/app/docker-compose.prod.yml +1 -0
- package/app/docker-compose.yml +1 -0
- package/app/docs/i18n/zh-CN/README.md +4 -0
- package/app/docs/openapi.yaml +1 -1
- package/app/open-sse/package.json +1 -1
- package/app/open-sse/translator/index.ts +9 -0
- package/app/open-sse/translator/response/openai-responses.ts +17 -4
- package/app/package-lock.json +3 -3
- package/app/package.json +1 -1
- package/app/src/app/api/restart/route.ts +2 -2
- package/app/src/app/api/shutdown/route.ts +1 -1
- package/app/src/lib/db/core.ts +36 -5
- package/app/src/lib/gracefulShutdown.ts +3 -5
- package/app/tests/unit/fixes-p1.test.mjs +76 -0
- package/app/tests/unit/responses-translation-fixes.test.mjs +3 -1
- package/package.json +1 -1
- /package/app/.next/static/{FON4TJvqiYocduWLr9KDp → KbjOFkfScJgP8s3aOWBvF}/_buildManifest.js +0 -0
- /package/app/.next/static/{FON4TJvqiYocduWLr9KDp → KbjOFkfScJgP8s3aOWBvF}/_clientMiddlewareManifest.js +0 -0
- /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):**
|
package/app/docker-compose.yml
CHANGED
|
@@ -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):**
|
package/app/docs/openapi.yaml
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
package/app/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omniroute",
|
|
3
|
-
"version": "3.4.
|
|
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.
|
|
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.
|
|
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
|
+
"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:
|
|
4
|
+
// Graceful restart: SIGTERM flows through the shutdown handler before the process manager restarts
|
|
5
5
|
setTimeout(() => {
|
|
6
|
-
process.
|
|
6
|
+
process.kill(process.pid, "SIGTERM");
|
|
7
7
|
}, 500);
|
|
8
8
|
|
|
9
9
|
return NextResponse.json({ status: "restarting" });
|
package/app/src/lib/db/core.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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
|
+
"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": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|