Compare commits

...

11 Commits

Author SHA1 Message Date
diegosouzapw ac68022233 feat(release): v2.2.3 — bug fixes from community PRs
Build Electron Desktop App / Validate version (push) Failing after 41s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
fix(google-cli): remove fake projectId fallback causing permission/verification errors (#285)
- antigravity.ts, openai-to-gemini.ts, geminiHelper.ts
- Throws clear error instead of silently sending with random project IDs

fix(claude): extend empty tool name filter to all message roles (#288)
- Pass 1.4 now covers all roles, not just assistant
- Filters tool_result with missing tool_use_id
- Filters top-level body.tools with empty names
- Explicit @swc/helpers COPY in Dockerfile runner-base stage
2026-03-10 12:53:47 -03:00
Nina Gleichner c2b31f6b20 Fix empty tool name 400 errors from Claude API and missing @swc/helpers in Docker (#288)
* Initial plan

* fix: filter empty tool names and missing tool_use_id; add @swc/helpers to Docker

Co-authored-by: ngleichner1 <263653359+ngleichner1@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ngleichner1 <263653359+ngleichner1@users.noreply.github.com>
2026-03-10 12:46:17 -03:00
Jack 54b1d8c8de fix(google-cli): stop random project fallback and require real OAuth projectId (#285)
Co-authored-by: jack <jack@plutus-32g.local>
2026-03-10 12:46:15 -03:00
diegosouzapw cd1ab696b2 docs: add npm run system-info to Support and troubleshooting sections
Users are now directed to run 'npm run system-info' when reporting bugs.
Added to:
- ## Support → '🐛 Reporting a Bug?' subsection
- Pain point #10 '🐛 I can't diagnose errors' bullet list
2026-03-10 12:45:20 -03:00
diegosouzapw d9d0640f6e feat(release): v2.2.2 — system-info script, Kimi/Mistral/Llama aliases
Build Electron Desktop App / Validate version (push) Failing after 28s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
- New npm run system-info command (#280)
- Kimi K2/K2.5 Fireworks path aliases (#265)
- Mistral/Llama short aliases (#278)
- Custom alias instructions via Settings → Model Aliases
2026-03-10 11:00:20 -03:00
Diego Rodrigues de Sa e Souza e19046116a feat: system-info.mjs script + Kimi/Mistral/Llama model aliases (#280 #265 #278) (#284)
System Info Script (#280):
- New scripts/system-info.mjs: collects Node.js version, OmniRoute version,
  OS info, Agent CLI tool versions (iflow, gemini, claude, codex, antigravity,
  droid, openclaw, kilo, cursor, aider), Docker/PM2 status, system packages
- Outputs system-info.txt for easy attachment to bug reports
- Added npm script: 'npm run system-info'

Model Aliases (#265 #278):
- Added Kimi K2/K2.5 Fireworks path aliases to built-in alias map
  (fireworks/accounts/fireworks/models/kimi-k2p5 → moonshotai/Kimi-K2.5)
- Added short aliases: kimi-k2p5, kimi-k2, mistral-large, mistral-small,
  codestral, llama-3.3, llama-3-70b, llama-3-8b
- Custom aliases can be added via Settings → Model Aliases tab in dashboard

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-03-10 10:59:27 -03:00
diegosouzapw 82a621ec08 feat(release): v2.2.1 — bug fixes, security patches, CI hardening
Build Electron Desktop App / Validate version (push) Failing after 30s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
Bug Fixes:
- fix(imageRegistry): add gemini-3.1-flash-image-preview to antigravity (#273)
- fix(models-route): add ollama-cloud to PROVIDER_MODELS_CONFIG (#276)
- fix(models-route): clear 400 error when no apiKey configured (#277)

Security:
- fix(mitm): TLS rejectUnauthorized defaults to true, opt-out via env var
- fix(backup): path traversal guards (safePath, safeProfilePath, safeLogPath)
- fix(usageHistory): prototype pollution via Object.create(null) + hasOwnProperty
- fix(deps): dompurify ^3.3.2 (CVE-2026-0540)
- fix(ci): add global permissions: contents: read to ci.yml

CI:
- fix(ci): @swc/helpers override + regenerated package-lock.json
- fix(ci): npm-publish skip if version already exists on npm
- fix(ci): npm-publish use npm install instead of npm ci
- fix(lint): cursor.ts isToolBoundaryAbort any → unknown
2026-03-10 09:50:19 -03:00
Diego Rodrigues de Sa e Souza ce560ebe9d fix: resolve issues #273, #276, #277 — image routing, models route, missing-key error (#282)
Squash merge PR #282: bug fixes for #273 (Gemini image routing), #276 (Ollama Cloud models), #277 (missing apiKey error), lint fix, and all security code-scanning patches.
2026-03-10 09:48:50 -03:00
diegosouzapw f900a81ec9 fix(ci): use npm install in npm-publish to survive lock file drift on old tags
npm ci fails if the tag commit's lock file is out of sync (as happened
with v2.2.0 when @swc/helpers was missing). npm install is safe here
because the publish workflow only needs deps to run prepublish.mjs —
strict lock enforcement is not required for the publish step.
2026-03-10 09:48:35 -03:00
diegosouzapw 2a620b178d fix(ci): skip npm publish if version already exists on npm registry
Prevents E403 failures when a release event fires more than once for the
same version (e.g. re-running a failed workflow or duplicate tag event).

The publish step now checks whether the version is already on npm and
exits cleanly with a warning instead of failing the workflow.
2026-03-10 09:46:14 -03:00
diegosouzapw 5aaaad529b fix(ci): sync package-lock.json — add @swc/helpers override + regenerate lock
All 3 CI workflows (CI, npm-publish, docker-publish) were failing with:
  'Missing: @swc/helpers@0.5.19 from lock file'

Root cause: npm version bump ran without npm install, causing the lock file
to be out of sync with CI's npm resolver (which resolves @swc/helpers@0.5.19
while local npm 10.9.4 resolves 0.5.15).

Fix: added 'overrides': { '@swc/helpers': '^0.5.19' } to package.json and
regenerated package-lock.json with npm install.

Also updated .agents/workflows/generate-release.md to enforce:
- Always use 'npm version patch --no-git-tag-version' (never minor/major)
- Always run 'npm install' after bumping to keep lock file in sync
- Version threshold: 2.x.10 → 2.(x+1).0 (manual)
2026-03-10 09:25:35 -03:00
24 changed files with 488 additions and 101 deletions
+48 -29
View File
@@ -6,61 +6,79 @@ description: Create a new release, bump version up to 1.x.10 threshold, update c
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
> NEVER use `npm version minor` or `npm version major`.
> Always use: `npm version patch --no-git-tag-version`
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
## Steps
### 1. Determine new version
Check current version in `package.json` and increment the patch number:
Check current version in `package.json` and increment the **patch** number only:
```bash
grep '"version"' package.json
```
Version format: `1.x.y`increment `y` for patch, `x` for minor (threshold: y=10 triggers x+1).
Version format: `2.x.y`examples:
### 2. Finalize CHANGELOG.md
Replace `[Unreleased]` header with the new version and date:
```markdown
## [1.x.y] — YYYY-MM-DD
```
### 3. Bump version in package.json
- `2.1.2``2.1.3` (patch)
- `2.1.9``2.1.10` (patch)
- `2.1.10``2.2.0` (minor threshold — do manually with `sed`)
```bash
sed -i 's/"version": "OLD"/"version": "NEW"/' package.json
# ALWAYS use patch:
npm version patch --no-git-tag-version
```
### 4. Stage, commit, and tag
### 2. Regenerate lock file (REQUIRED after version bump)
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
```bash
npm install
```
### 3. Finalize CHANGELOG.md
Replace `[Unreleased]` header with the new version and date.
Keep an empty `## [Unreleased]` section above it.
```markdown
## [Unreleased]
---
## [2.x.y] — YYYY-MM-DD
```
### 4. Update openapi.yaml version
```bash
sed -i 's/version: OLD/version: NEW/' docs/openapi.yaml
```
### 5. Stage, commit, and tag
// turbo-all
```bash
git add -A
git commit -m "feat(release): vX.Y.Z — summary of changes"
git tag -a vX.Y.Z -m "Release vX.Y.Z — summary"
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
git commit -m "chore(release): v2.x.y — summary of changes"
git tag -a v2.x.y -m "Release v2.x.y"
```
### 5. Push to GitHub
### 6. Push to GitHub
```bash
git push origin main
git push origin vX.Y.Z
git push origin main --tags
```
### 6. Publish to npm
```bash
npm publish
```
Wait for completion (prepublishOnly runs `npm run build:cli` automatically).
### 7. Create GitHub release
```bash
gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_notes.md
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
```
### 8. Deploy to VPS (if requested)
@@ -68,7 +86,7 @@ gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_note
See `/deploy-vps` workflow for Akamai VPS or use npm for local VPS:
```bash
ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
ssh root@<VPS_IP> "npm install -g omniroute@2.x.y && pm2 restart omniroute"
```
## Notes
@@ -76,3 +94,4 @@ ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
- After npm publish, verify with `npm info omniroute version`
- Lock file sync errors are caused by skipping `npm install` after version bump
+3
View File
@@ -10,6 +10,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
+9 -2
View File
@@ -23,7 +23,7 @@ jobs:
registry-url: https://registry.npmjs.org
- name: Install dependencies (skip scripts to avoid heavy build)
run: npm ci --ignore-scripts
run: npm install --ignore-scripts --no-audit --no-fund
- name: Sync version from release tag
run: |
@@ -39,6 +39,13 @@ jobs:
run: node scripts/prepublish.mjs
- name: Publish to npm
run: npm publish --access public
run: |
VERSION=$(node -p "require('./package.json').version")
# Check if this version is already published — skip instead of failing with E403
if npm view "omniroute@${VERSION}" version --silent 2>/dev/null | grep -q "^${VERSION}$"; then
echo "️⚠️ Version ${VERSION} is already published on npm — skipping."
exit 0
fi
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-1
View File
@@ -102,7 +102,6 @@ cloud/
security-analysis/
# Deploy workflow (contains sensitive VPS credentials)
.agent/workflows/deploy.md
clipr/
app.log
*.tgz
+53 -1
View File
@@ -7,7 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [Unreleased]
## [2.2.3] — 2026-03-10
> ### 🐛 Bug Fixes · 🔧 Reliability
### Bug Fixes
- **Antigravity/Gemini CLI: remove fake projectId fallback (#285)** — OmniRoute was generating random fallback project IDs (e.g. `useful-fuze-a04c5`) when OAuth credentials lacked a real GCP `projectId`. This caused confusing `Permission denied on resource project` and `Verify your account` errors from Google. Now throws a clear actionable error: _reconnect OAuth so OmniRoute can load your real Cloud Code project_. Affects `antigravity.ts`, `openai-to-gemini.ts`, `geminiHelper.ts`.
- **Claude Code: filter empty-named tool_use blocks across all message roles (#288)** — Pass 1.4 only filtered empty tool names from `assistant` messages. Extended to all roles (user, system). Also filters `tool_result` blocks missing `tool_use_id`, and top-level `body.tools` declarations with empty names. Prevents `Invalid input[x].name: empty string` 400 errors from Claude API.
- **Docker: explicit @swc/helpers copy (#288)** — Added `COPY --from=builder /app/node_modules/@swc/helpers` to Dockerfile `runner-base` stage. The standalone tracer doesn't always include this package, causing runtime `MODULE_NOT_FOUND` crashloops.
---
## [2.2.2] — 2026-03-10
> ### ✨ New Features · 🔀 Model Aliases
### New Features
- **system-info.mjs (#280)** — New `npm run system-info` command that collects Node.js version, OmniRoute version, OS info, CLI tool versions (iflow, gemini, claude, codex, antigravity, droid, openclaw, kilo, cursor, aider), Docker/PM2 status, and system packages. Outputs `system-info.txt` for easy attachment to bug reports.
### Model Aliases
- **Kimi K2/K2.5 Fireworks aliases (#265)** — Built-in aliases added: `fireworks/accounts/fireworks/models/kimi-k2p5` and `kimi-k2p5``moonshotai/Kimi-K2.5`; same for `kimi-k2``moonshotai/Kimi-K2`. Fireworks long path model names now auto-resolve.
- **Mistral short aliases (#278)** — `mistral-large``mistral-large-latest`, `mistral-small``mistral-small-latest`, `codestral``codestral-latest`.
- **Llama short aliases** — `llama-3.3``llama-3.3-70b-versatile`, `llama-3-70b``llama-3.3-70b-versatile`, `llama-3-8b``llama3-8b-8192`.
- **Custom aliases** — Users can define their own aliases in **Settings → Model Aliases** tab. Example: `gpt-5.4``cx/gpt-5.4`.
---
## [2.2.1] — 2026-03-10
> ### 🐛 Bug Fixes · 🔐 Security · 🔧 CI
### Bug Fixes
- **Gemini image routing (#273)** — `gemini-3.1-flash-image-preview` was missing from the `antigravity` image provider registry in `imageRegistry.ts`, causing image generation to fall through to the chat handler. Added alongside `gemini-2.5-flash-preview-image-generation`.
- **Ollama Cloud model listing (#276)** — `ollama-cloud` was absent from `PROVIDER_MODELS_CONFIG` in the models route, causing 400 errors when listing models from `api.ollama.com`. Entry added.
- **Missing apiKey error clarity (#277)** — When login is disabled and a provider has no API key configured, the model import route now returns `400` with a clear message instead of a generic `401 Unauthorized`.
### Security
- **TLS validation re-enabled (GHSA-50)** — `mitm/server.ts`: `rejectUnauthorized` now defaults to `true`. Opt-out only via `MITM_DISABLE_TLS_VERIFY=1`.
- **Path traversal hardening (GHSA-4149)** — Added `safePath()`, `safeProfilePath()`, `safeLogPath()` helpers across `backupService.ts`, `db/backup.ts`, `codex-profiles/route.ts`, and `mitm/server.ts`. All user-supplied IDs/filenames are now anchored within their allowed directories using `path.resolve()` + bounds check.
- **Prototype pollution fix (GHSA-1820)** — `usageHistory.ts`: `pendingRequests` maps now use `Object.create(null)` + `hasOwnProperty` guards, preventing `__proto__` / `constructor` injection via crafted provider IDs.
- **Dependency: dompurify updated to ^3.3.2** — Resolves CVE-2026-0540 (XSS in rendered HTML).
- **GitHub Actions: added `permissions: contents: read`** — Prevents token over-permission in CI jobs.
### CI
- **Lock file sync** — Added `@swc/helpers: "^0.5.19"` override in `package.json`; regenerated `package-lock.json`. Fixes `npm ci` failures across `ci.yml` and `docker-publish.yml`.
- **npm-publish: skip if version exists** — Workflow now checks registry before publishing; exits cleanly with a warning instead of failing with `E403` if the version is already on npm.
- **npm-publish: use `npm install` instead of `npm ci`** — Prevents publish failures when a tag commit's lock file is slightly out of sync.
- **Lint: `cursor.ts` any-budget** — Replaced `any` with `unknown` + type narrowing in `isToolBoundaryAbort()`.
---
+2
View File
@@ -29,6 +29,8 @@ RUN mkdir -p /app/data
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/standalone ./
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs
COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs
COPY --from=builder /app/scripts/healthcheck.mjs ./healthcheck.mjs
+11
View File
@@ -167,6 +167,16 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway f
- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md), open a PR, or pick a `good first issue`
- **Original Project**: [9router by decolua](https://github.com/decolua/9router)
### 🐛 Reporting a Bug?
When opening an issue, please run the system-info command and attach the generated file:
```bash
npm run system-info
```
This generates a `system-info.txt` with your Node.js version, OmniRoute version, OS details, installed CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages — everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue.
---
## 🔄 How It Works
@@ -358,6 +368,7 @@ When a call fails, the dev doesn't know if it was a rate limit, expired token, w
- **Translator Playground** — 4 debugging modes: Playground (format translation), Chat Tester (round-trip), Test Bench (batch), Live Monitor (real-time)
- **Request Telemetry** — p50/p95/p99 latency + X-Request-Id tracing
- **File-Based Logging with Rotation** — Console interceptor captures everything to JSON log with size-based rotation
- **System Info Report**`npm run system-info` generates `system-info.txt` with your full environment (Node version, OmniRoute version, OS, CLI tools, Docker/PM2 status). Attach it when reporting issues for instant triage.
</details>
+4 -1
View File
@@ -63,7 +63,10 @@ export const IMAGE_PROVIDERS = {
authType: "oauth",
authHeader: "bearer",
format: "gemini-image", // Special format: uses Gemini generateContent API
models: [{ id: "gemini-2.5-flash-preview-image-generation", name: "Nano Banana" }],
models: [
{ id: "gemini-2.5-flash-preview-image-generation", name: "Gemini 2.5 Flash Image" },
{ id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image Preview" },
],
supportedSizes: ["1024x1024"],
},
+4 -13
View File
@@ -38,14 +38,11 @@ export class AntigravityExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
const bodyProjectId = body?.project;
const credentialsProjectId = credentials?.projectId;
const hasExplicitProject = !!(bodyProjectId || credentialsProjectId);
const projectId = bodyProjectId || credentialsProjectId || this.generateProjectId();
const projectId = bodyProjectId || credentialsProjectId;
if (!hasExplicitProject) {
console.warn(
`[Antigravity] ⚠️ No projectId provided via body or credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors if the account has no active GCP project. ` +
`Ensure the OAuth token includes a valid project or the request includes a project field.`
if (!projectId) {
throw new Error(
"Missing Google projectId for Antigravity account. Please reconnect OAuth so OmniRoute can fetch your real Cloud Code project (loadCodeAssist)."
);
}
@@ -128,12 +125,6 @@ export class AntigravityExecutor extends BaseExecutor {
}
}
generateProjectId() {
const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)];
const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)];
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
generateSessionId() {
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
}
+10 -5
View File
@@ -148,12 +148,17 @@ function parseCursorJsonErrorFrame(text: string) {
}
}
function isToolBoundaryAbort(jsonError: any, toolCallCount: number) {
function isToolBoundaryAbort(jsonError: unknown, toolCallCount: number) {
if (!jsonError || toolCallCount <= 0) return false;
const code = jsonError?.error?.code || "";
const debugError = jsonError?.error?.details?.[0]?.debug?.error || "";
const title = jsonError?.error?.details?.[0]?.debug?.details?.title || "";
const detail = jsonError?.error?.details?.[0]?.debug?.details?.detail || "";
const e = jsonError as Record<string, unknown>;
const err = e?.error as Record<string, unknown> | undefined;
const details = (err?.details as Record<string, unknown>[] | undefined)?.[0];
const debug = details?.debug as Record<string, unknown> | undefined;
const debugDetails = debug?.details as Record<string, unknown> | undefined;
const code = (err?.code as string) || "";
const debugError = (debug?.error as string) || "";
const title = (debugDetails?.title as string) || "";
const detail = (debugDetails?.detail as string) || "";
const message = `${title} ${detail}`.toLowerCase();
const isAbortedCode = code === "aborted" || debugError === "ERROR_USER_ABORTED_REQUEST";
return isAbortedCode && message.includes("tool call ended before result was received");
+18
View File
@@ -31,6 +31,24 @@ const BUILT_IN_ALIASES: Record<string, string> = {
"gpt-4-0125-preview": "gpt-4-turbo",
"gpt-4-1106-preview": "gpt-4-turbo",
"gpt-3.5-turbo-0125": "gpt-3.5-turbo",
// Kimi/Moonshot — Fireworks long-path aliases (#265)
"accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
"fireworks/accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
"kimi-k2p5": "moonshotai/Kimi-K2.5",
"accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
"fireworks/accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
"kimi-k2": "moonshotai/Kimi-K2",
// Mistral short aliases
"mistral-large": "mistral-large-latest",
"mistral-small": "mistral-small-latest",
codestral: "codestral-latest",
// Llama short aliases
"llama-3.3": "llama-3.3-70b-versatile",
"llama-3-70b": "llama-3.3-70b-versatile",
"llama-3-8b": "llama3-8b-8192",
};
// ── Custom Aliases (persisted via Settings API) ─────────────────────────────
+12 -2
View File
@@ -127,14 +127,24 @@ export function prepareClaudeRequest(body, provider = null) {
}
// Pass 1.4: Filter out tool_use blocks with empty names (causes Claude 400 error)
// Apply to ALL roles (assistant tool_use + any user messages that may carry tool_use)
// Also filter tool_result blocks with missing tool_use_id
for (const msg of filtered) {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
if (Array.isArray(msg.content)) {
msg.content = msg.content.filter(
(block) => block.type !== "tool_use" || (block.name && block.name.trim())
(block) => block.type !== "tool_use" || (block.name && block.name?.trim())
);
msg.content = msg.content.filter(
(block) => block.type !== "tool_result" || block.tool_use_id
);
}
}
// Also filter top-level tool declarations with empty names
if (body.tools && Array.isArray(body.tools)) {
body.tools = body.tools.filter((tool) => tool.name && tool.name?.trim());
}
// Pass 1.5: Fix tool_use/tool_result ordering
// Each tool_use must have tool_result in the NEXT message (not same message with other content)
filtered = fixToolUseOrdering(filtered);
@@ -126,15 +126,6 @@ export function generateSessionId() {
return `-${Math.floor(Math.random() * 9000000000000000000)}`;
}
// Generate project ID
export function generateProjectId() {
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
const nouns = ["fuze", "wave", "spark", "flow", "core"];
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
// Helper: Remove unsupported keywords recursively from object/array
function removeUnsupportedKeywords(obj, keywords) {
if (!obj || typeof obj !== "object") return;
@@ -175,6 +175,9 @@ export function openaiToClaudeRequest(model, body, stream) {
};
});
// Filter out tools with empty names (would cause Claude 400 error)
result.tools = result.tools.filter((tool) => tool.name && tool.name?.trim());
// Add cache_control to last tool that doesn't have defer_loading
// Tools with defer_loading=true cannot have cache_control (API rejects it)
for (let i = result.tools.length - 1; i >= 0; i--) {
@@ -227,6 +230,8 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr
if (part.type === "text" && part.text) {
blocks.push({ type: "text", text: part.text });
} else if (part.type === "tool_result") {
// Skip tool_result with no tool_use_id (would be useless and may cause errors)
if (!part.tool_use_id) continue;
blocks.push({
type: "tool_result",
tool_use_id: part.tool_use_id,
@@ -15,7 +15,6 @@ import {
tryParseJSON,
generateRequestId,
generateSessionId,
generateProjectId,
cleanJSONSchemaForAntigravity,
} from "../helpers/geminiHelper.ts";
@@ -321,13 +320,11 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
// Wrap Gemini CLI format in Cloud Code wrapper
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
const hasRealProject = !!credentials?.projectId;
const projectId = credentials?.projectId || generateProjectId();
const projectId = credentials?.projectId;
if (!hasRealProject) {
console.warn(
`[${isAntigravity ? "Antigravity" : "GeminiCLI"}] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
if (!projectId) {
throw new Error(
`${isAntigravity ? "Antigravity" : "GeminiCLI"} account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests.`
);
}
@@ -374,13 +371,11 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
}
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
const hasRealProject = !!credentials?.projectId;
const projectId = credentials?.projectId || generateProjectId();
const projectId = credentials?.projectId;
if (!hasRealProject) {
console.warn(
`[Antigravity/Claude] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
if (!projectId) {
throw new Error(
"Antigravity/Claude account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests."
);
}
+33 -8
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "2.2.0",
"version": "2.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "2.2.0",
"version": "2.2.3",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -18,6 +18,7 @@
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.2",
"bottleneck": "^2.19.5",
"dompurify": "^3.3.2",
"express": "^5.2.1",
"fetch-socks": "^1.3.2",
"http-proxy-middleware": "^3.0.5",
@@ -2985,9 +2986,9 @@
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
@@ -5674,10 +5675,13 @@
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -6864,6 +6868,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -8773,6 +8778,15 @@
"marked": "14.0.0"
}
},
"node_modules/monaco-editor/node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -8964,6 +8978,17 @@
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+7 -2
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.2.0",
"version": "2.2.3",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
@@ -76,7 +76,8 @@
"check": "npm run lint && npm run test",
"prepublishOnly": "npm run build:cli",
"postinstall": "node scripts/postinstall.mjs",
"prepare": "husky"
"prepare": "husky",
"system-info": "node scripts/system-info.mjs"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
@@ -84,6 +85,7 @@
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.2",
"bottleneck": "^2.19.5",
"dompurify": "^3.3.2",
"express": "^5.2.1",
"fetch-socks": "^1.3.2",
"http-proxy-middleware": "^3.0.5",
@@ -138,5 +140,8 @@
"*.{json,md,yml,yaml,css}": [
"prettier --write"
]
},
"overrides": {
"@swc/helpers": "^0.5.19"
}
}
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env node
/**
* system-info.mjs OmniRoute System Information Reporter (#280)
*
* Collects system/environment info for bug reports.
* Usage: node scripts/system-info.mjs [--output system-info.txt]
*
* Output includes:
* - Node.js version
* - OmniRoute version
* - OS info
* - Relevant system packages (if apt available)
* - Agent CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.)
* - Docker / PM2 status
*/
import { execSync } from "child_process";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import os from "os";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
// ── Helpers ────────────────────────────────────────────────────────────────
function run(cmd, fallback = "N/A") {
try {
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
} catch {
return fallback;
}
}
function toolVersion(cmd, args = "--version") {
const version = run(`${cmd} ${args}`, null);
if (version === null) return "not installed";
// Trim to first line, remove prefixes like "v", "Version: "
return version
.split("\n")[0]
.replace(/^(version\s*:?\s*|v)/i, "")
.trim();
}
function section(title) {
const line = "─".repeat(60);
return `\n${line}\n ${title}\n${line}\n`;
}
// ── Collect Info ──────────────────────────────────────────────────────────
const lines = [];
lines.push("OmniRoute System Information Report");
lines.push(`Generated: ${new Date().toISOString()}`);
// ── Node.js & Runtime ────────────────────────────────────────────────────
lines.push(section("Node.js & Runtime"));
lines.push(`Node.js: ${process.version}`);
lines.push(`npm: v${run("npm --version")}`);
lines.push(`Platform: ${process.platform} (${process.arch})`);
lines.push(`OS: ${os.type()} ${os.release()} (${os.arch()})`);
lines.push(`Hostname: ${os.hostname()}`);
lines.push(`CPUs: ${os.cpus().length}x ${os.cpus()[0]?.model || "unknown"}`);
lines.push(`Total RAM: ${Math.round(os.totalmem() / 1024 / 1024)} MB`);
lines.push(`Free RAM: ${Math.round(os.freemem() / 1024 / 1024)} MB`);
// ── OmniRoute Version ────────────────────────────────────────────────────
lines.push(section("OmniRoute"));
try {
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
lines.push(`Version: ${pkg.version}`);
lines.push(`Name: ${pkg.name}`);
} catch {
lines.push("Version: unable to read package.json");
}
const installedGlobal = run("npm list -g omniroute --depth=0 2>/dev/null | grep omniroute");
lines.push(`Global npm: ${installedGlobal || "not installed globally"}`);
const pm2Status = run("pm2 list 2>/dev/null | grep omniroute | awk '{print $4, $10, $12}'");
lines.push(`PM2 status: ${pm2Status || "not running via PM2"}`);
// ── Agent CLI Tools ──────────────────────────────────────────────────────
lines.push(section("Agent CLI Tools"));
const cliTools = [
{ name: "iflow-cli", cmd: "iflow", args: "--version" },
{ name: "gemini-cli", cmd: "gemini", args: "--version" },
{ name: "claude-code", cmd: "claude", args: "--version" },
{ name: "openai-codex", cmd: "codex", args: "--version" },
{ name: "antigravity", cmd: "antigravity", args: "--version" },
{ name: "droid", cmd: "droid", args: "--version" },
{ name: "openclaw", cmd: "openclaw", args: "--version" },
{ name: "kilo", cmd: "kilo", args: "--version" },
{ name: "cursor", cmd: "cursor", args: "--version" },
{ name: "aider", cmd: "aider", args: "--version" },
];
for (const { name, cmd, args } of cliTools) {
const v = toolVersion(cmd, args);
lines.push(`${name.padEnd(20)} ${v}`);
}
// ── Docker ───────────────────────────────────────────────────────────────
lines.push(section("Docker"));
lines.push(`Docker: ${run("docker --version", "not installed")}`);
const dockerContainers = run(
"docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null",
"N/A"
);
lines.push(`Containers:\n${dockerContainers}`);
// ── System Packages ──────────────────────────────────────────────────────
lines.push(section("System Packages (relevant)"));
const relevantPkgs = ["build-essential", "libssl-dev", "openssl", "libsqlite3-dev", "python3"];
for (const pkg of relevantPkgs) {
const ver = run(`dpkg -l ${pkg} 2>/dev/null | grep '^ii' | awk '{print $3}'`, "not found");
lines.push(`${pkg.padEnd(24)} ${ver}`);
}
// ── Environment Variables (safe subset) ─────────────────────────────────
lines.push(section("Environment Variables (non-sensitive)"));
const safeEnvKeys = [
"NODE_ENV",
"PORT",
"DATA_DIR",
"DB_BACKUPS_DIR",
"LOG_LEVEL",
"NEXT_PUBLIC_APP_URL",
"ROUTER_API_KEY_HINT",
];
for (const key of safeEnvKeys) {
const val = process.env[key];
if (val !== undefined) {
// Mask if looks like a secret
const masked = val.length > 8 ? val.substring(0, 4) + "****" : "****";
lines.push(`${key.padEnd(28)} ${masked}`);
}
}
// ── Output ───────────────────────────────────────────────────────────────
const report = lines.join("\n") + "\n";
// Write to file
const outArg = process.argv.find((a) => a.startsWith("--output="));
const outFile = outArg
? outArg.replace("--output=", "")
: process.argv[process.argv.indexOf("--output") + 1] || "system-info.txt";
const outPath = join(ROOT, outFile);
writeFileSync(outPath, report);
console.log(report);
console.log(`\n✅ Report saved to: ${outPath}`);
console.log(
`📎 Attach this file when reporting issues at: https://github.com/diegosouzapw/OmniRoute/issues`
);
@@ -10,6 +10,19 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
const PROFILES_DIR = path.join(resolveDataDir(), "codex-profiles");
/**
* Resolve a path inside PROFILES_DIR and verify it stays within bounds.
* Throws on path traversal attempts.
*/
function safeProfilePath(...segments: string[]): string {
const resolved = path.resolve(PROFILES_DIR, ...segments);
const base = path.resolve(PROFILES_DIR);
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
throw new Error("Invalid path: directory traversal detected");
}
return resolved;
}
/**
* Ensure profiles directory exists
*/
+15 -1
View File
@@ -247,6 +247,14 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
"ollama-cloud": {
url: "https://api.ollama.com/v1/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => data.models || data.data || [],
},
};
/**
@@ -389,7 +397,13 @@ export async function GET(request, { params }) {
// Get auth token
const token = accessToken || apiKey;
if (!token) {
return NextResponse.json({ error: "No valid token found" }, { status: 401 });
return NextResponse.json(
{
error:
"No API key configured for this provider. Please add an API key in the provider settings.",
},
{ status: 400 }
);
}
// Build request URL
+17 -2
View File
@@ -159,11 +159,26 @@ export async function listDbBackups() {
export async function restoreDbBackup(backupId: string) {
const backupDir = DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups");
const backupPath = path.join(backupDir, backupId);
if (!backupId.startsWith("db_") || !backupId.endsWith(".sqlite")) {
// Validate format: must be db_<timestamp>_<reason>.sqlite, no path separators
if (
!backupId.startsWith("db_") ||
!backupId.endsWith(".sqlite") ||
backupId.includes(path.sep) ||
backupId.includes("/")
) {
throw new Error("Invalid backup ID");
}
const backupPath = path.resolve(backupDir, backupId);
// Prevent path traversal: resolved path must stay within backupDir
if (
!backupPath.startsWith(path.resolve(backupDir) + path.sep) &&
backupPath !== path.resolve(backupDir)
) {
throw new Error("Invalid backup ID: path traversal detected");
}
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup not found: ${backupId}`);
}
+11 -5
View File
@@ -35,8 +35,8 @@ const pendingRequests: {
byModel: Record<string, number>;
byAccount: Record<string, Record<string, number>>;
} = {
byModel: {},
byAccount: {},
byModel: Object.create(null) as Record<string, number>,
byAccount: Object.create(null) as Record<string, Record<string, number>>,
};
/**
@@ -50,16 +50,22 @@ export function trackPendingRequest(
) {
const modelKey = provider ? `${model} (${provider})` : model;
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
// Use hasOwnProperty guard to prevent prototype pollution via crafted keys
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byModel, modelKey)) {
pendingRequests.byModel[modelKey] = 0;
}
pendingRequests.byModel[modelKey] = Math.max(
0,
pendingRequests.byModel[modelKey] + (started ? 1 : -1)
);
if (connectionId) {
if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {};
if (!pendingRequests.byAccount[connectionId][modelKey])
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount, connectionId)) {
pendingRequests.byAccount[connectionId] = Object.create(null) as Record<string, number>;
}
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount[connectionId], modelKey)) {
pendingRequests.byAccount[connectionId][modelKey] = 0;
}
pendingRequests.byAccount[connectionId][modelKey] = Math.max(
0,
pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1)
+17 -3
View File
@@ -45,12 +45,22 @@ const CHAT_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
const LOG_DIR = path.join(__dirname, "../../logs/mitm");
if (ENABLE_FILE_LOG && !fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
// Safe log filename: only alphanumeric + hyphens, anchored inside LOG_DIR
function safeLogPath(name) {
const safe = name.replace(/[^a-zA-Z0-9_\-]/g, "_").substring(0, 80);
const resolved = path.resolve(LOG_DIR, safe);
if (!resolved.startsWith(path.resolve(LOG_DIR) + path.sep)) {
throw new Error("Path traversal attempt detected in log filename");
}
return resolved;
}
function saveRequestLog(url, bodyBuffer) {
if (!ENABLE_FILE_LOG) return;
try {
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}.json`);
const filePath = safeLogPath(`${ts}_${urlSlug}.json`);
const body = JSON.parse(bodyBuffer.toString());
fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
console.log(`💾 Saved request: ${filePath}`);
@@ -64,7 +74,7 @@ function saveResponseLog(url, data) {
try {
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}_response.txt`);
const filePath = safeLogPath(`${ts}_${urlSlug}_response.txt`);
fs.writeFileSync(filePath, data);
console.log(`💾 Saved response: ${filePath}`);
} catch {
@@ -156,6 +166,10 @@ function getMappedModel(model) {
async function passthrough(req, res, bodyBuffer) {
const targetIP = await resolveTargetIP();
// TLS validation is enabled by default. Set MITM_DISABLE_TLS_VERIFY=1 only
// in controlled local environments where the target uses a self-signed cert.
const rejectUnauthorized = process.env.MITM_DISABLE_TLS_VERIFY !== "1";
const forwardReq = https.request(
{
hostname: targetIP,
@@ -164,7 +178,7 @@ async function passthrough(req, res, bodyBuffer) {
method: req.method,
headers: { ...req.headers, host: TARGET_HOST },
servername: TARGET_HOST,
rejectUnauthorized: false,
rejectUnauthorized,
},
(forwardRes) => {
res.writeHead(forwardRes.statusCode, forwardRes.headers);
+18 -4
View File
@@ -5,11 +5,24 @@ import { resolveDataDir } from "@/lib/dataPaths";
const BACKUP_DIR = path.join(resolveDataDir(), "backups");
const MAX_BACKUPS_PER_TOOL = 5;
/**
* Resolve a path within BACKUP_DIR and verify it stays within bounds.
* Throws if the resolved path escapes BACKUP_DIR (path traversal guard).
*/
function safePath(...segments: string[]): string {
const resolved = path.resolve(BACKUP_DIR, ...segments);
const base = path.resolve(BACKUP_DIR);
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
throw new Error("Invalid path: directory traversal detected");
}
return resolved;
}
/**
* Get backup directory for a specific tool
*/
function getToolBackupDir(toolId: string) {
return path.join(BACKUP_DIR, toolId);
return safePath(toolId);
}
/**
@@ -136,7 +149,8 @@ export async function listBackups(toolId: string) {
*/
export async function restoreBackup(toolId: string, backupId: string) {
const dir = getToolBackupDir(toolId);
const backupPath = path.join(dir, backupId);
// Anchor backupId within the tool dir — prevent path traversal via backupId
const backupPath = safePath(toolId, backupId);
const metaPath = backupPath + ".meta.json";
// Read metadata to find original path
@@ -174,8 +188,8 @@ export async function restoreBackup(toolId: string, backupId: string) {
* Delete a specific backup by its id.
*/
export async function deleteBackup(toolId: string, backupId: string) {
const dir = getToolBackupDir(toolId);
const backupPath = path.join(dir, backupId);
// Anchor backupId within the tool dir — prevent path traversal via backupId
const backupPath = safePath(toolId, backupId);
const metaPath = backupPath + ".meta.json";
try {