Release v3.7.9 (continued development) (#1982)

* chore: add GPT-5.5 Instant and support Node 26 (#1977)

chore: add GPT-5.5 Instant model to Codex registry + Node 26 support with CI + improved native SQLite error handling. Integrated into release/v3.7.9

* feat: enhance cost formatting and add Codex GPT-5.5 pricing support

* fix formatting

---------

Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: Jan Leon <jan.gaschler@gmail.com>
Co-authored-by: 05dunski <05dunski-kredo@icloud.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-06 01:21:31 -03:00
committed by GitHub
parent dfd83bacd1
commit 7665ad3950
28 changed files with 471 additions and 796 deletions

View File

@@ -13,7 +13,7 @@ body:
attributes: attributes:
label: OmniRoute Version label: OmniRoute Version
description: "Run `omniroute --version` or check the left sidebar in the dashboard." description: "Run `omniroute --version` or check the left sidebar in the dashboard."
placeholder: "e.g. 3.0.9" placeholder: "e.g. 3.7.9"
validations: validations:
required: true required: true
@@ -44,7 +44,7 @@ body:
id: os-version id: os-version
attributes: attributes:
label: OS Version label: OS Version
placeholder: "e.g. Windows 11 23H2, macOS 15.3, Ubuntu 24.04" placeholder: "e.g. Windows 11 25H2, macOS 26.5, Ubuntu 26.04"
validations: validations:
required: false required: false
@@ -53,7 +53,7 @@ body:
attributes: attributes:
label: Node.js Version label: Node.js Version
description: "Run `node --version`. Skip if using Docker." description: "Run `node --version`. Skip if using Docker."
placeholder: "e.g. 22.12.0" placeholder: "e.g. 24.15.0"
validations: validations:
required: false required: false
@@ -70,7 +70,7 @@ body:
id: model id: model
attributes: attributes:
label: Model(s) Involved label: Model(s) Involved
placeholder: "e.g. claude-sonnet-4-20250514, gpt-4o, gemini-2.5-pro" placeholder: "e.g. claude-opus-4-7, gpt-5.5, gemini-3.1-pro"
validations: validations:
required: false required: false

View File

@@ -27,4 +27,4 @@
## Reviewer Notes ## Reviewer Notes
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about. - Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.

View File

@@ -18,6 +18,7 @@ permissions:
env: env:
CI_NODE_VERSION: "24" CI_NODE_VERSION: "24"
CI_NODE_24_VERSION: "24" CI_NODE_24_VERSION: "24"
CI_NODE_26_VERSION: "26"
jobs: jobs:
lint: lint:
@@ -206,6 +207,30 @@ jobs:
- run: npm run build - run: npm run build
- run: node --import tsx/esm --test --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts - run: node --import tsx/esm --test --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
node-26-compat:
name: Node 26 Compatibility (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_26_VERSION }}
cache: npm
- run: npm ci
- run: npm run check:node-runtime
- run: npm run build
- run: node --import tsx/esm --test --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
test-coverage: test-coverage:
name: Coverage name: Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -441,6 +466,7 @@ jobs:
- electron-package-smoke - electron-package-smoke
- test-unit - test-unit
- node-24-compat - node-24-compat
- node-26-compat
- test-coverage - test-coverage
- sonarqube - sonarqube
- coverage-pr-comment - coverage-pr-comment
@@ -494,6 +520,8 @@ jobs:
echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY" echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 24 Compatibility | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1519,3 +1519,4 @@ MIT License - see [LICENSE](LICENSE) for details.
<sub><a href="https://omniroute.online">omniroute.online</a></sub> <sub><a href="https://omniroute.online">omniroute.online</a></sub>
</div> </div>
<!-- GitHub Discussions enabled for community Q&A --> <!-- GitHub Discussions enabled for community Q&A -->

View File

@@ -1121,10 +1121,11 @@ export const REGISTRY: Record<string, RegistryEntry> = {
models: [ models: [
{ id: "gpt-5.5-pro", name: "GPT-5.5 Pro" }, //pro tier only { id: "gpt-5.5-pro", name: "GPT-5.5 Pro" }, //pro tier only
{ id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking" }, //plus, pro tier { id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking" }, //plus, pro tier
{ id: "gpt-5.5", name: "GPT-5.5 Instant" }, //free, plus, pro tier
{ id: "gpt-5.4-pro", name: "GPT-5.4 Pro" }, //pro tier only { id: "gpt-5.4-pro", name: "GPT-5.4 Pro" }, //pro tier only
{ id: "gpt-5.4-thinking", name: "GPT-5.4 Thinking" }, //plus, pro tier { id: "gpt-5.4-thinking", name: "GPT-5.4 Thinking" }, //plus, pro tier
{ id: "gpt-5.4-thinking-mini", name: "GPT-5.4 Thinking Mini" }, //free-login only { id: "gpt-5.4-thinking-mini", name: "GPT-5.4 Thinking Mini" }, //free-login only
{ id: "gpt-5.3", name: "GPT-5.3" }, //free, free-login, plus, pro tier { id: "gpt-5.3", name: "GPT-5.3 Instant" }, //free, free-login, plus, pro tier
{ id: "gpt-5.3-mini", name: "GPT-5.3 Mini" }, //limit fallback { id: "gpt-5.3-mini", name: "GPT-5.3 Mini" }, //limit fallback
{ id: "gpt-5.2-pro", name: "GPT-5.2 Pro" }, //pro tier only { id: "gpt-5.2-pro", name: "GPT-5.2 Pro" }, //pro tier only
{ id: "gpt-5.2-thinking", name: "GPT-5.2 Thinking" }, //plus ~ tier { id: "gpt-5.2-thinking", name: "GPT-5.2 Thinking" }, //plus ~ tier

View File

@@ -453,7 +453,10 @@ export function shouldMarkAccountExhaustedFrom429(
model: string | null | undefined = null, model: string | null | undefined = null,
connectionPassthroughModels?: boolean connectionPassthroughModels?: boolean
): boolean { ): boolean {
return !hasPerModelQuota(provider, model, connectionPassthroughModels); return (
shouldPreserveQuotaSignalsFor429(provider) &&
!hasPerModelQuota(provider, model, connectionPassthroughModels)
);
} }
/** /**

View File

@@ -54,6 +54,10 @@ export function stripCloudCodeThinkingConfig(
if (isRecord(next.request)) { if (isRecord(next.request)) {
const request = { ...next.request }; const request = { ...next.request };
delete request.reasoning_effort;
delete request.reasoning;
delete request.thinking;
if ("generationConfig" in request) { if ("generationConfig" in request) {
request.generationConfig = stripGeminiThinkingConfig(request.generationConfig); request.generationConfig = stripGeminiThinkingConfig(request.generationConfig);
} }

357
package-lock.json generated
View File

@@ -33,7 +33,7 @@
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"lucide-react": "^1.14.0", "lucide-react": "^1.14.0",
< "marked": "^18.0.3", "marked": "^18.0.3",
"mermaid": "^11.14.0", "mermaid": "^11.14.0",
"monaco-editor": "^0.55.1", "monaco-editor": "^0.55.1",
"next": "^16.2.3", "next": "^16.2.3",
@@ -96,38 +96,12 @@
"wtfnode": "^0.10.1" "wtfnode": "^0.10.1"
}, },
"engines": { "engines": {
"node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <25" "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27"
}, },
"optionalDependencies": { "optionalDependencies": {
"keytar": "^7.9.0" "keytar": "^7.9.0"
} }
}, },
"../omniroute-docs-site": {
"name": "docs-site",
"version": "0.1.0",
"extraneous": true,
"dependencies": {
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.4",
"fuse.js": "^7.3.0",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"glob": "^13.0.6",
"gray-matter": "^4.0.3",
"tailwindcss": "^4",
"typescript": "^5"
}
},
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
"version": "4.4.4", "version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
@@ -673,21 +647,21 @@
} }
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.9.1", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/wasi-threads": "1.2.0", "@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.9.1", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -695,9 +669,9 @@
} }
}, },
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1737,6 +1711,9 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1753,6 +1730,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1769,6 +1749,9 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1785,6 +1768,9 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1801,6 +1787,9 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1817,6 +1806,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1833,6 +1825,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1849,6 +1844,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1865,6 +1863,9 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1887,6 +1888,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1909,6 +1913,9 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1931,6 +1938,9 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1953,6 +1963,9 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1975,6 +1988,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1997,6 +2013,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2019,6 +2038,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2269,16 +2291,22 @@
} }
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@emnapi/core": "^1.4.3", "@tybys/wasm-util": "^0.10.1"
"@emnapi/runtime": "^1.4.3", },
"@tybys/wasm-util": "^0.10.0" "funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
@@ -2336,6 +2364,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2352,6 +2383,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2368,6 +2402,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2384,6 +2421,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2549,6 +2589,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2565,6 +2608,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2581,6 +2627,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2597,6 +2646,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2838,6 +2890,9 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2858,6 +2913,9 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2878,6 +2936,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2898,6 +2959,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2918,6 +2982,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2938,6 +3005,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3326,6 +3396,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3343,6 +3416,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3360,6 +3436,9 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3377,6 +3456,9 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3394,6 +3476,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3411,6 +3496,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3454,25 +3542,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.12", "version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
@@ -3634,6 +3703,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3650,6 +3722,9 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3666,6 +3741,9 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3682,6 +3760,9 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3698,6 +3779,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3714,6 +3798,9 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0 AND MIT", "license": "Apache-2.0 AND MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3928,6 +4015,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3945,6 +4035,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3962,6 +4055,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3979,6 +4075,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4018,70 +4117,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.8.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.8.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
@@ -4186,9 +4221,9 @@
} }
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -5044,6 +5079,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5058,6 +5096,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5072,6 +5113,9 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5086,6 +5130,9 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5100,6 +5147,9 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5114,6 +5164,9 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5128,6 +5181,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5142,6 +5198,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -5165,6 +5224,19 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
@@ -10440,10 +10512,6 @@
"version": "29.1.1", "version": "29.1.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
<<<<<<< HEAD
=======
"dev": true,
>>>>>>> upstream/release/v3.7.9
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/css-color": "^5.1.11",
@@ -10850,6 +10918,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -10871,6 +10942,9 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -10892,6 +10966,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -10913,6 +10990,9 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -11247,15 +11327,9 @@
} }
}, },
"node_modules/marked": { "node_modules/marked": {
<<<<<<< HEAD
"version": "18.0.3", "version": "18.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz",
"integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==",
=======
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
>>>>>>> upstream/release/v3.7.9
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
@@ -11492,7 +11566,6 @@
"uuid": "^11.1.0" "uuid": "^11.1.0"
} }
}, },
<<<<<<< HEAD
"node_modules/mermaid/node_modules/marked": { "node_modules/mermaid/node_modules/marked": {
"version": "16.4.2", "version": "16.4.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
@@ -11505,8 +11578,6 @@
"node": ">= 20" "node": ">= 20"
} }
}, },
=======
>>>>>>> upstream/release/v3.7.9
"node_modules/mermaid/node_modules/uuid": { "node_modules/mermaid/node_modules/uuid": {
"version": "11.1.1", "version": "11.1.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",

View File

@@ -36,7 +36,7 @@
"open-sse" "open-sse"
], ],
"engines": { "engines": {
"node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <25" "node": ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27"
}, },
"keywords": [ "keywords": [
"ai", "ai",

View File

@@ -930,14 +930,14 @@ export default function SystemStorageTab() {
setPurgeLogsLoading(true); setPurgeLogsLoading(true);
setPurgeLogsStatus({ type: "", message: "" }); setPurgeLogsStatus({ type: "", message: "" });
try { try {
const res = await fetch("/api/logs/purge", { method: "DELETE" }); const res = await fetch("/api/settings/purge-logs", { method: "POST" });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
setPurgeLogsStatus({ setPurgeLogsStatus({
type: "success", type: "success",
message: message:
t("logsPurged", { count: data.deletedCount }) || t("logsDeleted", { count: data.deleted }) ||
`Purged ${data.deletedCount} old log entries`, `Purged ${data.deleted} expired log(s)`,
}); });
} else { } else {
setPurgeLogsStatus({ setPurgeLogsStatus({
@@ -953,9 +953,9 @@ export default function SystemStorageTab() {
}} }}
> >
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true"> <span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
delete_forever auto_delete
</span> </span>
{t("purgeOldLogs") || "Purge Old Logs"} {t("purgeExpiredLogs") || "Purge Expired Logs"}
</Button> </Button>
{purgeLogsStatus.message && ( {purgeLogsStatus.message && (
<div <div
@@ -977,247 +977,6 @@ export default function SystemStorageTab() {
</div> </div>
</div> </div>
{/* Purge Section */}
<div className="pt-3 border-t border-border/50 mb-4">
<div className="flex items-center gap-2 mb-3">
<span className="material-symbols-outlined text-[18px] text-red-500" aria-hidden="true">
auto_delete
</span>
<p className="font-medium">Purge Data</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div className="flex flex-col gap-2">
<Button
variant="outline"
size="sm"
loading={purgeLogsLoading}
onClick={async () => {
setPurgeLogsLoading(true);
try {
const res = await fetch("/api/db/cleanup/quota-snapshots", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setPurgeLogsStatus({
type: "success",
message: `Purged ${data.deleted} quota snapshots`,
});
} else {
setPurgeLogsStatus({
type: "error",
message: data.error || "Failed to purge quota snapshots",
});
}
} catch (err) {
setPurgeLogsStatus({ type: "error", message: "Error purging quota snapshots" });
} finally {
setPurgeLogsLoading(false);
}
}}
>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
delete
</span>
Purge Quota Snapshots
</Button>
</div>
<div className="flex flex-col gap-2">
<Button
variant="outline"
size="sm"
loading={purgeLogsLoading}
onClick={async () => {
setPurgeLogsLoading(true);
try {
const res = await fetch("/api/db/cleanup/call-logs", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setPurgeLogsStatus({
type: "success",
message: `Purged ${data.deleted} call logs`,
});
} else {
setPurgeLogsStatus({
type: "error",
message: data.error || "Failed to purge call logs",
});
}
} catch (err) {
setPurgeLogsStatus({ type: "error", message: "Error purging call logs" });
} finally {
setPurgeLogsLoading(false);
}
}}
>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
delete
</span>
Purge Call Logs
</Button>
</div>
<div className="flex flex-col gap-2">
<Button
variant="outline"
size="sm"
loading={purgeLogsLoading}
onClick={async () => {
setPurgeLogsLoading(true);
try {
const res = await fetch("/api/db/cleanup/detailed-logs", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setPurgeLogsStatus({
type: "success",
message: `Purged ${data.deleted} detailed logs`,
});
} else {
setPurgeLogsStatus({
type: "error",
message: data.error || "Failed to purge detailed logs",
});
}
} catch (err) {
setPurgeLogsStatus({ type: "error", message: "Error purging detailed logs" });
} finally {
setPurgeLogsLoading(false);
}
}}
>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
delete
</span>
Purge Detailed Logs
</Button>
</div>
</div>
{purgeLogsStatus.message && (
<div
className={`p-3 rounded-lg text-sm ${
purgeLogsStatus.type === "success"
? "bg-green-500/10 text-green-500 border border-green-500/20"
: "bg-red-500/10 text-red-500 border border-red-500/20"
}`}
role="alert"
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
{purgeLogsStatus.type === "success" ? "check_circle" : "error"}
</span>
{purgeLogsStatus.message}
</div>
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2 mb-3">
<Button
variant="outline"
size="sm"
loading={clearCacheLoading}
onClick={async () => {
setClearCacheLoading(true);
setClearCacheStatus({ type: "", message: "" });
try {
const res = await fetch("/api/cache", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setClearCacheStatus({
type: "success",
message: t("cacheCleared") || "Cache cleared successfully",
});
} else {
setClearCacheStatus({
type: "error",
message: data.error || t("clearCacheFailed") || "Failed to clear cache",
});
}
} catch {
setClearCacheStatus({ type: "error", message: t("errorOccurred") });
} finally {
setClearCacheLoading(false);
}
}}
>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
delete_sweep
</span>
{t("clearCache") || "Clear Cache"}
</Button>
<Button
variant="outline"
size="sm"
loading={purgeLogsLoading}
onClick={async () => {
setPurgeLogsLoading(true);
setPurgeLogsStatus({ type: "", message: "" });
try {
const res = await fetch("/api/settings/purge-logs", { method: "POST" });
const data = await res.json();
if (res.ok) {
setPurgeLogsStatus({
type: "success",
message:
t("logsDeleted", { count: data.deleted }) ||
`Purged ${data.deleted} expired log(s)`,
});
} else {
setPurgeLogsStatus({
type: "error",
message: data.error || t("purgeLogsFailed") || "Failed to purge logs",
});
}
} catch {
setPurgeLogsStatus({ type: "error", message: t("errorOccurred") });
} finally {
setPurgeLogsLoading(false);
}
}}
>
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
auto_delete
</span>
{t("purgeExpiredLogs") || "Purge Expired Logs"}
</Button>
</div>
{(clearCacheStatus.message || purgeLogsStatus.message) && (
<div className="flex flex-col gap-2">
{clearCacheStatus.message && (
<div
className={`p-3 rounded-lg text-sm ${
clearCacheStatus.type === "success"
? "bg-green-500/10 text-green-500 border border-green-500/20"
: "bg-red-500/10 text-red-500 border border-red-500/20"
}`}
role="alert"
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
{clearCacheStatus.type === "success" ? "check_circle" : "error"}
</span>
{clearCacheStatus.message}
</div>
</div>
)}
{purgeLogsStatus.message && (
<div
className={`p-3 rounded-lg text-sm ${
purgeLogsStatus.type === "success"
? "bg-green-500/10 text-green-500 border border-green-500/20"
: "bg-red-500/10 text-red-500 border border-red-500/20"
}`}
role="alert"
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
{purgeLogsStatus.type === "success" ? "check_circle" : "error"}
</span>
{purgeLogsStatus.message}
</div>
</div>
)}
</div>
)}
{/* Purge Data section */} {/* Purge Data section */}
<div className="pt-3 border-t border-border/50"> <div className="pt-3 border-t border-border/50">
<div className="flex items-start justify-between gap-3 flex-wrap mb-3"> <div className="flex items-start justify-between gap-3 flex-wrap mb-3">

View File

@@ -64,6 +64,44 @@ function findKeyInsensitive(obj: Record<string, any> | undefined | null, key: st
return obj[key.toLowerCase()]; return obj[key.toLowerCase()];
} }
function uniqueValues(values: Array<string | null | undefined>): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
result.push(normalized);
}
return result;
}
function stripCodexEffortSuffix(model: string): string {
return model.replace(/-(?:xhigh|high|medium|low|none)$/i, "");
}
function getPricingModelCandidates(
model: string,
normalizeModelName: (model: string) => string
): string[] {
const normalizedModel = normalizeModelName(model);
const lowerModel = model.toLowerCase();
const lowerNormalized = normalizedModel.toLowerCase();
const hyphenModel = lowerModel.replace(/\./g, "-");
const hyphenNormalized = lowerNormalized.replace(/\./g, "-");
const effortBaseModel = stripCodexEffortSuffix(lowerNormalized);
return uniqueValues([
lowerModel,
lowerNormalized,
hyphenModel,
hyphenNormalized,
effortBaseModel,
effortBaseModel.replace(/\./g, "-"),
lowerNormalized === "codex-auto-review" ? "gpt-5.5" : null,
]);
}
function resolveModelPricing( function resolveModelPricing(
pricingByProvider: PricingByProvider, pricingByProvider: PricingByProvider,
providerAliasMap: Record<string, string>, providerAliasMap: Record<string, string>,
@@ -105,22 +143,15 @@ function resolveModelPricing(
if (pLower === "antigravity") providerPricing = findKeyInsensitive(pricingByProvider, "ag"); if (pLower === "antigravity") providerPricing = findKeyInsensitive(pricingByProvider, "ag");
} }
const normalizedModel = normalizeModelName(model).toLowerCase(); const modelCandidates = getPricingModelCandidates(model, normalizeModelName);
const shortModel = normalizedModel; // normalizeModelName behaves exactly like shortModelName
const hyphenModel = model.toLowerCase().replace(/\./g, "-");
const hyphenNormalized = normalizedModel.replace(/\./g, "-");
const lowerModel = model.toLowerCase();
const tryFind = (prov: Record<string, unknown> | null | undefined) => { const tryFind = (prov: Record<string, unknown> | null | undefined) => {
if (!prov || typeof prov !== "object") return null; if (!prov || typeof prov !== "object") return null;
return ( for (const candidate of modelCandidates) {
findKeyInsensitive(prov as Record<string, unknown>, lowerModel) || const pricing = findKeyInsensitive(prov as Record<string, unknown>, candidate);
findKeyInsensitive(prov as Record<string, unknown>, normalizedModel) || if (pricing) return pricing;
findKeyInsensitive(prov as Record<string, unknown>, shortModel) || }
findKeyInsensitive(prov as Record<string, unknown>, hyphenModel) || return null;
findKeyInsensitive(prov as Record<string, unknown>, hyphenNormalized) ||
null
);
}; };
let pricing = providerPricing ? tryFind(providerPricing) : null; let pricing = providerPricing ? tryFind(providerPricing) : null;
@@ -139,7 +170,8 @@ function resolveModelPricing(
// Last resort fallback for historical usage (e.g. "gpt-4" missing, matches "gpt-4.1" or first available) // Last resort fallback for historical usage (e.g. "gpt-4" missing, matches "gpt-4.1" or first available)
if (!pricing && providerPricing && typeof providerPricing === "object") { if (!pricing && providerPricing && typeof providerPricing === "object") {
for (const [key, val] of Object.entries(providerPricing as Record<string, unknown>)) { for (const [key, val] of Object.entries(providerPricing as Record<string, unknown>)) {
if (key.includes(lowerModel) || lowerModel.includes(key)) { const lm = model.toLowerCase();
if (key.includes(lm) || lm.includes(key)) {
pricing = val; pricing = val;
break; break;
} }
@@ -506,11 +538,20 @@ export async function GET(request: Request) {
SUM(CASE WHEN (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as total, SUM(CASE WHEN (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as total,
SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' AND (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as with_requested, SUM(CASE WHEN requested_model IS NOT NULL AND requested_model != '' AND (combo_name IS NULL OR combo_name = '') THEN 1 ELSE 0 END) as with_requested,
SUM(CASE SUM(CASE
WHEN requested_model IS NOT NULL WHEN (combo_name IS NULL OR combo_name = '')
AND requested_model IS NOT NULL
AND requested_model != '' AND requested_model != ''
AND model IS NOT NULL AND model IS NOT NULL
AND model != ''
THEN 1 ELSE 0 END
) as fallback_eligible,
SUM(CASE
WHEN (combo_name IS NULL OR combo_name = '')
AND requested_model IS NOT NULL
AND requested_model != ''
AND model IS NOT NULL
AND model != ''
AND LOWER(CASE WHEN instr(requested_model, '/') > 0 THEN substr(requested_model, instr(requested_model, '/') + 1) ELSE requested_model END) != LOWER(model) AND LOWER(CASE WHEN instr(requested_model, '/') > 0 THEN substr(requested_model, instr(requested_model, '/') + 1) ELSE requested_model END) != LOWER(model)
AND (combo_name IS NULL OR combo_name = '')
THEN 1 ELSE 0 END THEN 1 ELSE 0 END
) as fallbacks ) as fallbacks
FROM call_logs FROM call_logs
@@ -544,10 +585,11 @@ export async function GET(request: Request) {
lastRequest: summaryRow?.lastRequest || "", lastRequest: summaryRow?.lastRequest || "",
fallbackCount: Number(fallbackRow?.fallbacks || 0), fallbackCount: Number(fallbackRow?.fallbacks || 0),
fallbackRatePct: fallbackRatePct:
Number(fallbackRow?.with_requested || 0) > 0 Number(fallbackRow?.fallback_eligible || 0) > 0
? Number( ? Number(
( (
(Number(fallbackRow?.fallbacks || 0) / Number(fallbackRow?.with_requested || 1)) * (Number(fallbackRow?.fallbacks || 0) /
Number(fallbackRow?.fallback_eligible || 1)) *
100 100
).toFixed(2) ).toFixed(2)
) )

View File

@@ -196,6 +196,16 @@ export const autoNavSections: AutoGenNavSection[] = [
"fileName": "UNINSTALL.md" "fileName": "UNINSTALL.md"
} }
] ]
},
{
"title": "Other",
"items": [
{
"slug": "rfc-auto-assessment",
"title": "RFC: Auto-Assessment & Self-Healing Combo Engine",
"fileName": "RFC-AUTO-ASSESSMENT.md"
}
]
} }
]; ];
@@ -731,6 +741,25 @@ export const autoSearchIndex: AutoGenSearchItem[] = [
"Source Install (git clone)" "Source Install (git clone)"
] ]
}, },
{
"slug": "rfc-auto-assessment",
"title": "RFC: Auto-Assessment & Self-Healing Combo Engine",
"fileName": "RFC-AUTO-ASSESSMENT.md",
"section": "Other",
"content": "Omniroute's combo system currently requires manual configuration: users must know which providers and models are actually working, then manually wire them into combo chains. When providers fail (rate limits, auth errors, model deprecation), combos silently degrade — routing to dead endpoints that ti",
"headings": [
"Summary",
"Problem Statement",
"What we encountered (real production incident)",
"Current flow (broken)",
"Proposed flow (self-healing)",
"Architecture",
"New Components",
"Detailed Design",
"1. Assessor — src/domain/assessor.ts",
"2. Categorizer — src/domain/categorizer.ts"
]
},
{ {
"slug": "api-explorer", "slug": "api-explorer",
"title": "API Explorer", "title": "API Explorer",
@@ -773,5 +802,6 @@ export const autoAllSlugs: string[] = [
"coverage-plan", "coverage-plan",
"i18n", "i18n",
"release-checklist", "release-checklist",
"uninstall" "uninstall",
"rfc-auto-assessment"
]; ];

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import Link from "next/link";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import OmniRouteLogo from "@/shared/components/OmniRouteLogo"; import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
@@ -61,12 +62,12 @@ export default function Footer() {
{/* Resources */} {/* Resources */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h4 className="font-bold text-white">{t("resources")}</h4> <h4 className="font-bold text-white">{t("resources")}</h4>
<a <Link
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors" className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="/docs" href="/docs"
> >
{t("documentation")} {t("documentation")}
</a> </Link>
<a <a
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors" className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/diegosouzapw/OmniRoute" href="https://github.com/diegosouzapw/OmniRoute"

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import OmniRouteLogo from "@/shared/components/OmniRouteLogo"; import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
@@ -41,12 +42,12 @@ export default function Navigation() {
> >
{t("howItWorks")} {t("howItWorks")}
</a> </a>
<a <Link
className="text-gray-300 hover:text-white text-sm font-medium transition-colors" className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
href="/docs" href="/docs"
> >
{t("docsLink")} {t("docsLink")}
</a> </Link>
<a <a
className="text-gray-300 hover:text-white text-sm font-medium transition-colors flex items-center gap-1" className="text-gray-300 hover:text-white text-sm font-medium transition-colors flex items-center gap-1"
href="https://github.com/diegosouzapw/OmniRoute" href="https://github.com/diegosouzapw/OmniRoute"
@@ -98,12 +99,13 @@ export default function Navigation() {
> >
{t("howItWorks")} {t("howItWorks")}
</a> </a>
<a <Link
className="text-gray-300 hover:text-white text-sm font-medium transition-colors" className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
href="/docs" href="/docs"
onClick={() => setMobileMenuOpen(false)}
> >
{t("docsLink")} {t("docsLink")}
</a> </Link>
<a <a
className="text-gray-300 hover:text-white text-sm font-medium transition-colors" className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
href="https://github.com/diegosouzapw/OmniRoute" href="https://github.com/diegosouzapw/OmniRoute"

View File

@@ -88,6 +88,44 @@ const CRITICAL_DB_TABLES: CriticalTableSpec[] = [
{ table: "webhooks", maxRows: 5_000 }, { table: "webhooks", maxRows: 5_000 },
]; ];
function isNativeSqliteLoadError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return (
message.includes("Module did not self-register") ||
message.includes("NODE_MODULE_VERSION") ||
message.includes("ERR_DLOPEN_FAILED") ||
(error as any)?.code === "ERR_DLOPEN_FAILED"
);
}
function createNativeSqliteLoadError(error: unknown): Error {
const message = error instanceof Error ? error.message : String(error);
const detail =
`better-sqlite3 native binding failed to load for Node.js ${process.version}. ` +
"This usually happens after switching Node.js versions without rebuilding native modules. " +
"Run `npm rebuild better-sqlite3` in the OmniRoute project and start again. " +
`Original error: ${message}`;
const wrapped = new Error(detail);
wrapped.name = "NativeSqliteLoadError";
(wrapped as any).cause = error;
(wrapped as any).code = (error as any)?.code || "ERR_DLOPEN_FAILED";
return wrapped;
}
function openSqliteDatabase(
sqliteFile: string,
options?: ConstructorParameters<typeof Database>[1]
): SqliteDatabase {
try {
return new Database(sqliteFile, options);
} catch (error: unknown) {
if (isNativeSqliteLoadError(error)) {
throw createNativeSqliteLoadError(error);
}
throw error;
}
}
// Ensure data directory exists — with fallback for restricted home directories (#133) // Ensure data directory exists — with fallback for restricted home directories (#133)
if (!isCloud && !fs.existsSync(DATA_DIR)) { if (!isCloud && !fs.existsSync(DATA_DIR)) {
try { try {
@@ -662,7 +700,7 @@ function captureCriticalDbState(sqliteFile: string): PreservedCriticalDbState {
let probe: SqliteDatabase | null = null; let probe: SqliteDatabase | null = null;
try { try {
probe = new Database(sqliteFile, { readonly: true }); probe = openSqliteDatabase(sqliteFile, { readonly: true });
for (const tableSpec of CRITICAL_DB_TABLES) { for (const tableSpec of CRITICAL_DB_TABLES) {
if (!hasTable(probe, tableSpec.table)) continue; if (!hasTable(probe, tableSpec.table)) continue;
@@ -1013,7 +1051,7 @@ export function getDbInstance(): SqliteDatabase {
if (isBuildPhase) { if (isBuildPhase) {
console.log("[DB] Build phase detected — using in-memory SQLite (read-only)"); console.log("[DB] Build phase detected — using in-memory SQLite (read-only)");
} }
const memoryDb = new Database(":memory:"); const memoryDb = openSqliteDatabase(":memory:");
memoryDb.pragma("journal_mode = WAL"); memoryDb.pragma("journal_mode = WAL");
memoryDb.exec(SCHEMA_SQL); memoryDb.exec(SCHEMA_SQL);
ensureUsageHistoryColumns(memoryDb); ensureUsageHistoryColumns(memoryDb);
@@ -1088,7 +1126,7 @@ export function getDbInstance(): SqliteDatabase {
// Uses a single probe connection that becomes the real connection when possible. // Uses a single probe connection that becomes the real connection when possible.
if (fs.existsSync(sqliteFile)) { if (fs.existsSync(sqliteFile)) {
try { try {
const probe = new Database(sqliteFile, { readonly: true }); const probe = openSqliteDatabase(sqliteFile, { readonly: true });
const hasOldSchema = probe const hasOldSchema = probe
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'") .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'")
.get(); .get();
@@ -1109,7 +1147,7 @@ export function getDbInstance(): SqliteDatabase {
console.log( console.log(
`[DB] Old schema_migrations table found but data exists — preserving data (#146)` `[DB] Old schema_migrations table found but data exists — preserving data (#146)`
); );
const fixDb = new Database(sqliteFile); const fixDb = openSqliteDatabase(sqliteFile);
try { try {
fixDb.exec("DROP TABLE IF EXISTS schema_migrations"); fixDb.exec("DROP TABLE IF EXISTS schema_migrations");
fixDb.pragma("wal_checkpoint(TRUNCATE)"); fixDb.pragma("wal_checkpoint(TRUNCATE)");
@@ -1141,12 +1179,7 @@ export function getDbInstance(): SqliteDatabase {
console.warn("[DB] Could not probe existing DB:", message); console.warn("[DB] Could not probe existing DB:", message);
// If the error is a Node module/ABI failure, throw it immediately to avoid renaming the database // If the error is a Node module/ABI failure, throw it immediately to avoid renaming the database
if ( if (isNativeSqliteLoadError(e) || message.includes("could not be found")) {
message.includes("Module did not self-register") ||
message.includes("could not be found") ||
message.includes("ERR_DLOPEN_FAILED") ||
(e as any)?.code === "ERR_DLOPEN_FAILED"
) {
throw e; throw e;
} }
@@ -1180,7 +1213,7 @@ export function getDbInstance(): SqliteDatabase {
} }
} }
const db = new Database(sqliteFile); const db = openSqliteDatabase(sqliteFile);
db.pragma("journal_mode = WAL"); db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000"); db.pragma("busy_timeout = 5000");
db.pragma("synchronous = NORMAL"); db.pragma("synchronous = NORMAL");

View File

@@ -144,9 +144,9 @@ export default function Footer() {
© {new Date().getFullYear()} {APP_CONFIG.name} Inc. All rights reserved. © {new Date().getFullYear()} {APP_CONFIG.name} Inc. All rights reserved.
</p> </p>
<div className="flex gap-6 text-sm text-text-muted"> <div className="flex gap-6 text-sm text-text-muted">
<a href="/docs" className="hover:text-primary transition-colors"> <Link href="/docs" className="hover:text-primary transition-colors">
Documentation Documentation
</a> </Link>
<Link href="/terms" className="hover:text-primary transition-colors"> <Link href="/terms" className="hover:text-primary transition-colors">
Terms Terms
</Link> </Link>

View File

@@ -26,11 +26,7 @@ export default function DocsBreadcrumbs({ labels, className }: DocsBreadcrumbsPr
aria-label="Breadcrumb" aria-label="Breadcrumb"
className={cn("flex items-center gap-2 text-sm text-text-muted mb-6", className)} className={cn("flex items-center gap-2 text-sm text-text-muted mb-6", className)}
> >
<<<<<<< HEAD
<Link href="/docs" className="hover:text-text-main transition-colors">
=======
<Link href="/" className="hover:text-text-main transition-colors"> <Link href="/" className="hover:text-text-main transition-colors">
>>>>>>> upstream/release/v3.7.9
Docs Docs
</Link> </Link>
{crumbs.map((crumb, i) => ( {crumbs.map((crumb, i) => (

View File

@@ -14,21 +14,6 @@ export const Default: Story = {
tabs: [ tabs: [
{ {
label: "Installation", label: "Installation",
<<<<<<< HEAD
content: <div className="p-2">Run npm install to get started.</div>,
},
{
label: "Configuration",
content: <div className="p-2">Edit your config.json file.</div>,
},
{
label: "Usage",
content: <div className="p-2">Import the provider and start routing.</div>,
},
],
},
};
=======
content: '<div className="p-2">Run npm install to get started.</div>', content: '<div className="p-2">Run npm install to get started.</div>',
}, },
{ label: "Configuration", content: '<div className="p-2">Edit your config.json file.</div>' }, { label: "Configuration", content: '<div className="p-2">Edit your config.json file.</div>' },
@@ -39,4 +24,3 @@ export const Default: Story = {
], ],
}, },
}; };
>>>>>>> upstream/release/v3.7.9

View File

@@ -291,7 +291,8 @@ test("shouldMarkAccountExhaustedFrom429 skips connection poisoning for compatibl
shouldMarkAccountExhaustedFrom429("openai-compatible-custom-node", "any-model"), shouldMarkAccountExhaustedFrom429("openai-compatible-custom-node", "any-model"),
false false
); );
assert.equal(shouldMarkAccountExhaustedFrom429("openai", "gpt-4o-mini"), true); assert.equal(shouldMarkAccountExhaustedFrom429("openai", "gpt-4o-mini"), false);
assert.equal(shouldMarkAccountExhaustedFrom429("claude", "claude-sonnet-4-6"), true);
}); });
test("hasPerModelQuota returns true for GitHub Copilot provider (#1624)", () => { test("hasPerModelQuota returns true for GitHub Copilot provider (#1624)", () => {

View File

@@ -76,6 +76,6 @@ test("AntigravityExecutor.transformRequest keeps Claude bridge output cap and st
projectId: "project-1", projectId: "project-1",
}); });
assert.equal(result.request.generationConfig.maxOutputTokens, 16_384); assert.equal(result.request.max_tokens, 16_384);
assert.equal(result.request.generationConfig.thinkingConfig, undefined); assert.equal(result.request.thinking, undefined);
}); });

View File

@@ -840,7 +840,7 @@ test("chatCore strips unsupported reasoning params and caps provider token field
assert.equal(call.body.temperature, undefined); assert.equal(call.body.temperature, undefined);
assert.equal(call.body.presence_penalty, undefined); assert.equal(call.body.presence_penalty, undefined);
assert.equal(call.body.max_tokens, 16384); assert.equal(call.body.max_tokens, undefined);
assert.equal(call.body.max_completion_tokens, 16384); assert.equal(call.body.max_completion_tokens, 16384);
}); });

View File

@@ -1092,6 +1092,7 @@ test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catal
assert.deepEqual(ids, [ assert.deepEqual(ids, [
"gpt-5.5-pro", "gpt-5.5-pro",
"gpt-5.5-thinking", "gpt-5.5-thinking",
"gpt-5.5",
"gpt-5.4-pro", "gpt-5.4-pro",
"gpt-5.4-thinking", "gpt-5.4-thinking",
"gpt-5.4-thinking-mini", "gpt-5.4-thinking-mini",

View File

@@ -14,8 +14,19 @@ import { SEARCH_INDEX } from "../../src/app/docs/lib/searchIndex";
// docsNavigation structure // docsNavigation structure
// ────────────────────────────────────────────── // ──────────────────────────────────────────────
test("docsNavigation has 6 sections", () => { test("docsNavigation has expected sections", () => {
assert.equal(docsNavigation.length, 6); assert.deepEqual(
docsNavigation.map((section) => section.title),
[
"Getting Started",
"Features",
"API & Protocols",
"Deployment",
"Operations",
"Development",
"Other",
]
);
}); });
test("every section has title and items", () => { test("every section has title and items", () => {
@@ -147,12 +158,12 @@ test("extractHeadings strips bold and code from heading text", () => {
test("renderMarkdown converts headings to HTML", () => { test("renderMarkdown converts headings to HTML", () => {
const html = renderMarkdown("# Title\n## Section\n### Subsection\n#### Details"); const html = renderMarkdown("# Title\n## Section\n### Subsection\n#### Details");
assert.ok(html.includes('<h1'), "h1 tag"); assert.ok(html.includes("<h1"), "h1 tag");
assert.ok(html.includes("Title"), "h1 text"); assert.ok(html.includes("Title"), "h1 text");
assert.ok(html.includes('<h2'), "h2 tag"); assert.ok(html.includes("<h2"), "h2 tag");
assert.ok(html.includes("Section"), "h2 text"); assert.ok(html.includes("Section"), "h2 text");
assert.ok(html.includes('<h3'), "h3 tag"); assert.ok(html.includes("<h3"), "h3 tag");
assert.ok(html.includes('<h4'), "h4 tag"); assert.ok(html.includes("<h4"), "h4 tag");
}); });
test("renderMarkdown sanitizes XSS content", () => { test("renderMarkdown sanitizes XSS content", () => {

View File

@@ -66,6 +66,41 @@ describe("encryption module", () => {
}); });
}); });
describe("migrateLegacyEncryptedString() behavior", () => {
it("should upgrade legacy encrypted tokens to static key tokens", async () => {
const secret = "test-secret-key-12345";
const plaintext = "legacy-token";
const legacyEncrypted = createLegacyEncrypted(plaintext, secret);
vi.stubEnv("STORAGE_ENCRYPTION_KEY", secret);
vi.resetModules();
const { migrateLegacyEncryptedString, decrypt } = await import("@/lib/db/encryption");
const result = migrateLegacyEncryptedString(legacyEncrypted);
expect(result.updated).toBe(true);
expect(result.value).not.toBe(legacyEncrypted);
expect(result.value).toMatch(/^enc:v1:/);
const decrypted = decrypt(result.value);
expect(decrypted).toBe(plaintext);
});
it("should NOT upgrade static-key encrypted tokens", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, migrateLegacyEncryptedString } = await import("@/lib/db/encryption");
const plaintext = "modern-token";
const encrypted = encrypt(plaintext);
const result = migrateLegacyEncryptedString(encrypted!);
expect(result.updated).toBe(false);
expect(result.value).toBe(encrypted);
});
});
describe("passthrough mode: no STORAGE_ENCRYPTION_KEY set → plaintext stored", () => { describe("passthrough mode: no STORAGE_ENCRYPTION_KEY set → plaintext stored", () => {
it("should return plaintext when encryption key is not set", async () => { it("should return plaintext when encryption key is not set", async () => {
// No STORAGE_ENCRYPTION_KEY set // No STORAGE_ENCRYPTION_KEY set

View File

@@ -1,327 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createCipheriv, randomBytes, scryptSync, createHash } from "crypto";
// Test helper to manually create legacy-encrypted values
function createLegacyEncrypted(plaintext: string, secret: string): string {
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 16;
const KEY_LENGTH = 32;
const PREFIX = "enc:v1:";
// OLD dynamic salt derivation (the bug)
const dynamicSalt = createHash("sha256").update(secret).digest().slice(0, 16);
const legacyKey = scryptSync(secret, dynamicSalt, KEY_LENGTH);
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, legacyKey, iv);
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
return `${PREFIX}${iv.toString("hex")}:${encrypted}:${authTag}`;
}
describe("encryption module", () => {
beforeEach(() => {
// Clear all env vars and reset modules before each test
vi.unstubAllEnvs();
vi.resetModules();
});
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});
describe("encrypt/decrypt roundtrip with static key (PRIMARY path)", () => {
it("should encrypt and decrypt a value successfully", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, decrypt } = await import("@/lib/db/encryption");
const plaintext = "my-secret-api-key";
const encrypted = encrypt(plaintext);
expect(encrypted).toBeDefined();
expect(encrypted).toMatch(/^enc:v1:/);
expect(encrypted).not.toBe(plaintext);
const decrypted = decrypt(encrypted!);
expect(decrypted).toBe(plaintext);
});
it("should handle multiple encrypt/decrypt cycles", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, decrypt } = await import("@/lib/db/encryption");
const values = ["token1", "token2", "token3"];
const encrypted = values.map((v) => encrypt(v));
const decrypted = encrypted.map((e) => decrypt(e!));
expect(decrypted).toEqual(values);
});
});
describe("migrateLegacyEncryptedString() behavior", () => {
it("should upgrade legacy encrypted tokens to static key tokens", async () => {
const secret = "test-secret-key-12345";
const plaintext = "legacy-token";
const legacyEncrypted = createLegacyEncrypted(plaintext, secret);
vi.stubEnv("STORAGE_ENCRYPTION_KEY", secret);
vi.resetModules();
const { migrateLegacyEncryptedString, decrypt } = await import("@/lib/db/encryption");
const result = migrateLegacyEncryptedString(legacyEncrypted);
expect(result.updated).toBe(true);
expect(result.value).not.toBe(legacyEncrypted);
expect(result.value).toMatch(/^enc:v1:/);
const decrypted = decrypt(result.value);
expect(decrypted).toBe(plaintext);
});
it("should NOT upgrade static-key encrypted tokens", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, migrateLegacyEncryptedString } = await import("@/lib/db/encryption");
const plaintext = "modern-token";
const encrypted = encrypt(plaintext);
const result = migrateLegacyEncryptedString(encrypted!);
expect(result.updated).toBe(false);
expect(result.value).toBe(encrypted);
});
});
describe("passthrough mode: no STORAGE_ENCRYPTION_KEY set → plaintext stored", () => {
it("should return plaintext when encryption key is not set", async () => {
// No STORAGE_ENCRYPTION_KEY set
vi.resetModules();
const { encrypt, decrypt, isEncryptionEnabled } = await import("@/lib/db/encryption");
expect(isEncryptionEnabled()).toBe(false);
const plaintext = "my-api-key";
const encrypted = encrypt(plaintext);
expect(encrypted).toBe(plaintext);
const decrypted = decrypt(plaintext);
expect(decrypted).toBe(plaintext);
});
it("should handle null and undefined in passthrough mode", async () => {
vi.resetModules();
const { encrypt, decrypt } = await import("@/lib/db/encryption");
expect(encrypt(null)).toBeNull();
expect(encrypt(undefined)).toBeUndefined();
expect(decrypt(null)).toBeNull();
expect(decrypt(undefined)).toBeUndefined();
});
});
describe("encryptConnectionFields / decryptConnectionFields helpers", () => {
it("should encrypt all credential fields in a connection object", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encryptConnectionFields } = await import("@/lib/db/encryption");
const conn = {
id: "conn-123",
apiKey: "plain-api-key",
accessToken: "plain-access-token",
refreshToken: "plain-refresh-token",
idToken: "plain-id-token",
};
const encrypted = encryptConnectionFields(conn);
expect(encrypted.id).toBe("conn-123");
expect(encrypted.apiKey).toMatch(/^enc:v1:/);
expect(encrypted.accessToken).toMatch(/^enc:v1:/);
expect(encrypted.refreshToken).toMatch(/^enc:v1:/);
expect(encrypted.idToken).toMatch(/^enc:v1:/);
});
it("should decrypt all credential fields in a connection object", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encryptConnectionFields, decryptConnectionFields } = await import("@/lib/db/encryption");
const conn = {
id: "conn-123",
apiKey: "plain-api-key",
accessToken: "plain-access-token",
refreshToken: "plain-refresh-token",
idToken: "plain-id-token",
};
const encrypted = encryptConnectionFields({ ...conn });
const decrypted = decryptConnectionFields(encrypted);
expect(decrypted.id).toBe("conn-123");
expect(decrypted.apiKey).toBe("plain-api-key");
expect(decrypted.accessToken).toBe("plain-access-token");
expect(decrypted.refreshToken).toBe("plain-refresh-token");
expect(decrypted.idToken).toBe("plain-id-token");
});
});
describe("edge cases: null/undefined inputs, already-encrypted, malformed ciphertext", () => {
it("should handle null and undefined inputs", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, decrypt } = await import("@/lib/db/encryption");
expect(encrypt(null)).toBeNull();
expect(encrypt(undefined)).toBeUndefined();
expect(decrypt(null)).toBeNull();
expect(decrypt(undefined)).toBeUndefined();
});
it("should not double-encrypt already-encrypted values", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt } = await import("@/lib/db/encryption");
const plaintext = "my-secret";
const encrypted = encrypt(plaintext);
expect(encrypted).toMatch(/^enc:v1:/);
// Try to encrypt again
const doubleEncrypted = encrypt(encrypted!);
expect(doubleEncrypted).toBe(encrypted);
});
it("should return null for malformed ciphertext (missing parts)", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { decrypt } = await import("@/lib/db/encryption");
const malformed = "enc:v1:onlyonepart";
const result = decrypt(malformed);
expect(result).toBeNull();
});
it("should return null for malformed ciphertext (invalid hex)", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { decrypt } = await import("@/lib/db/encryption");
const malformed = "enc:v1:notvalidhex:notvalidhex:notvalidhex";
const result = decrypt(malformed);
expect(result).toBeNull();
});
it("should return null for ciphertext with wrong auth tag", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt, decrypt } = await import("@/lib/db/encryption");
const plaintext = "my-secret";
const encrypted = encrypt(plaintext);
// Tamper with the auth tag
const parts = encrypted!.split(":");
parts[parts.length - 1] = "0000000000000000000000000000000000000000000000000000000000000000";
const tampered = parts.join(":");
const result = decrypt(tampered);
expect(result).toBeNull();
});
it("should return plaintext unchanged if not encrypted (legacy plaintext)", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { decrypt } = await import("@/lib/db/encryption");
const plaintext = "not-encrypted-value";
const result = decrypt(plaintext);
expect(result).toBe(plaintext);
});
it("should return null when trying to decrypt without encryption key", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { encrypt } = await import("@/lib/db/encryption");
const plaintext = "my-secret";
const encrypted = encrypt(plaintext);
// Now remove the key and try to decrypt
vi.unstubAllEnvs();
vi.resetModules();
const { decrypt } = await import("@/lib/db/encryption");
const result = decrypt(encrypted!);
expect(result).toBeNull();
});
});
describe("validateEncryptionConfig() with various key states", () => {
it("should return valid when no key is set (passthrough mode)", async () => {
vi.resetModules();
const { validateEncryptionConfig } = await import("@/lib/db/encryption");
const result = validateEncryptionConfig();
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
it("should return valid when a proper key is set", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "test-secret-key-12345");
vi.resetModules();
const { validateEncryptionConfig } = await import("@/lib/db/encryption");
const result = validateEncryptionConfig();
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
it("should return valid when key is empty string (treated as not set)", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", "");
vi.resetModules();
const { validateEncryptionConfig } = await import("@/lib/db/encryption");
const result = validateEncryptionConfig();
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
it("should return invalid when key is whitespace only", async () => {
vi.stubEnv("STORAGE_ENCRYPTION_KEY", " ");
vi.resetModules();
const { validateEncryptionConfig } = await import("@/lib/db/encryption");
const result = validateEncryptionConfig();
expect(result.valid).toBe(false);
expect(result.error).toContain("empty");
});
});
});

View File

@@ -35,6 +35,7 @@ test("getNodeRuntimeSupport accepts patched Node 24, 22 and 20 LTS lines", () =>
}); });
assert.equal(getNodeRuntimeSupport("20.20.2").nodeCompatible, true); assert.equal(getNodeRuntimeSupport("20.20.2").nodeCompatible, true);
assert.equal(getNodeRuntimeSupport("26.0.0").nodeCompatible, true);
assert.deepEqual(getNodeRuntimeSupport("24.1.0"), { assert.deepEqual(getNodeRuntimeSupport("24.1.0"), {
nodeVersion: "v24.1.0", nodeVersion: "v24.1.0",
nodeCompatible: true, nodeCompatible: true,

View File

@@ -102,6 +102,5 @@ test("T15: openaiToClaudeRequest converts system array content into a Claude sys
const translated = openaiToClaudeRequest("claude-sonnet-4", request, false); const translated = openaiToClaudeRequest("claude-sonnet-4", request, false);
assert.ok(Array.isArray(translated.system)); assert.ok(Array.isArray(translated.system));
// system[0] is the injected Claude prompt; user-provided system content is system[1]. assert.equal(translated.system[0].text, "System rules A\nSystem rules B");
assert.equal(translated.system[1].text, "System rules A\nSystem rules B");
}); });

View File

@@ -185,8 +185,7 @@ test("Claude native messages can be round-tripped through OpenAI into Claude OAu
content: [{ type: "text", text: "reply with OK only" }], content: [{ type: "text", text: "reply with OK only" }],
}, },
]); ]);
assert.ok(Array.isArray(translated.system)); assert.equal(translated.system, undefined);
assert.equal(translated.system[0]?.text?.includes("You are Claude Code"), true);
}); });
test("CodexExecutor maps fast service tier to priority", () => { test("CodexExecutor maps fast service tier to priority", () => {