mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
committed by
GitHub
parent
dfd83bacd1
commit
7665ad3950
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -13,7 +13,7 @@ body:
|
||||
attributes:
|
||||
label: OmniRoute Version
|
||||
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:
|
||||
required: true
|
||||
|
||||
@@ -44,7 +44,7 @@ body:
|
||||
id: os-version
|
||||
attributes:
|
||||
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:
|
||||
required: false
|
||||
|
||||
@@ -53,7 +53,7 @@ body:
|
||||
attributes:
|
||||
label: Node.js Version
|
||||
description: "Run `node --version`. Skip if using Docker."
|
||||
placeholder: "e.g. 22.12.0"
|
||||
placeholder: "e.g. 24.15.0"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -70,7 +70,7 @@ body:
|
||||
id: model
|
||||
attributes:
|
||||
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:
|
||||
required: false
|
||||
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -27,4 +27,4 @@
|
||||
|
||||
## 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.
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -18,6 +18,7 @@ permissions:
|
||||
env:
|
||||
CI_NODE_VERSION: "24"
|
||||
CI_NODE_24_VERSION: "24"
|
||||
CI_NODE_26_VERSION: "26"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -206,6 +207,30 @@ jobs:
|
||||
- run: npm run build
|
||||
- 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:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
@@ -441,6 +466,7 @@ jobs:
|
||||
- electron-package-smoke
|
||||
- test-unit
|
||||
- node-24-compat
|
||||
- node-26-compat
|
||||
- test-coverage
|
||||
- sonarqube
|
||||
- coverage-pr-comment
|
||||
@@ -494,6 +520,8 @@ jobs:
|
||||
echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|--------|" >> "$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 "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -1519,3 +1519,4 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
<sub><a href="https://omniroute.online">omniroute.online</a></sub>
|
||||
</div>
|
||||
<!-- GitHub Discussions enabled for community Q&A -->
|
||||
|
||||
|
||||
@@ -1121,10 +1121,11 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
models: [
|
||||
{ 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", 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-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.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.2-pro", name: "GPT-5.2 Pro" }, //pro tier only
|
||||
{ id: "gpt-5.2-thinking", name: "GPT-5.2 Thinking" }, //plus ~ tier
|
||||
|
||||
@@ -453,7 +453,10 @@ export function shouldMarkAccountExhaustedFrom429(
|
||||
model: string | null | undefined = null,
|
||||
connectionPassthroughModels?: boolean
|
||||
): boolean {
|
||||
return !hasPerModelQuota(provider, model, connectionPassthroughModels);
|
||||
return (
|
||||
shouldPreserveQuotaSignalsFor429(provider) &&
|
||||
!hasPerModelQuota(provider, model, connectionPassthroughModels)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,6 +54,10 @@ export function stripCloudCodeThinkingConfig(
|
||||
|
||||
if (isRecord(next.request)) {
|
||||
const request = { ...next.request };
|
||||
delete request.reasoning_effort;
|
||||
delete request.reasoning;
|
||||
delete request.thinking;
|
||||
|
||||
if ("generationConfig" in request) {
|
||||
request.generationConfig = stripGeminiThinkingConfig(request.generationConfig);
|
||||
}
|
||||
|
||||
357
package-lock.json
generated
357
package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lowdb": "^7.0.1",
|
||||
"lucide-react": "^1.14.0",
|
||||
< "marked": "^18.0.3",
|
||||
"marked": "^18.0.3",
|
||||
"mermaid": "^11.14.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"next": "^16.2.3",
|
||||
@@ -96,38 +96,12 @@
|
||||
"wtfnode": "^0.10.1"
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
||||
@@ -673,21 +647,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -695,9 +669,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -1737,6 +1711,9 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1753,6 +1730,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1769,6 +1749,9 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1785,6 +1768,9 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1801,6 +1787,9 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1817,6 +1806,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1833,6 +1825,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1849,6 +1844,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1865,6 +1863,9 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1887,6 +1888,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1909,6 +1913,9 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1931,6 +1938,9 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1953,6 +1963,9 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1975,6 +1988,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1997,6 +2013,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2019,6 +2038,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2269,16 +2291,22 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
"@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/@next/env": {
|
||||
@@ -2336,6 +2364,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2352,6 +2383,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2368,6 +2402,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2384,6 +2421,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2549,6 +2589,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2565,6 +2608,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2581,6 +2627,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2597,6 +2646,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2838,6 +2890,9 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2858,6 +2913,9 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2878,6 +2936,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2898,6 +2959,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2918,6 +2982,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2938,6 +3005,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3326,6 +3396,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3343,6 +3416,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3360,6 +3436,9 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3377,6 +3456,9 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3394,6 +3476,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3411,6 +3496,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3454,25 +3542,6 @@
|
||||
"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": {
|
||||
"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",
|
||||
@@ -3634,6 +3703,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3650,6 +3722,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3666,6 +3741,9 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3682,6 +3760,9 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3698,6 +3779,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3714,6 +3798,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3928,6 +4015,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3945,6 +4035,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3962,6 +4055,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3979,6 +4075,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4018,70 +4117,6 @@
|
||||
"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": {
|
||||
"version": "4.2.4",
|
||||
"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": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -5044,6 +5079,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5058,6 +5096,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5072,6 +5113,9 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5086,6 +5130,9 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5100,6 +5147,9 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5114,6 +5164,9 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5128,6 +5181,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5142,6 +5198,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5165,6 +5224,19 @@
|
||||
"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": {
|
||||
"version": "1.11.1",
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"dev": true,
|
||||
>>>>>>> upstream/release/v3.7.9
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^5.1.11",
|
||||
@@ -10850,6 +10918,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10871,6 +10942,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10892,6 +10966,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10913,6 +10990,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11247,15 +11327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
<<<<<<< HEAD
|
||||
"version": "18.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz",
|
||||
"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",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
@@ -11492,7 +11566,6 @@
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"node_modules/mermaid/node_modules/marked": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
|
||||
@@ -11505,8 +11578,6 @@
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
=======
|
||||
>>>>>>> upstream/release/v3.7.9
|
||||
"node_modules/mermaid/node_modules/uuid": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"open-sse"
|
||||
],
|
||||
"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": [
|
||||
"ai",
|
||||
|
||||
@@ -930,14 +930,14 @@ export default function SystemStorageTab() {
|
||||
setPurgeLogsLoading(true);
|
||||
setPurgeLogsStatus({ type: "", message: "" });
|
||||
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();
|
||||
if (res.ok) {
|
||||
setPurgeLogsStatus({
|
||||
type: "success",
|
||||
message:
|
||||
t("logsPurged", { count: data.deletedCount }) ||
|
||||
`Purged ${data.deletedCount} old log entries`,
|
||||
t("logsDeleted", { count: data.deleted }) ||
|
||||
`Purged ${data.deleted} expired log(s)`,
|
||||
});
|
||||
} else {
|
||||
setPurgeLogsStatus({
|
||||
@@ -953,9 +953,9 @@ export default function SystemStorageTab() {
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
delete_forever
|
||||
auto_delete
|
||||
</span>
|
||||
{t("purgeOldLogs") || "Purge Old Logs"}
|
||||
{t("purgeExpiredLogs") || "Purge Expired Logs"}
|
||||
</Button>
|
||||
{purgeLogsStatus.message && (
|
||||
<div
|
||||
@@ -977,247 +977,6 @@ export default function SystemStorageTab() {
|
||||
</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 */}
|
||||
<div className="pt-3 border-t border-border/50">
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap mb-3">
|
||||
|
||||
@@ -64,6 +64,44 @@ function findKeyInsensitive(obj: Record<string, any> | undefined | null, key: st
|
||||
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(
|
||||
pricingByProvider: PricingByProvider,
|
||||
providerAliasMap: Record<string, string>,
|
||||
@@ -105,22 +143,15 @@ function resolveModelPricing(
|
||||
if (pLower === "antigravity") providerPricing = findKeyInsensitive(pricingByProvider, "ag");
|
||||
}
|
||||
|
||||
const normalizedModel = normalizeModelName(model).toLowerCase();
|
||||
const shortModel = normalizedModel; // normalizeModelName behaves exactly like shortModelName
|
||||
const hyphenModel = model.toLowerCase().replace(/\./g, "-");
|
||||
const hyphenNormalized = normalizedModel.replace(/\./g, "-");
|
||||
const lowerModel = model.toLowerCase();
|
||||
const modelCandidates = getPricingModelCandidates(model, normalizeModelName);
|
||||
|
||||
const tryFind = (prov: Record<string, unknown> | null | undefined) => {
|
||||
if (!prov || typeof prov !== "object") return null;
|
||||
return (
|
||||
findKeyInsensitive(prov as Record<string, unknown>, lowerModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, normalizedModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, shortModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, hyphenModel) ||
|
||||
findKeyInsensitive(prov as Record<string, unknown>, hyphenNormalized) ||
|
||||
null
|
||||
);
|
||||
for (const candidate of modelCandidates) {
|
||||
const pricing = findKeyInsensitive(prov as Record<string, unknown>, candidate);
|
||||
if (pricing) return pricing;
|
||||
}
|
||||
return 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)
|
||||
if (!pricing && providerPricing && typeof providerPricing === "object") {
|
||||
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;
|
||||
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 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
|
||||
WHEN (combo_name IS NULL OR combo_name = '')
|
||||
AND requested_model IS NOT NULL
|
||||
AND requested_model != ''
|
||||
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 (combo_name IS NULL OR combo_name = '')
|
||||
THEN 1 ELSE 0 END
|
||||
) as fallbacks
|
||||
FROM call_logs
|
||||
@@ -544,10 +585,11 @@ export async function GET(request: Request) {
|
||||
lastRequest: summaryRow?.lastRequest || "",
|
||||
fallbackCount: Number(fallbackRow?.fallbacks || 0),
|
||||
fallbackRatePct:
|
||||
Number(fallbackRow?.with_requested || 0) > 0
|
||||
Number(fallbackRow?.fallback_eligible || 0) > 0
|
||||
? Number(
|
||||
(
|
||||
(Number(fallbackRow?.fallbacks || 0) / Number(fallbackRow?.with_requested || 1)) *
|
||||
(Number(fallbackRow?.fallbacks || 0) /
|
||||
Number(fallbackRow?.fallback_eligible || 1)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
)
|
||||
|
||||
@@ -196,6 +196,16 @@ export const autoNavSections: AutoGenNavSection[] = [
|
||||
"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)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"title": "API Explorer",
|
||||
@@ -773,5 +802,6 @@ export const autoAllSlugs: string[] = [
|
||||
"coverage-plan",
|
||||
"i18n",
|
||||
"release-checklist",
|
||||
"uninstall"
|
||||
"uninstall",
|
||||
"rfc-auto-assessment"
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
|
||||
|
||||
@@ -61,12 +62,12 @@ export default function Footer() {
|
||||
{/* Resources */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h4 className="font-bold text-white">{t("resources")}</h4>
|
||||
<a
|
||||
<Link
|
||||
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
|
||||
href="/docs"
|
||||
>
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
|
||||
href="https://github.com/diegosouzapw/OmniRoute"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
|
||||
@@ -41,12 +42,12 @@ export default function Navigation() {
|
||||
>
|
||||
{t("howItWorks")}
|
||||
</a>
|
||||
<a
|
||||
<Link
|
||||
className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/docs"
|
||||
>
|
||||
{t("docsLink")}
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
className="text-gray-300 hover:text-white text-sm font-medium transition-colors flex items-center gap-1"
|
||||
href="https://github.com/diegosouzapw/OmniRoute"
|
||||
@@ -98,12 +99,13 @@ export default function Navigation() {
|
||||
>
|
||||
{t("howItWorks")}
|
||||
</a>
|
||||
<a
|
||||
<Link
|
||||
className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
|
||||
href="/docs"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t("docsLink")}
|
||||
</a>
|
||||
</Link>
|
||||
<a
|
||||
className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
|
||||
href="https://github.com/diegosouzapw/OmniRoute"
|
||||
|
||||
@@ -88,6 +88,44 @@ const CRITICAL_DB_TABLES: CriticalTableSpec[] = [
|
||||
{ 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)
|
||||
if (!isCloud && !fs.existsSync(DATA_DIR)) {
|
||||
try {
|
||||
@@ -662,7 +700,7 @@ function captureCriticalDbState(sqliteFile: string): PreservedCriticalDbState {
|
||||
|
||||
let probe: SqliteDatabase | null = null;
|
||||
try {
|
||||
probe = new Database(sqliteFile, { readonly: true });
|
||||
probe = openSqliteDatabase(sqliteFile, { readonly: true });
|
||||
|
||||
for (const tableSpec of CRITICAL_DB_TABLES) {
|
||||
if (!hasTable(probe, tableSpec.table)) continue;
|
||||
@@ -1013,7 +1051,7 @@ export function getDbInstance(): SqliteDatabase {
|
||||
if (isBuildPhase) {
|
||||
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.exec(SCHEMA_SQL);
|
||||
ensureUsageHistoryColumns(memoryDb);
|
||||
@@ -1088,7 +1126,7 @@ export function getDbInstance(): SqliteDatabase {
|
||||
// Uses a single probe connection that becomes the real connection when possible.
|
||||
if (fs.existsSync(sqliteFile)) {
|
||||
try {
|
||||
const probe = new Database(sqliteFile, { readonly: true });
|
||||
const probe = openSqliteDatabase(sqliteFile, { readonly: true });
|
||||
const hasOldSchema = probe
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'")
|
||||
.get();
|
||||
@@ -1109,7 +1147,7 @@ export function getDbInstance(): SqliteDatabase {
|
||||
console.log(
|
||||
`[DB] Old schema_migrations table found but data exists — preserving data (#146)`
|
||||
);
|
||||
const fixDb = new Database(sqliteFile);
|
||||
const fixDb = openSqliteDatabase(sqliteFile);
|
||||
try {
|
||||
fixDb.exec("DROP TABLE IF EXISTS schema_migrations");
|
||||
fixDb.pragma("wal_checkpoint(TRUNCATE)");
|
||||
@@ -1141,12 +1179,7 @@ export function getDbInstance(): SqliteDatabase {
|
||||
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 (
|
||||
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"
|
||||
) {
|
||||
if (isNativeSqliteLoadError(e) || message.includes("could not be found")) {
|
||||
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("busy_timeout = 5000");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
|
||||
@@ -144,9 +144,9 @@ export default function Footer() {
|
||||
© {new Date().getFullYear()} {APP_CONFIG.name} Inc. All rights reserved.
|
||||
</p>
|
||||
<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
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-primary transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
|
||||
@@ -26,11 +26,7 @@ export default function DocsBreadcrumbs({ labels, className }: DocsBreadcrumbsPr
|
||||
aria-label="Breadcrumb"
|
||||
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">
|
||||
>>>>>>> upstream/release/v3.7.9
|
||||
Docs
|
||||
</Link>
|
||||
{crumbs.map((crumb, i) => (
|
||||
|
||||
@@ -14,21 +14,6 @@ export const Default: Story = {
|
||||
tabs: [
|
||||
{
|
||||
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>',
|
||||
},
|
||||
{ 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
|
||||
|
||||
@@ -291,7 +291,8 @@ test("shouldMarkAccountExhaustedFrom429 skips connection poisoning for compatibl
|
||||
shouldMarkAccountExhaustedFrom429("openai-compatible-custom-node", "any-model"),
|
||||
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)", () => {
|
||||
|
||||
@@ -76,6 +76,6 @@ test("AntigravityExecutor.transformRequest keeps Claude bridge output cap and st
|
||||
projectId: "project-1",
|
||||
});
|
||||
|
||||
assert.equal(result.request.generationConfig.maxOutputTokens, 16_384);
|
||||
assert.equal(result.request.generationConfig.thinkingConfig, undefined);
|
||||
assert.equal(result.request.max_tokens, 16_384);
|
||||
assert.equal(result.request.thinking, undefined);
|
||||
});
|
||||
|
||||
@@ -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.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);
|
||||
});
|
||||
|
||||
|
||||
@@ -1092,6 +1092,7 @@ test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catal
|
||||
assert.deepEqual(ids, [
|
||||
"gpt-5.5-pro",
|
||||
"gpt-5.5-thinking",
|
||||
"gpt-5.5",
|
||||
"gpt-5.4-pro",
|
||||
"gpt-5.4-thinking",
|
||||
"gpt-5.4-thinking-mini",
|
||||
|
||||
@@ -14,8 +14,19 @@ import { SEARCH_INDEX } from "../../src/app/docs/lib/searchIndex";
|
||||
// docsNavigation structure
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
test("docsNavigation has 6 sections", () => {
|
||||
assert.equal(docsNavigation.length, 6);
|
||||
test("docsNavigation has expected sections", () => {
|
||||
assert.deepEqual(
|
||||
docsNavigation.map((section) => section.title),
|
||||
[
|
||||
"Getting Started",
|
||||
"Features",
|
||||
"API & Protocols",
|
||||
"Deployment",
|
||||
"Operations",
|
||||
"Development",
|
||||
"Other",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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('<h2'), "h2 tag");
|
||||
assert.ok(html.includes("<h2"), "h2 tag");
|
||||
assert.ok(html.includes("Section"), "h2 text");
|
||||
assert.ok(html.includes('<h3'), "h3 tag");
|
||||
assert.ok(html.includes('<h4'), "h4 tag");
|
||||
assert.ok(html.includes("<h3"), "h3 tag");
|
||||
assert.ok(html.includes("<h4"), "h4 tag");
|
||||
});
|
||||
|
||||
test("renderMarkdown sanitizes XSS content", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
it("should return plaintext when encryption key is not set", async () => {
|
||||
// No STORAGE_ENCRYPTION_KEY set
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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("26.0.0").nodeCompatible, true);
|
||||
assert.deepEqual(getNodeRuntimeSupport("24.1.0"), {
|
||||
nodeVersion: "v24.1.0",
|
||||
nodeCompatible: true,
|
||||
|
||||
@@ -102,6 +102,5 @@ test("T15: openaiToClaudeRequest converts system array content into a Claude sys
|
||||
|
||||
const translated = openaiToClaudeRequest("claude-sonnet-4", request, false);
|
||||
assert.ok(Array.isArray(translated.system));
|
||||
// system[0] is the injected Claude prompt; user-provided system content is system[1].
|
||||
assert.equal(translated.system[1].text, "System rules A\nSystem rules B");
|
||||
assert.equal(translated.system[0].text, "System rules A\nSystem rules B");
|
||||
});
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
]);
|
||||
assert.ok(Array.isArray(translated.system));
|
||||
assert.equal(translated.system[0]?.text?.includes("You are Claude Code"), true);
|
||||
assert.equal(translated.system, undefined);
|
||||
});
|
||||
|
||||
test("CodexExecutor maps fast service tier to priority", () => {
|
||||
|
||||
Reference in New Issue
Block a user