Compare commits

...

49 Commits

Author SHA1 Message Date
diegosouzapw 2306081dab feat: sidebar reorganization + CLI fingerprint to Agents page
Build Electron Desktop App / Validate version (push) Failing after 32s
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 CLI sidebar section: Tools + Agents
- Move Media to Debug section
- Move Health + Logs to System section
- Move CLI Fingerprint card from Settings > Security to Agents page
- Add cliToolsShort i18n key to all 30 languages
2026-03-07 11:55:18 -03:00
diegosouzapw 8963c62adb feat(release): v2.0.11 — ACP Agents Dashboard + Anti-Ban Docs
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
- ACP Agents Dashboard: Debug > Agents page with 14 built-in CLI agents
- Custom agent support: register any CLI via dashboard form
- Fix themeCoral i18n in all 30 languages
- Improved anti-ban feature descriptions in README
- Sync all 29 translated READMEs with new features
- CHANGELOG entry for v2.0.11
2026-03-07 11:22:16 -03:00
diegosouzapw e4a11bd6d0 feat: ACP Agents dashboard + themeCoral i18n fix
- Add Dashboard > Debug > Agents page: grid of 14 built-in CLI agents
  with installation status, version detection, protocol badges
- Add custom agent support: users can register any CLI tool via form
- Expand registry from 5 to 14 built-in agents (aider, opencode, cline,
  qwen-code, forge, amazon-q, interpreter, cursor-cli, warp)
- Add 60-second detection cache to avoid repeated execSync calls
- API: GET lists all agents, POST adds custom/refreshes, DELETE removes
- Settings schema: add customAgents array field
- Fix missing themeCoral in settings namespace for all 30 languages
- Add agents sidebar key in all 30 languages
- Add agents page i18n namespace (en + pt-BR)
2026-03-07 11:19:12 -03:00
diegosouzapw 2f6e63771f docs: sync v2.0.9+ features across all 30 READMEs
Added new features table (Playground, CLI Fingerprints, ACP, Custom
apiFormat routing, Codex workspace isolation, Electron auto-update)
to all 29 translated READMEs + English source.
2026-03-07 11:05:40 -03:00
diegosouzapw cbd60c853e feat(release): v2.0.10 — CLI fingerprint UI toggle
Build Electron Desktop App / Validate version (push) Failing after 27s
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
2026-03-07 10:57:16 -03:00
diegosouzapw 2d8091340f feat: CLI fingerprint UI toggle + fix playground i18n (#223)
- Add CLI Fingerprint Matching card in Settings > Security tab
  - Per-provider toggle chips (codex, claude, github, antigravity)
  - Emerald-themed UI with fingerprint icon and active count
  - Settings synced to runtime cache via API
- Fix playground i18n missing from 29 non-English sidebar translations
- Add cliCompatProviders to settings schema (Zod validation)
- Add setCliCompatProviders/getCliCompatProviders to cliFingerprints.ts
- Add i18n keys for CLI fingerprint settings (en + pt-BR)
2026-03-07 10:56:58 -03:00
diegosouzapw 2025c16c82 fix: deploy workflow uses node app/server.js for pm2 2026-03-07 10:40:00 -03:00
diegosouzapw 811fb7f9b2 docs: fix deploy workflow to use npm-only approach
Old workflow used git clone at /opt/omniroute-app but recent releases
used npm install -g, causing version mismatch. PM2 now runs from
/usr/lib/node_modules/omniroute directly. Removed all references to
the stale local directory.
2026-03-07 10:38:49 -03:00
diegosouzapw a2bd32e76c feat(release): v2.0.9 — playground, CLI fingerprints, ACP
Build Electron Desktop App / Validate version (push) Failing after 27s
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
2026-03-07 10:26:34 -03:00
Diego Rodrigues de Sa e Souza 89c07f4e8a Merge pull request #240 from diegosouzapw/feat/all-features-234-223-235
feat: playground, CLI fingerprints, ACP support (#234, #223, #235)
2026-03-07 10:25:28 -03:00
diegosouzapw ac89069671 feat: playground, CLI fingerprints, ACP support (#234, #223, #235)
- Add model playground page to dashboard (provider/model/endpoint selectors, Monaco editor, streaming, abort)
- Add CLI fingerprint matching (per-provider header/body ordering to match native CLI binaries)
- Add ACP module (CLI agent discovery, process spawner, session manager, API endpoint)
- Add playground to sidebar debug section with i18n support
- Close #192 and #200 as stale (v1.8.1, no repro info)
2026-03-07 10:24:43 -03:00
diegosouzapw 7cb420d8e6 feat(release): v2.0.8 — custom image model handler resolution
Build Electron Desktop App / Validate version (push) Failing after 26s
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
2026-03-07 10:05:20 -03:00
Diego Rodrigues de Sa e Souza d3919d441f Merge pull request #239 from diegosouzapw/fix/issue-238-image-handler
fix: pass resolved provider to image handler for custom models (#238)
2026-03-07 10:04:24 -03:00
diegosouzapw 4b5824babc fix: pass resolved provider to image handler for custom models (#238) 2026-03-07 10:03:48 -03:00
diegosouzapw fb87df14fd feat(release): v2.0.7 — custom image model routing + Codex OAuth workspace isolation
Build Electron Desktop App / Validate version (push) Failing after 34s
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
2026-03-07 06:58:07 -03:00
Diego Rodrigues de Sa e Souza da9e4e929b Merge pull request #237 from diegosouzapw/fix/issue-232-236-image-oauth
fix: custom image model routing + Codex OAuth workspace isolation (#232, #236)
2026-03-07 06:56:49 -03:00
diegosouzapw 10b23b15ae fix: custom image model routing + Codex OAuth workspace isolation (#232, #236) 2026-03-07 06:56:09 -03:00
diegosouzapw 30fba39b35 feat(release): v2.0.6 — custom model apiFormat routing fix
Build Electron Desktop App / Validate version (push) Failing after 33s
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
2026-03-07 01:36:21 -03:00
Diego Rodrigues de Sa e Souza 5a75ff67c9 Merge pull request #233 from diegosouzapw/fix/issue-204-apiformat-routing
fix: wire apiFormat from custom model DB into routing layer (#204)
2026-03-07 01:35:30 -03:00
diegosouzapw 358828b617 fix: wire apiFormat from custom model DB into routing layer (#204) 2026-03-07 01:26:59 -03:00
diegosouzapw e080c4a16a feat(release): v2.0.5 — fix Chat→Responses reasoning IDs, electron auto-update, dependency bumps
Build Electron Desktop App / Validate version (push) Failing after 31s
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
2026-03-06 18:51:24 -03:00
Diego Rodrigues de Sa e Souza 04b7e38baf Merge pull request #221 from benzntech/feat/electron-auto-update
feat(electron): add auto-update functionality with electron-updater
2026-03-06 18:49:54 -03:00
Diego Rodrigues de Sa e Souza 7ee23fbe19 Merge pull request #230 from diegosouzapw/dependabot/npm_and_yarn/express-rate-limit-8.3.0
deps: bump express-rate-limit from 8.2.1 to 8.3.0
2026-03-06 18:49:51 -03:00
Diego Rodrigues de Sa e Souza c49bdb4ebb Merge pull request #229 from diegosouzapw/dependabot/github_actions/docker/build-push-action-7
chore(deps): bump docker/build-push-action from 6 to 7
2026-03-06 18:49:48 -03:00
Diego Rodrigues de Sa e Souza 0f7efed8d5 Merge pull request #228 from diegosouzapw/dependabot/github_actions/actions/upload-artifact-7
chore(deps): bump actions/upload-artifact from 4 to 7
2026-03-06 18:49:46 -03:00
Diego Rodrigues de Sa e Souza d07bc6dcf3 Merge pull request #227 from diegosouzapw/dependabot/github_actions/docker/login-action-4
chore(deps): bump docker/login-action from 3 to 4
2026-03-06 18:49:43 -03:00
Diego Rodrigues de Sa e Souza d607d46fa3 Merge pull request #226 from diegosouzapw/dependabot/github_actions/actions/download-artifact-8
chore(deps): bump actions/download-artifact from 4 to 8
2026-03-06 18:49:40 -03:00
Diego Rodrigues de Sa e Souza 2225dd14aa Merge pull request #225 from diegosouzapw/dependabot/github_actions/actions/cache-5
chore(deps): bump actions/cache from 4 to 5
2026-03-06 18:49:37 -03:00
Diego Rodrigues de Sa e Souza f6c0e7bbbe Merge pull request #222 from benzntech/fix/electron-release-duplicate-asset
fix(ci): remove duplicate OmniRoute.exe entry in electron release workflow
2026-03-06 18:49:28 -03:00
Diego Rodrigues de Sa e Souza c4675c5219 Merge pull request #231 from diegosouzapw/fix/issue-224-reasoning-ids
fix: omit synthesized reasoning items in Chat→Responses translation (#224)
2026-03-06 18:49:25 -03:00
diegosouzapw 2d977a3c4d fix: omit synthesized reasoning items in Chat→Responses translation (#224) 2026-03-06 18:48:34 -03:00
dependabot[bot] 9405918258 deps: bump express-rate-limit from 8.2.1 to 8.3.0
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.3.0.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.3.0)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:46:36 +00:00
dependabot[bot] a69d7dd4b5 chore(deps): bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:27:03 +00:00
dependabot[bot] 428e6cb53f chore(deps): bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:59 +00:00
dependabot[bot] c9a2955d28 chore(deps): bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:54 +00:00
dependabot[bot] 7aefcd3437 chore(deps): bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:51 +00:00
dependabot[bot] 79f4f79c46 chore(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:46 +00:00
benzntech c11c275678 fix(electron): address auto-updater review issues
- Remove unused dialog import
- Stop Next.js server before quitAndInstall() to prevent data loss
- Propagate errors from checkForUpdates/downloadUpdate to IPC handlers
  so renderer can distinguish success from failure
- Remove meaningless return value from install-update handler
2026-03-06 19:22:41 +05:30
benzntech bbcd1d3a08 fix(ci): remove duplicate OmniRoute.exe entry in electron release workflow
Duplicate release-assets/OmniRoute.exe glob caused softprops/action-gh-release
to attempt a second upload of the same asset, triggering a 404 Not Found error
on the GitHub release asset update API. The file is already covered by the
*.exe glob pattern above it.
2026-03-06 19:18:41 +05:30
benzntech 3342d5b931 feat(electron): add auto-update functionality with electron-updater 2026-03-06 18:54:00 +05:30
diegosouzapw f96ee44213 feat(release): v2.0.4 — round-robin lastUsedAt persistence, zod standalone build fix
Build Electron Desktop App / Validate version (push) Failing after 31s
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
2026-03-05 23:24:56 -03:00
Diego Rodrigues de Sa e Souza bc53fe0cd9 Merge pull request #219 from diegosouzapw/fix/issue-218-round-robin-lastUsedAt
fix: persist lastUsedAt for round-robin + zod in standalone build (#218, #217)
2026-03-05 23:24:13 -03:00
diegosouzapw 97a67b5d3e fix: persist lastUsedAt in provider_connections schema for round-robin (#218)
- Add last_used_at column to provider_connections CREATE TABLE schema
- Add ensureProviderConnectionsColumns migration for existing databases
- Add last_used_at to INSERT and UPDATE SQL in providers.ts
- Add last_used_at to JSON migration INSERT in core.ts
- Add zod to serverExternalPackages in next.config.mjs (#217)

Fixes #218: Round-robin routing strategy now correctly persists
the lastUsedAt timestamp, allowing rotation between accounts.

Fixes #217: zod module is now properly included in standalone/Docker
builds by declaring it as a server external package.
2026-03-05 23:22:10 -03:00
diegosouzapw 1ffa58be76 feat(release): v2.0.3 — deferred tools cache_control fix, quota system hardening
Build Electron Desktop App / Validate version (push) Failing after 29s
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
2026-03-05 21:57:04 -03:00
Diego Rodrigues de Sa e Souza a5cf51c0b9 Merge pull request #214 from DavyMassoneto/fix/claude-oauth-usage-endpoint
fix: harden quota system — code review fixes + build fix
2026-03-05 21:55:23 -03:00
Diego Rodrigues de Sa e Souza 3d38cbf70f Merge pull request #216 from DavyMassoneto/fix/defer-loading-cache-control-conflict
fix: skip cache_control on deferred tools + remove stale schemas.js
2026-03-05 21:55:14 -03:00
DavyMassoneto 196a4e037c fix: skip cache_control on deferred tools + remove stale schemas.js
- Skip tools with defer_loading=true when assigning cache_control
  (Anthropic API rejects the combination with 400)
- Delete stale schemas.js that shadowed the .ts source, causing
  missing cloudSyncActionSchema export

Fixes #215
2026-03-05 20:19:58 -03:00
DavyMassoneto bfe495931f fix(claude): correct utilization semantics, harden quota cache, fix premature model unavailability
- Fix inverted Claude OAuth utilization (remaining, not used)
- Add hasUtilization() guard to prevent false exhaustion from empty responses
- Centralize anthropic-version into CLAUDE_CONFIG.apiVersion
- Add parseDate() for safe date validation in quota cache
- Batch background refresh with MAX_CONCURRENT_REFRESHES=5
- Move setModelUnavailable to after all accounts exhausted, not first 429
- Extract safePercentage() to shared utils (dedup)
- Use isRecord() type guard in usage API route
- Exclude binary files from Tailwind v4 source scanning
2026-03-05 19:39:59 -03:00
DavyMassoneto 11bcdd810a feat: quota-aware account selection + fix premature model unavailability
- Move setModelUnavailable from per-account loop to all-accounts-exhausted path
- Clear model unavailability on successful fallback
- Add in-memory quota cache with background refresh (5min active, 20min exhausted)
- Integrate quota cache in account selection to skip exhausted accounts
- Mark accounts as exhausted from 429 when no cached quota data exists
- Populate quota cache from dashboard usage endpoint
2026-03-05 18:49:56 -03:00
103 changed files with 3434 additions and 1267 deletions
+42 -21
View File
@@ -4,52 +4,73 @@ description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35)
# Deploy to VPS Workflow
Deploy OmniRoute to the production VPS using Node.js + PM2 (no Docker).
Deploy OmniRoute to the production VPS using `npm install -g` + PM2.
**VPS:** `69.164.221.35` (Akamai, Ubuntu 24.04, 1GB RAM + 2.5GB swap)
**App path:** `/opt/omniroute-app`
**Local VPS:** `192.168.0.15` (same setup)
**Process manager:** PM2 (`omniroute`)
**Port:** `20128`
> [!IMPORTANT]
> PM2 runs from the global npm package at `/usr/lib/node_modules/omniroute`.
> **DO NOT** use git clone or local copies. The `npm install -g` command handles
> building, publishing, and installing the standalone app in one step.
## Steps
### 1. Push to GitHub
### 1. Publish to npm
Ensure all changes are committed and pushed:
Ensure the version in `package.json` is bumped and the package is published:
```bash
git push origin main
npm publish
```
### 2. SSH into VPS, pull latest code, rebuild, and restart
### 2. Install on VPS and restart PM2
// turbo-all
```bash
ssh root@69.164.221.35 "
cd /opt/omniroute-app &&
git fetch origin &&
git reset --hard origin/main &&
export NODE_OPTIONS='--max-old-space-size=1536' &&
npm install --no-audit --no-fund &&
npm run build &&
pm2 restart omniroute &&
pm2 save &&
echo '✅ Deploy complete!'
"
ssh root@69.164.221.35 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
```
For the local VPS:
```bash
ssh root@192.168.0.15 "npm install -g omniroute@latest && pm2 restart omniroute && pm2 save && echo '✅ Deploy complete!'"
```
### 3. Verify the deployment
```bash
ssh root@69.164.221.35 "pm2 list && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
ssh root@69.164.221.35 "pm2 list && cat \$(npm root -g)/omniroute/package.json | grep version | head -1 && curl -s -o /dev/null -w 'HTTP %{http_code}' http://localhost:20128/"
```
Expected: PM2 shows `online`, HTTP returns `307` (redirect to login).
Expected: PM2 shows `online`, version matches published, HTTP returns `307` (redirect to login).
## How it works
1. `npm publish` builds Next.js standalone + bundles everything into the npm package
2. `npm install -g omniroute@latest` downloads and installs to `/usr/lib/node_modules/omniroute/`
3. PM2 is registered to run `npm start` from that directory (cwd: `/usr/lib/node_modules/omniroute`)
4. `pm2 restart omniroute` picks up the new code immediately
## PM2 Setup (one-time)
If PM2 needs to be reconfigured from scratch:
```bash
ssh root@<VPS> "
cd /usr/lib/node_modules/omniroute &&
PORT=20128 pm2 start app/server.js --name omniroute --env PORT=20128 &&
pm2 save &&
pm2 startup
"
```
## Notes
- The VPS has only 1GB RAM. `NODE_OPTIONS='--max-old-space-size=1536'` uses swap for the build.
- The `.env` file is at `/usr/lib/node_modules/omniroute/.env`. Back it up before major npm updates.
- PM2 is configured with `pm2 startup` to auto-restart on reboot.
- The `.env` file is at `/opt/omniroute-app/.env` (copied from the old Docker setup at `/opt/omniroute/.env`).
- Nginx proxies `omniroute.online``localhost:20128`.
- The VPS has only 1GB RAM — builds happen locally via `npm publish`, not on the VPS.
+3 -3
View File
@@ -31,14 +31,14 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
target: runner-base
@@ -87,7 +87,7 @@ jobs:
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+3 -4
View File
@@ -78,7 +78,7 @@ jobs:
cache: npm
- name: Cache node_modules
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -120,7 +120,7 @@ jobs:
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: electron-${{ matrix.platform }}
path: release-assets/
@@ -136,7 +136,7 @@ jobs:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: release-assets
merge-multiple: true
@@ -172,6 +172,5 @@ jobs:
release-assets/*.blockmap
release-assets/*.source.tar.gz
release-assets/*.source.zip
release-assets/OmniRoute.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -117,3 +117,8 @@ icon.iconset/
# VS Code Extension (independent Git repo)
vscode-extension/
# SQLite residual files
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal
+210
View File
@@ -7,6 +7,216 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
---
## [2.0.11] — 2026-03-07
> ### 🤖 ACP Agents Dashboard + Anti-Ban Docs
### ✨ New Features
- **ACP Agents Dashboard** — New Debug > Agents page: grid of 14 built-in CLI agents (codex, claude, goose, gemini, openclaw, aider, opencode, cline, qwen-code, forge, amazon-q, interpreter, cursor-cli, warp) with installation status, version detection, protocol badges, and custom agent form
- **Custom Agent Support** — Users can register any CLI tool for auto-detection via dashboard form (name, binary, version command, spawn args). Stored in settings DB
- **60-Second Detection Cache** — Agent detection results cached to avoid repeated `execSync` calls
### 🐛 Bug Fixes
- **Fix `settings.themeCoral` untranslated** — Theme color "Coral" was missing from the `settings` i18n namespace in all 30 languages. Added translations for all
### 📝 Documentation
- **Anti-Ban Features Clarified** — Improved README descriptions for TLS Fingerprint Spoofing and CLI Fingerprint Matching, emphasizing ban-risk reduction benefits and proxy IP preservation
- **ACP Agents Dashboard** — Added to v2.0.9+ features table and deployment features table in README
### 📁 Files Changed
| File | Change |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| `src/lib/acp/registry.ts` | Expanded from 5 to 14 agents + custom agent support + 60s cache |
| `src/app/api/acp/agents/route.ts` | GET/POST/DELETE for full agent management |
| `src/app/(dashboard)/dashboard/agents/page.tsx` | New dashboard page |
| `src/shared/components/Sidebar.tsx` | Added Agents to Debug section |
| `src/shared/validation/settingsSchemas.ts` | Added `customAgents` array field |
| `src/i18n/messages/*.json` (×30) | Fixed `themeCoral`, added sidebar `agents` key, agents namespace |
---
## [2.0.9] — 2026-03-07
> ### 🚀 Feature Drop — Playground, CLI Fingerprints, ACP
### ✨ New Features
- **#234 — Model Playground** — Dashboard page to test any model directly (provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing metrics). Available in the Debug sidebar section.
- **#223 — CLI Fingerprint Matching** — Per-provider header/body field ordering to match native CLI binary fingerprints, reducing account flagging risk. Enable via `CLI_COMPAT_<PROVIDER>=1` or `CLI_COMPAT_ALL=1` env vars.
- **#235 — ACP Support** — Agent Client Protocol module with CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner/manager, and `/api/acp/agents` endpoint.
### 🧹 Housekeeping
- **#192 & #200** — Closed as stale (needs-info, v1.8.1, no reproduction info provided)
---
## [2.0.8] — 2026-03-07
> ### 🐛 Bug Fix — Custom Image Model Handler Resolution
### 🐛 Bug Fixes
- **#238 — Custom image models still fail in handler layer** — v2.0.7 fixed the route-layer validation, but the handler (`handleImageGeneration()`) called `parseImageModel()` again internally, rejecting custom models a second time. Fix: handler now accepts an optional `resolvedProvider` parameter; when provided, it skips re-validation and routes custom models to the OpenAI-compatible handler with a synthetic config. PR #239
### 📁 Files Changed
| File | Change |
| -------------------------------------------- | -------------------------------------------------------------------------------- |
| `open-sse/handlers/imageGeneration.ts` | Added `resolvedProvider` param + custom model fallback |
| `src/app/api/v1/images/generations/route.ts` | Tracks `isCustomModel`, passes `resolvedProvider`, credentials for custom models |
---
## [2.0.7] — 2026-03-07
> ### 🐛 Bug Fixes — Custom Image Models + Codex OAuth Workspace Isolation
### 🐛 Bug Fixes
- **#232 — Custom Gemini image models fail on `/v1/images/generations`** — Custom models tagged with `supportedEndpoints: ["images"]` appeared in the model listing (GET) but were rejected by the POST handler. `parseImageModel()` only checked the built-in `IMAGE_PROVIDERS` registry. Fix: added a custom model DB fallback for models with the `images` endpoint tag. PR #237
- **#236 — Codex OAuth overwrites existing connection when same email added to another workspace** — The OAuth callback route had 3 upsert blocks matching connections by email-only, bypassing the workspace-aware logic in `createProviderConnection()`. When the same email authenticated to a new workspace, the existing connection's `workspaceId` was silently overwritten. Fix: for Codex, the match now also checks `providerSpecificData.workspaceId`, allowing separate connections per workspace. PR #237
### 📁 Files Changed
| File | Change |
| ------------------------------------------------ | ---------------------------------------------------- |
| `src/app/api/v1/images/generations/route.ts` | Custom model DB fallback in POST handler |
| `src/app/api/oauth/[provider]/[action]/route.ts` | Workspace-aware Codex matching in 3 upsert locations |
### ⏭️ Issues Triaged
- **#234** — Playground feature request — Acknowledged, added to roadmap
- **#235** — ACP support feature request — Acknowledged, added to roadmap
---
## [2.0.6] — 2026-03-07
> ### 🐛 Bug Fix — Custom Model API Format Routing
### 🐛 Bug Fixes
- **#204 — Custom model `apiFormat` not used in routing** — Custom models configured with `apiFormat: "responses"` in the dashboard were still being routed through the Chat Completions translator. The `apiFormat` field was stored in the DB and displayed in the UI, but never consumed by the routing layer. Fix: `getModelInfo()` now returns `apiFormat` from the custom model DB, and both `resolveModelOrError()` functions override `targetFormat` to `openai-responses` when set. PR #233
### ✅ Issues Closed
- **#205** — Combo endpoint support — Already implemented in v2.0.2
- **#206** — Manual model→endpoint mapping — Already implemented in v2.0.2
- **#223** — CLI fingerprint parity — Responded with 4-phase roadmap
### 📁 Files Changed
| File | Change |
| --------------------------------- | ---------------------------------------------------------------------- |
| `src/sse/services/model.ts` | Added `lookupCustomModelApiFormat()`, enriched `getModelInfo()` return |
| `src/sse/handlers/chat.ts` | Override `targetFormat` when `apiFormat === "responses"` |
| `src/sse/handlers/chatHelpers.ts` | Same override in duplicate `resolveModelOrError()` |
---
## [2.0.5] — 2026-03-06
> ### 🐛 Bug Fix, Electron Auto-Update & Dependency Bumps
### 🐛 Bug Fixes
- **#224 — Chat→Responses translation creates invalid reasoning IDs** — Removed synthetic reasoning item generation in `openaiToOpenAIResponsesRequest()`. The translator was creating reasoning items with IDs like `reasoning_15`, but OpenAI's Responses API requires server-generated `rs_*` IDs, causing `400 Invalid Request` errors from Responses-compatible upstreams. Fix: omit reasoning items entirely during translation
- **CI: duplicate OmniRoute.exe in release workflow** — Removed redundant explicit `release-assets/OmniRoute.exe` entry that caused `softprops/action-gh-release` to fail with 404 on duplicate upload. PR #222 by @benzntech
### ✨ New Features
- **Electron Auto-Update** — Added auto-update functionality to the desktop app using `electron-updater`. Includes IPC handlers for check/download/install, "Check for Updates" in system tray menu, desktop notification when update is ready, and silent startup check (3s delay). PR #221 by @benzntech
### 📦 Dependencies
- Bump `actions/cache` from 4 to 5 (#225)
- Bump `actions/download-artifact` from 4 to 8 (#226)
- Bump `docker/login-action` from 3 to 4 (#227)
- Bump `actions/upload-artifact` from 4 to 7 (#228)
- Bump `docker/build-push-action` from 6 to 7 (#229)
- Bump `express-rate-limit` from 8.2.1 to 8.3.0 (#230)
### 📁 Files Changed
| File | Change |
| ------------------------------------------------- | ---------------------------------------------------- |
| `open-sse/translator/request/openai-responses.ts` | Remove synthetic reasoning item generation |
| `.github/workflows/electron-release.yml` | Remove duplicate exe entry, bump GH Actions |
| `.github/workflows/docker-publish.yml` | Bump docker/login-action and build-push-action |
| `electron/main.js` | Auto-updater setup, IPC handlers, tray menu |
| `electron/package.json` | Added electron-updater dep and GitHub publish config |
| `electron/preload.js` | Exposed update APIs via contextBridge |
| `package-lock.json` | Updated express-rate-limit |
---
## [2.0.4] — 2026-03-06
> ### 🐛 Bug Fixes — Round-Robin Persistence & Docker Compatibility
### 🐛 Bug Fixes
- **#218 — Round-robin sticks to one account** — Added `last_used_at` column to `provider_connections` schema. Round-robin routing relied on `lastUsedAt` to rotate between accounts, but the column was missing from the database — the value was always `null`, causing selection to fall back to the same account. Includes auto-migration for existing databases
- **#217`Cannot find module 'zod'` in Docker/standalone builds** — Added `zod` to `serverExternalPackages` in `next.config.mjs`. Next.js standalone builds weren't tracing `zod` through dynamic imports, causing crashes on Docker startup. Data is **not lost** — the crash prevented the server from reading the existing database
### 📁 Files Changed
| File | Change |
| ------------------------- | ------------------------------------------------------ |
| `src/lib/db/core.ts` | Schema + migration + JSON migration for `last_used_at` |
| `src/lib/db/providers.ts` | INSERT + UPDATE SQL for `last_used_at` |
| `next.config.mjs` | `serverExternalPackages: ['better-sqlite3', 'zod']` |
---
## [2.0.3] — 2026-03-05
> ### 🐛 Bug Fixes & Quota System Hardening
### 🐛 Bug Fixes
- **#215 — Deferred tools 400 error** — Skip `cache_control` on tools with `defer_loading=true` when assigning prompt caching to the last tool. Previously, the API rejected requests with 400 when MCP tools (Playwright, etc.) had deferred loading enabled. Fix applied in both `claudeHelper.ts` and `openai-to-claude.ts` translation layers. PR #216 by @DavyMassoneto
- **Stale compiled schemas.js** — Deleted stale compiled `schemas.js` (912 lines) that shadowed the TypeScript `.ts` source, causing `cloudSyncActionSchema` warnings in the dashboard. PR #216 by @DavyMassoneto
- **#202 — False quota exhaustion** — Fixed empty API responses (`{}`) creating quota objects with `utilization ?? 0` = 0% remaining, incorrectly marking accounts as exhausted. Added `hasUtilization()` guard. PR #214 by @DavyMassoneto
- **Invalid date crash** — `parseDate()` now validates dates before comparison, handling `Invalid Date` from malformed `resetAt` values gracefully. PR #214 by @DavyMassoneto
- **`total=0` false infinite quota** — `normalizeQuotas` now defaults to 0% remaining when `total` is zero (was incorrectly reporting 100%). PR #214 by @DavyMassoneto
- **Tailwind v4 build failure** — Tailwind v4 scanned `*.sqlite-shm`/`.sqlite-wal` binary files, triggering "Invalid code point" errors. Added `@source not` exclusions in `globals.css`. PR #214 by @DavyMassoneto
### ✨ Improvements
- **Quota-aware account selection** — All load-balancing strategies (sticky, round-robin, p2c, random, least-used, cost-optimized, fill-first) now prioritize accounts with available quota over exhausted ones. PR #214 by @DavyMassoneto
- **Concurrent refresh protection** — `tickRunning` flag prevents overlapping background quota refresh ticks; `refreshingSet` deduplicates per-connection refreshes. Thundering herd prevention with `MAX_CONCURRENT_REFRESHES=5`. PR #214 by @DavyMassoneto
- **`clearModelUnavailability` on success** — Model unavailability is now cleared on every successful request, not only on fallback paths. PR #214 by @DavyMassoneto
- **Centralized `anthropic-version`** — Hardcoded `anthropic-version` header (3 occurrences) centralized into `CLAUDE_CONFIG.apiVersion`. PR #214 by @DavyMassoneto
- **Extracted `safePercentage()` utility** — Shared percentage validation function extracted to `src/shared/utils/formatting.ts`, eliminating duplication between backend and frontend. PR #214 by @DavyMassoneto
- **`isRecord()` type guard** — Replaces inline `typeof` chain in usage API route. PR #214 by @DavyMassoneto
### 📁 Files Changed
| File | Change |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `open-sse/translator/helpers/claudeHelper.ts` | Skip `cache_control` on deferred tools |
| `open-sse/translator/request/openai-to-claude.ts` | Same fix in translator layer |
| `src/shared/validation/schemas.js` | **DELETED** — stale compiled JS |
| `.gitignore` | Exclude Tailwind binary scanning |
| `open-sse/services/usage.ts` | Legacy endpoint fallback logging |
| `src/domain/quotaCache.ts` | **NEW** — Core quota cache with hardening |
| `src/shared/utils/formatting.ts` | **NEW**`safePercentage()` utility |
| `src/instrumentation.ts` | Startup log for quota cache |
| `src/sse/handlers/chat.ts` | `clearModelUnavailability` + `markAccountExhaustedFrom429` |
| `src/sse/services/auth.ts` | Quota-aware account selection |
| `src/app/globals.css` | Tailwind `@source not` exclusions |
| `src/app/api/usage/[connectionId]/route.ts` | `isRecord()` type guard |
| `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx` | Use `remainingPercentage` directly |
| `src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx` | Use shared `safePercentage()` |
---
## [2.0.2] — 2026-03-05
> ### 🐛 Bug Fixes & ✨ Endpoint-Aware Model Management
+12
View File
@@ -53,6 +53,18 @@ _وكيل API العالمي الخاص بك - نقطة نهاية واحدة،
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 موفر الذكاء الاصطناعي المجاني لوكلاء البرمجة المفضلين لديك
_قم بتوصيل أي أداة IDE أو CLI مدعومة بالذكاء الاصطناعي من خلال OmniRoute - بوابة واجهة برمجة التطبيقات المجانية للترميز غير المحدود._
+24 -11
View File
@@ -53,6 +53,18 @@ _Вашият универсален API прокси — една крайна
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Безплатен доставчик на AI за вашите любими кодиращи агенти
_Свържете всеки базиран на AI IDE или CLI инструмент чрез OmniRoute — безплатен API шлюз за неограничено кодиране._
@@ -919,17 +931,18 @@ OmniRoute v2.0 е създаден като операционна платфо
### 🛡️ Устойчивост, сигурност и управление
| Характеристика | Какво прави |
| -------------------------------------------- | ---------------------------------------------------------------------- |
| 🔌 **Прекъсвачи** | Пътуване/възстановяване на ниво доставчик с прагови контроли |
| 🛡️ **Anti-Thundering Herd** | Защита на Mutex + семафор при събития за повторен опит/скорост |
| 🧠 **Семантичен + кеш на подписа** | Намаляване на разходите/закъснението с два кеш слоя |
| ⚡ **Искане на идемпотентност** | Дублиран защитен прозорец |
| 🔒 **TLS Fingerprint Spoofing** | По-добра съвместимост с доставчици, филтрирани срещу бот |
| 🌐 **IP филтриране** | Списък с разрешени/списъци с блокирани контроли за открити внедрявания |
| 📊 **Редактируеми ограничения на скоростта** | Конфигурируеми глобални/на ниво доставчик ограничения с постоянство |
| 🔑 **API Key Management + Scoping** | Сигурно издаване/ротация на ключове и контроли на модел/доставчик |
| 🛡️ **Защитен `/models`** | Опционално удостоверяване и скриване на доставчик за каталог на модели |
| Характеристика | Какво прави |
| -------------------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Прекъсвачи** | Пътуване/възстановяване на ниво доставчик с прагови контроли |
| 🛡️ **Anti-Thundering Herd** | Защита на Mutex + семафор при събития за повторен опит/скорост |
| 🧠 **Семантичен + кеш на подписа** | Намаляване на разходите/закъснението с два кеш слоя |
| ⚡ **Искане на идемпотентност** | Дублиран защитен прозорец |
| 🔒 **TLS Fingerprint Spoofing** | По-добра съвместимост с доставчици, филтрирани срещу бот |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **IP филтриране** | Списък с разрешени/списъци с блокирани контроли за открити внедрявания |
| 📊 **Редактируеми ограничения на скоростта** | Конфигурируеми глобални/на ниво доставчик ограничения с постоянство |
| 🔑 **API Key Management + Scoping** | Сигурно издаване/ротация на ключове и контроли на модел/доставчик |
| 🛡️ **Защитен `/models`** | Опционално удостоверяване и скриване на доставчик за каталог на модели |
### 📊 Наблюдаемост и анализ
+24 -11
View File
@@ -53,6 +53,18 @@ _Din universelle API-proxy — ét slutpunkt, 36+ udbydere, ingen nedetid. Nu me
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Gratis AI-udbyder til dine foretrukne kodningsagenter
_Tilslut ethvert AI-drevet IDE- eller CLI-værktøj gennem OmniRoute - gratis API-gateway til ubegrænset kodning._
@@ -920,17 +932,18 @@ OmniRoute v2.0 er bygget som en operationel platform, ikke kun en relæ-proxy.
### 🛡️ Resiliens, sikkerhed og styring
| Funktion | Hvad det gør |
| ----------------------------------- | --------------------------------------------------------------------- |
| 🔌 **Maksimalafbrydere** | Trip/recover på udbyderniveau med tærskelkontrol |
| 🛡️ **Anti-tordenbesætning** | Mutex + semaforbeskyttelse ved genforsøg/rate hændelser |
| 🧠 **Semantisk + signaturcache** | Reduktion af omkostninger/latens med to cachelag |
| ⚡ **Anmod om idempotens** | Dobbelt beskyttelsesvindue |
| 🔒 **TLS Fingerprint Spoofing** | Bedre kompatibilitet med anti-bot-filtrerede udbydere |
| 🌐 **IP-filtrering** | Tilladelsesliste/blokeringslistekontrol for udsatte implementeringer |
| 📊 **Redigerbare satsgrænser** | Konfigurerbare grænser på globalt niveau/udbyderniveau med persistens |
| 🔑 **API Key Management + Scoping** | Sikker nøgleudstedelse/rotation og model-/leverandørkontrol |
| 🛡️ **Beskyttet `/models`** | Valgfri godkendelse og udbyderskjul til modelkatalog |
| Funktion | Hvad det gør |
| ----------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Maksimalafbrydere** | Trip/recover på udbyderniveau med tærskelkontrol |
| 🛡️ **Anti-tordenbesætning** | Mutex + semaforbeskyttelse ved genforsøg/rate hændelser |
| 🧠 **Semantisk + signaturcache** | Reduktion af omkostninger/latens med to cachelag |
| ⚡ **Anmod om idempotens** | Dobbelt beskyttelsesvindue |
| 🔒 **TLS Fingerprint Spoofing** | Bedre kompatibilitet med anti-bot-filtrerede udbydere |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **IP-filtrering** | Tilladelsesliste/blokeringslistekontrol for udsatte implementeringer |
| 📊 **Redigerbare satsgrænser** | Konfigurerbare grænser på globalt niveau/udbyderniveau med persistens |
| 🔑 **API Key Management + Scoping** | Sikker nøgleudstedelse/rotation og model-/leverandørkontrol |
| 🛡️ **Beskyttet `/models`** | Valgfri godkendelse og udbyderskjul til modelkatalog |
### 📊 Observerbarhed og analyse
+12
View File
@@ -879,6 +879,18 @@ Wenn OmniRoute minimiert ist, befindet es sich mit schnellen Aktionen in Ihrer T
OmniRoute v2.0 ist als Betriebsplattform konzipiert und nicht nur als Relay-Proxy.
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Agenten- und Protokolloperationen (v2.0)| Funktion | Was es tut |
| ------------------------------------ | -------------------------------------------------------------------------------- |
+12
View File
@@ -11,6 +11,18 @@ _Tu proxy de API universal — un endpoint, 36+ proveedores, cero tiempo de inac
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Proveedor de IA Gratuito para tus agentes de programación favoritos
_Conecta cualquier IDE o herramienta CLI con IA a través de OmniRoute — gateway de API gratuito para programación ilimitada._
+12
View File
@@ -11,6 +11,18 @@ _Universaali API-välityspalvelin yksi päätepiste, yli 36 palveluntarjoaja
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Ilmainen AI Provider suosikkikoodaajillesi
_Yhdistä mikä tahansa tekoälyllä toimiva IDE- tai CLI-työkalu OmniRouten kautta ilmainen API-yhdyskäytävä rajoittamattomaan koodaukseen._
+25 -12
View File
@@ -11,6 +11,18 @@ _Votre proxy API universel — un endpoint, 36+ fournisseurs, zéro temps d'arr
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Fournisseur IA gratuit pour vos agents de programmation préférés
_Connectez n'importe quel IDE ou outil CLI alimenté par l'IA via OmniRoute — passerelle API gratuite pour un codage illimité._
@@ -863,18 +875,19 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Résilience & Sécurité
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Ouverture/fermeture auto par fournisseur avec seuils configurables |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + sémaphore de rate-limit pour les fournisseurs avec clé API |
| 🧠 **Cache sémantique** | Cache à deux niveaux (signature + sémantique) réduit coût et latence |
| ⚡ **Idempotence des requêtes** | Fenêtre de dédup 5s pour les requêtes dupliquées |
| 🔒 **Spoofing TLS Fingerprint** | Contournement de détection de bot via wreq-js |
| 🌐 **Filtrage IP** | Allowlist/blocklist pour le contrôle d'accès API |
| 📊 **Rate limits éditables** | RPM configurable, intervalle minimum, concurrence max |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Ouverture/fermeture auto par fournisseur avec seuils configurables |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + sémaphore de rate-limit pour les fournisseurs avec clé API |
| 🧠 **Cache sémantique** | Cache à deux niveaux (signature + sémantique) réduit coût et latence |
| ⚡ **Idempotence des requêtes** | Fenêtre de dédup 5s pour les requêtes dupliquées |
| 🔒 **Spoofing TLS Fingerprint** | Contournement de détection de bot via wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Filtrage IP** | Allowlist/blocklist pour le contrôle d'accès API |
| 📊 **Rate limits éditables** | RPM configurable, intervalle minimum, concurrence max |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Observabilité & Analytique
+12
View File
@@ -11,6 +11,18 @@ _שרת ה-API האוניברסלי שלך - נקודת קצה אחת, 36+ ספ
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 ספק AI בחינם עבור סוכני הקידוד המועדפים עליך
_חבר כל כלי IDE או CLI המופעל על ידי AI דרך OmniRoute - שער API בחינם לקידוד בלתי מוגבל._
+12
View File
@@ -11,6 +11,18 @@ _Az univerzális API-proxy egy végpont, 36+ szolgáltató, nulla állásid
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Ingyenes mesterséges intelligencia szolgáltató kedvenc kódoló ügynökei számára
_Csatlakoztasson bármilyen mesterséges intelligencia-alapú IDE-t vagy CLI-eszközt az OmniRoute-on keresztül ingyenes API-átjáró a korlátlan kódoláshoz._
+12
View File
@@ -11,6 +11,18 @@ _Proksi API universal Anda — satu titik akhir, 36+ penyedia, tanpa waktu henti
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Penyedia AI gratis untuk agen coding favorit Anda
_Hubungkan alat IDE atau CLI apa pun yang didukung AI melalui OmniRoute — gerbang API gratis untuk pengkodean tanpa batas._
+12
View File
@@ -13,6 +13,18 @@ _आपका सार्वभौमिक एपीआई प्रॉक्
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 आपके पसंदीदा कोडिंग एजेंटों के लिए निःशुल्क एआई प्रदाता
_OmniRoute के माध्यम से किसी भी AI-संचालित IDE या CLI टूल को कनेक्ट करें - असीमित कोडिंग के लिए निःशुल्क API गेटवे।_
+25 -12
View File
@@ -11,6 +11,18 @@ _Il tuo proxy API universale — un endpoint, 36+ provider, zero downtime._
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Provider IA gratuito per i tuoi agenti di programmazione preferiti
_Connetti qualsiasi IDE o strumento CLI con IA tramite OmniRoute — gateway API gratuito per programmazione illimitata._
@@ -862,18 +874,19 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Resilienza & Sicurezza
| Funzionalità | Cosa Fa |
| ------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Apertura/chiusura auto per provider con soglie configurabili |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaforo rate-limit per provider con API key |
| 🧠 **Cache semantica** | Cache a due livelli (firma + semantica) riduce costi e latenza |
| ⚡ **Idempotenza richieste** | Finestra dedup 5s per richieste duplicate |
| 🔒 **Spoofing TLS Fingerprint** | Bypass rilevamento bot tramite wreq-js |
| 🌐 **Filtro IP** | Allowlist/blocklist per controllo accesso API |
| 📊 **Rate limit modificabili** | RPM, gap minimo e concorrenza massima configurabili |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| Funzionalità | Cosa Fa |
| ------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Apertura/chiusura auto per provider con soglie configurabili |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaforo rate-limit per provider con API key |
| 🧠 **Cache semantica** | Cache a due livelli (firma + semantica) riduce costi e latenza |
| ⚡ **Idempotenza richieste** | Finestra dedup 5s per richieste duplicate |
| 🔒 **Spoofing TLS Fingerprint** | Bypass rilevamento bot tramite wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Filtro IP** | Allowlist/blocklist per controllo accesso API |
| 📊 **Rate limit modificabili** | RPM, gap minimo e concorrenza massima configurabili |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Osservabilità & Analytics
+12
View File
@@ -11,6 +11,18 @@ _ユニバーサル API プロキシ — 1 つのエンドポイント、36 以
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 お気に入りのコーディング エージェント向けの無料 AI プロバイダー
_AI を活用した IDE または CLI ツールを、無制限のコーディングのための無料 API ゲートウェイである OmniRoute 経由で接続します。_
+12
View File
@@ -11,6 +11,18 @@ _범용 API 프록시 — 하나의 엔드포인트, 36개 이상의 공급자,
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 좋아하는 코딩 에이전트를 위한 무료 AI 제공업체
_무제한 코딩을 위한 무료 API 게이트웨이인 OmniRoute를 통해 AI 기반 IDE 또는 CLI 도구를 연결하세요._
+37 -21
View File
@@ -247,6 +247,7 @@ Providers like OpenAI/Codex block access from certain geographic regions. Users
- **Connection Tests via Proxy** — Connection tests use the configured proxy (no more direct bypass)
- **SOCKS5 Support** — Full SOCKS5 proxy support for outbound routing
- **TLS Fingerprint Spoofing** — Browser-like TLS fingerprint via `wreq-js` to bypass bot detection
- **🔏 CLI Fingerprint Matching** — Reorders headers and body fields to match native CLI binary signatures, drastically reducing account flagging risk. The proxy IP is preserved — you get both stealth **and** IP masking simultaneously
</details>
@@ -888,6 +889,18 @@ When minimized, OmniRoute lives in your system tray with quick actions:
OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw + 9 more), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Agent & Protocol Operations (v2.0)
| Feature | What It Does |
@@ -936,18 +949,19 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
### 🛡️ Resilience, Security & Governance
| Feature | What It Does |
| ----------------------------------- | ---------------------------------------------------------- |
| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events |
| 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers |
| ⚡ **Request Idempotency** | Duplicate protection window |
| 🔒 **TLS Fingerprint Spoofing** | Better compatibility with anti-bot filtered providers |
| 🌐 **IP Filtering** | Allowlist/blocklist control for exposed deployments |
| 📊 **Editable Rate Limits** | Configurable global/provider-level limits with persistence |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
| Feature | What It Does |
| ----------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breakers** | Per-model trip/recover with threshold controls |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore protections on retry/rate events |
| 🧠 **Semantic + Signature Cache** | Cost/latency reduction with two cache layers |
| ⚡ **Request Idempotency** | Duplicate protection window |
| 🔒 **TLS Fingerprint Spoofing** | Browser-like TLS fingerprint — **reduces bot detection and account flagging** |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **IP Filtering** | Allowlist/blocklist control for exposed deployments |
| 📊 **Editable Rate Limits** | Configurable global/provider-level limits with persistence |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
### 📊 Observability & Analytics
@@ -963,15 +977,17 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
### ☁️ Deployment & Platform
| Feature | What It Does |
| ---------------------------- | -------------------------------------------------------- |
| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloud environments |
| 💾 **Cloud Sync** | Configuration sync via cloud worker |
| 🔄 **Backup/Restore** | Export/import and disaster recovery flows |
| 🧙 **Onboarding Wizard** | First-run guided setup |
| 🔧 **CLI Tools Dashboard** | One-click setup for popular coding tools |
| 🌐 **i18n (30 languages)** | Full dashboard + docs language support with RTL coverage |
| 📂 **Custom Data Directory** | `DATA_DIR` override for storage location |
| Feature | What It Does |
| ----------------------------- | -------------------------------------------------------- |
| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloud environments |
| 💾 **Cloud Sync** | Configuration sync via cloud worker |
| 🔄 **Backup/Restore** | Export/import and disaster recovery flows |
| 🧙 **Onboarding Wizard** | First-run guided setup |
| 🔧 **CLI Tools Dashboard** | One-click setup for popular coding tools |
| 🎮 **Model Playground** | Test any provider/model/endpoint from the dashboard |
| 🔏 **CLI Fingerprint Toggle** | Per-provider fingerprint matching in Settings > Security |
| 🌐 **i18n (30 languages)** | Full dashboard + docs language support with RTL coverage |
| 📂 **Custom Data Directory** | `DATA_DIR` override for storage location |
### Feature Deep Dive
+27 -14
View File
@@ -11,6 +11,18 @@ _Proksi API universal anda — satu titik akhir, 36+ pembekal, masa henti sifar.
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Pembekal AI percuma untuk ejen pengekodan kegemaran anda
_Sambungkan mana-mana alat IDE atau CLI berkuasa AI melalui OmniRoute — get laluan API percuma untuk pengekodan tanpa had._
@@ -861,20 +873,21 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Ketahanan & Keselamatan
| Ciri | Apa yang Dilakukan |
| ----------------------------------- | ----------------------------------------------------------------------------------- |
| 🔌 **Pemutus Litar** | Auto buka/tutup setiap pembekal dengan ambang boleh dikonfigurasikan |
| 🛡️ **Kawanan Anti Guruh** | Had kadar Mutex + semaphore untuk pembekal kunci API |
| 🧠 **Cache Semantik** | Cache dua peringkat (tandatangan + semantik) mengurangkan kos & kependaman |
| ⚡ **Minta Idepotency** | Tetingkap pendua 5s untuk permintaan pendua |
| 🔒 **TLS Fingerprint Spoofing** | Pintas pengesanan bot berasaskan TLS melalui wreq-js |
| 🌐 **Penapisan IP** | Senarai kebenaran/senarai sekat untuk kawalan akses API |
| 📊 **Had Kadar Boleh Diedit** | RPM boleh dikonfigurasikan, jurang min dan serentak maksimum pada tahap sistem |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Perlindungan Titik Akhir API** | Gating pengesahan + penyekatan penyedia untuk titik akhir `/models` |
| 🔒 **Keterlihatan Proksi** | Lencana berkod warna: 🟢 global, 🟡 pembekal, 🔵 setiap sambungan dengan paparan IP |
| 🌐 **Konfigurasi Proksi 3 Tahap** | Konfigurasikan proksi pada peringkat global, setiap pembekal atau setiap sambungan |
| Ciri | Apa yang Dilakukan |
| ----------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Pemutus Litar** | Auto buka/tutup setiap pembekal dengan ambang boleh dikonfigurasikan |
| 🛡️ **Kawanan Anti Guruh** | Had kadar Mutex + semaphore untuk pembekal kunci API |
| 🧠 **Cache Semantik** | Cache dua peringkat (tandatangan + semantik) mengurangkan kos & kependaman |
| ⚡ **Minta Idepotency** | Tetingkap pendua 5s untuk permintaan pendua |
| 🔒 **TLS Fingerprint Spoofing** | Pintas pengesanan bot berasaskan TLS melalui wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Penapisan IP** | Senarai kebenaran/senarai sekat untuk kawalan akses API |
| 📊 **Had Kadar Boleh Diedit** | RPM boleh dikonfigurasikan, jurang min dan serentak maksimum pada tahap sistem |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Perlindungan Titik Akhir API** | Gating pengesahan + penyekatan penyedia untuk titik akhir `/models` |
| 🔒 **Keterlihatan Proksi** | Lencana berkod warna: 🟢 global, 🟡 pembekal, 🔵 setiap sambungan dengan paparan IP |
| 🌐 **Konfigurasi Proksi 3 Tahap** | Konfigurasikan proksi pada peringkat global, setiap pembekal atau setiap sambungan |
### 📊 Kebolehlihatan & Analitis
+12
View File
@@ -11,6 +11,18 @@ _Uw universele API-proxy: één eindpunt, meer dan 36 providers, geen downtime._
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Gratis AI-provider voor uw favoriete codeeragenten
_Verbind elke AI-aangedreven IDE- of CLI-tool via OmniRoute: gratis API-gateway voor onbeperkte codering._
+12
View File
@@ -11,6 +11,18 @@ _Din universelle API-proxy ett endepunkt, 36+ leverandører, null nedetid._
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Gratis AI-leverandør for dine favorittkodeagenter
_Koble til ethvert AI-drevet IDE- eller CLI-verktøy gjennom OmniRoute gratis API-gateway for ubegrenset koding._
+28 -15
View File
@@ -11,6 +11,18 @@ _Iyong unibersal na API proxy — isang endpoint, 36+ provider, zero downtime._
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Libreng AI Provider para sa iyong mga paboritong coding agent
_Ikonekta ang anumang AI-powered IDE o CLI tool sa pamamagitan ng OmniRoute — libreng API gateway para sa walang limitasyong coding._
@@ -861,21 +873,22 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Katatagan at Seguridad
| Tampok | Ano ang Ginagawa Nito |
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Awtomatikong buksan/isara ang bawat provider na may mga na-configure na threshold |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore rate-limit para sa mga API key provider |
| 🧠 **Semantic Cache** | Binabawasan ng two-tier na cache (pirma + semantiko) ang gastos at latency |
| ⚡ **Humiling ng Idempotency** | 5s dedup window para sa mga duplicate na kahilingan |
| 🔒 **TLS Fingerprint Spoofing** | I-bypass ang TLS-based na bot detection sa pamamagitan ng wreq-js |
| 🌐 **Pag-filter ng IP** | Allowlist/blocklist para sa API access control |
| 📊 **Mga Nae-edit na Limitasyon sa Rate** | Configurable RPM, min gap, at max na kasabay sa antas ng system |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Proteksyon sa Endpoint ng API** | Auth gating + pagharang ng provider para sa `/models` endpoint |
| 🔒 **Proxy Visibility** | Mga color-coded na badge: 🟢 global, 🟡 provider, 🔵 per-connection na may IP display |
| 🌐 **3-Level Proxy Config** | I-configure ang mga proxy sa global, per-provider, o per-connection level |
| Tampok | Ano ang Ginagawa Nito |
| ----------------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Awtomatikong buksan/isara ang bawat provider na may mga na-configure na threshold |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore rate-limit para sa mga API key provider |
| 🧠 **Semantic Cache** | Binabawasan ng two-tier na cache (pirma + semantiko) ang gastos at latency |
| ⚡ **Humiling ng Idempotency** | 5s dedup window para sa mga duplicate na kahilingan |
| 🔒 **TLS Fingerprint Spoofing** | I-bypass ang TLS-based na bot detection sa pamamagitan ng wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Pag-filter ng IP** | Allowlist/blocklist para sa API access control |
| 📊 **Mga Nae-edit na Limitasyon sa Rate** | Configurable RPM, min gap, at max na kasabay sa antas ng system |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Proteksyon sa Endpoint ng API** | Auth gating + pagharang ng provider para sa `/models` endpoint |
| 🔒 **Proxy Visibility** | Mga color-coded na badge: 🟢 global, 🟡 provider, 🔵 per-connection na may IP display |
| 🌐 **3-Level Proxy Config** | I-configure ang mga proxy sa global, per-provider, o per-connection level |
### 📊 Pagmamasid at Analytics
+12
View File
@@ -11,6 +11,18 @@ _Twój uniwersalny serwer proxy API — jeden punkt końcowy, ponad 36 dostawcó
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Bezpłatny dostawca AI dla Twoich ulubionych agentów kodujących
_Połącz dowolne narzędzie IDE lub CLI oparte na sztucznej inteligencji poprzez OmniRoute — bezpłatną bramę API dla nieograniczonego kodowania._
+12
View File
@@ -11,6 +11,18 @@ _Seu proxy de API universal — um endpoint, 36+ provedores, zero tempo de inati
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Provedor de IA Gratuito para seus agentes de programação favoritos
_Conecte qualquer IDE ou ferramenta CLI com IA através do OmniRoute — gateway de API gratuito para programação ilimitada._
+12
View File
@@ -11,6 +11,18 @@ _Seu proxy de API universal — um endpoint, mais de 36 provedores, tempo de ina
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Provedor de IA gratuito para seus agentes de codificação favoritos
_Conecte qualquer ferramenta IDE ou CLI com tecnologia de IA por meio do OmniRoute - gateway de API gratuito para codificação ilimitada._
+27 -14
View File
@@ -11,6 +11,18 @@ _Proxy-ul dvs. universal API - un punct final, peste 36 de furnizori, zero timpi
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Furnizor AI gratuit pentru agenții tăi preferați de codare
_Conectați orice instrument IDE sau CLI alimentat de AI prin OmniRoute — gateway API gratuit pentru codare nelimitată._
@@ -863,20 +875,21 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Reziliență și securitate
| Caracteristica | Ce face |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| 🔌 **Disjunctor** | Deschidere/închidere automată pentru fiecare furnizor cu praguri configurabile |
| 🛡️ **Turmă Anti-Tunete** | Limită de rată Mutex + semafor pentru furnizorii de chei API |
| 🧠 **Cache semantic** | Cache-ul pe două niveluri (semnătură + semantică) reduce costurile și latența |
| ⚡ **Solicita Idempotenta** | Fereastra de dedup 5s pentru cereri duplicate |
| 🔒 **TLS Fingerprint Spoofing** | Ocoliți detectarea botului bazată pe TLS prin wreq-js |
| 🌐 **Filtrare IP** | Lista permisă/lista blocată pentru controlul accesului API |
| 📊 **Limite de rată editabile** | RPM configurabil, interval minim și concurență maximă la nivel de sistem |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Protecție API Endpoint** | Autentificare + blocare furnizor pentru punctul final `/models` |
| 🔒 **Vizibilitatea proxy** | Ecusoane cu coduri de culoare: 🟢 global, 🟡 furnizor, 🔵 per conexiune cu afișaj IP |
| 🌐 **Configurare proxy pe 3 niveluri** | Configurați proxy-uri la nivel global, per furnizor sau per conexiune |
| Caracteristica | Ce face |
| -------------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Disjunctor** | Deschidere/închidere automată pentru fiecare furnizor cu praguri configurabile |
| 🛡️ **Turmă Anti-Tunete** | Limită de rată Mutex + semafor pentru furnizorii de chei API |
| 🧠 **Cache semantic** | Cache-ul pe două niveluri (semnătură + semantică) reduce costurile și latența |
| ⚡ **Solicita Idempotenta** | Fereastra de dedup 5s pentru cereri duplicate |
| 🔒 **TLS Fingerprint Spoofing** | Ocoliți detectarea botului bazată pe TLS prin wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Filtrare IP** | Lista permisă/lista blocată pentru controlul accesului API |
| 📊 **Limite de rată editabile** | RPM configurabil, interval minim și concurență maximă la nivel de sistem |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **Protecție API Endpoint** | Autentificare + blocare furnizor pentru punctul final `/models` |
| 🔒 **Vizibilitatea proxy** | Ecusoane cu coduri de culoare: 🟢 global, 🟡 furnizor, 🔵 per conexiune cu afișaj IP |
| 🌐 **Configurare proxy pe 3 niveluri** | Configurați proxy-uri la nivel global, per furnizor sau per conexiune |
### 📊 Observabilitate și analiză
+25 -12
View File
@@ -11,6 +11,18 @@ _Ваш универсальный API-прокси — одна точка до
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Бесплатный AI-провайдер для ваших любимых агентов программирования
_Подключайте любую IDE или CLI-инструмент с AI через OmniRoute — бесплатный API gateway для неограниченного программирования._
@@ -861,18 +873,19 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Устойчивость и безопасность
| Функция | Что делает |
| -------------------------------- | ---------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Авто-открытие/закрытие по провайдеру с настраиваемыми порогами |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + семафор для API key провайдеров |
| 🧠 **Семантический кеш** | Двухуровневый кеш (сигнатура + семантика) снижает стоимость |
| ⚡ **Идемпотентность запросов** | 5с окно дедупликации для дублирующихся запросов |
| 🔒 **Спуфинг TLS Fingerprint** | Обход обнаружения ботов через wreq-js |
| 🌐 **Фильтрация IP** | Allowlist/blocklist для контроля доступа к API |
| 📊 **Настраиваемые Rate Limits** | Настраиваемые RPM, минимальный интервал, макс. конкуррентность |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| Функция | Что делает |
| -------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Авто-открытие/закрытие по провайдеру с настраиваемыми порогами |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-Thundering Herd** | Mutex + семафор для API key провайдеров |
| 🧠 **Семантический кеш** | Двухуровневый кеш (сигнатура + семантика) снижает стоимость |
| ⚡ **Идемпотентность запросов** | 5с окно дедупликации для дублирующихся запросов |
| 🔒 **Спуфинг TLS Fingerprint** | Обход обнаружения ботов через wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **Фильтрация IP** | Allowlist/blocklist для контроля доступа к API |
| 📊 **Настраиваемые Rate Limits** | Настраиваемые RPM, минимальный интервал, макс. конкуррентность |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
### 📊 Наблюдаемость и аналитика
+12
View File
@@ -11,6 +11,18 @@ _Váš univerzálny proxy server API jeden koncový bod, 36+ poskytovateľov
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Bezplatný poskytovateľ AI pre vašich obľúbených kódovacích agentov
_Pripojte akýkoľvek nástroj IDE alebo CLI poháňaný AI cez OmniRoute bezplatnú bránu API pre neobmedzené kódovanie._
+28 -15
View File
@@ -11,6 +11,18 @@ _Din universella API-proxy — en slutpunkt, 36+ leverantörer, noll driftstopp.
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Gratis AI-leverantör för dina favoritkodningsagenter
_Anslut alla AI-drivna IDE- eller CLI-verktyg via OmniRoute — gratis API-gateway för obegränsad kodning._
@@ -861,21 +873,22 @@ npm run electron:build:linux # Linux (.AppImage)
### 🛡️ Motståndskraft och säkerhet
| Funktion | Vad det gör |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Autoöppna/stäng per leverantör med konfigurerbara trösklar |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-ånflock** | Mutex + semaforhastighetsgräns för API-nyckelleverantörer |
| 🧠 **Semantisk cache** | Tvåskiktscache (signatur + semantisk) minskar kostnaden och fördröjningen |
| ⚡ **Begär idempotens** | 5s dedup-fönster för dubblettförfrågningar |
| 🔒 **TLS Fingerprint Spoofing** | Förbi TLS-baserad botdetektering via wreq-js |
| 🌐 **IP-filtrering** | Tillåtelselista/blockeringslista för API-åtkomstkontroll |
| 📊 **Redigerbara hastighetsgränser** | Konfigurerbart RPM, min gap och max samtidiga på systemnivå |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **API Endpoint Protection** | Auth gating + leverantörsblockering för `/models` slutpunkt |
| 🔒 **Proxysynlighet** | Färgkodade märken: 🟢 global, 🟡 leverantör, 🔵 per anslutning med IP-display |
| 🌐 **Proxykonfiguration med 3 nivåer** | Konfigurera proxyservrar på global nivå, per leverantör eller per anslutningsnivå |
| Funktion | Vad det gör |
| -------------------------------------- | -------------------------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Autoöppna/stäng per leverantör med konfigurerbara trösklar |
| 🎯 **Endpoint-Aware Models** | Custom models declare supported endpoints + API format |
| 🛡️ **Anti-ånflock** | Mutex + semaforhastighetsgräns för API-nyckelleverantörer |
| 🧠 **Semantisk cache** | Tvåskiktscache (signatur + semantisk) minskar kostnaden och fördröjningen |
| ⚡ **Begär idempotens** | 5s dedup-fönster för dubblettförfrågningar |
| 🔒 **TLS Fingerprint Spoofing** | Förbi TLS-baserad botdetektering via wreq-js |
| 🔏 **CLI Fingerprint Matching** | Matches native CLI request signatures — **reduces ban risk while preserving proxy IP** |
| 🌐 **IP-filtrering** | Tillåtelselista/blockeringslista för API-åtkomstkontroll |
| 📊 **Redigerbara hastighetsgränser** | Konfigurerbart RPM, min gap och max samtidiga på systemnivå |
| 💾 **Rate Limit Persistence** | Learned limits survive restarts via SQLite with 60s debounce + 24h staleness |
| 🔄 **Token Refresh Resilience** | Per-provider circuit breaker (5 fails→30min) + 30s timeout per attempt |
| 🛡 **API Endpoint Protection** | Auth gating + leverantörsblockering för `/models` slutpunkt |
| 🔒 **Proxysynlighet** | Färgkodade märken: 🟢 global, 🟡 leverantör, 🔵 per anslutning med IP-display |
| 🌐 **Proxykonfiguration med 3 nivåer** | Konfigurera proxyservrar på global nivå, per leverantör eller per anslutningsnivå |
### 📊 Observerbarhet och analys
+12
View File
@@ -11,6 +11,18 @@ _พร็อกซี API สากลของคุณ — จุดสิ้
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 ผู้ให้บริการ AI ฟรีสำหรับตัวแทนการเขียนโค้ดที่คุณชื่นชอบ
_เชื่อมต่อเครื่องมือ IDE หรือ CLI ที่ขับเคลื่อนด้วย AI ผ่าน OmniRoute — เกตเวย์ API ฟรีสำหรับการเข้ารหัสไม่จำกัด_
+12
View File
@@ -11,6 +11,18 @@ _Ваш універсальний API-проксі — одна кінцева
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Безкоштовний постачальник AI для ваших улюблених агентів кодування
_Підключіть будь-який інструмент IDE або CLI на основі штучного інтелекту через OmniRoute — безкоштовний шлюз API для необмеженого програмування._
+12
View File
@@ -11,6 +11,18 @@ _Proxy API phổ quát của bạn — một điểm cuối, hơn 36 nhà cung c
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 Nhà cung cấp AI miễn phí cho các tác nhân mã hóa yêu thích của bạn
_Kết nối mọi công cụ IDE hoặc CLI được hỗ trợ bởi AI thông qua OmniRoute — cổng API miễn phí để mã hóa không giới hạn._
+12
View File
@@ -11,6 +11,18 @@ _您的通用 API 代理 — 一个端点,36+ 提供商,零停机时间。_
---
### 🚀 New in v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 🎮 **Model Playground** | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 **CLI Fingerprint Matching** | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. **Your proxy IP is preserved** |
| 🤝 **ACP Support (Agent Client Protocol)** | CLI agent discovery (Codex, Claude, Goose, Gemini CLI, OpenClaw), process spawner, `/api/acp/agents` endpoint |
| 🤖 **ACP Agents Dashboard** | Debug > Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool |
| 🔧 **Custom Model `apiFormat` Routing** | Custom models with `apiFormat: "responses"` now correctly route to the Responses API translator |
| 🏢 **Codex Workspace Isolation** | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 **Electron Auto-Update** | Desktop app checks for updates + auto-install on restart |
### 🤖 为您最爱的编程代理提供免费 AI
_通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API 网关,无限编程。_
+121
View File
@@ -26,10 +26,12 @@ const {
nativeImage,
shell,
session,
Notification,
} = require("electron");
const path = require("path");
const { spawn } = require("child_process");
const fs = require("fs");
const { autoUpdater } = require("electron-updater");
// ── Single Instance Lock ───────────────────────────────────
const gotTheLock = app.requestSingleInstanceLock();
@@ -62,6 +64,11 @@ let serverPort = 20128;
const getServerUrl = () => `http://localhost:${serverPort}`;
// ── Auto-Updater Configuration ──────────────────────────────
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.logger = console;
// ── Helper: Send IPC event to renderer (#5) ────────────────
function sendToRenderer(channel, data) {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -103,6 +110,77 @@ async function waitForServerExit(proc, timeoutMs = 5000) {
]);
}
// ── Auto-Updater Event Handlers ─────────────────────────────
function setupAutoUpdater() {
autoUpdater.on("checking-for-update", () => {
sendToRenderer("update-status", { status: "checking" });
console.log("[Electron] Checking for updates...");
});
autoUpdater.on("update-available", (info) => {
sendToRenderer("update-status", { status: "available", version: info.version });
console.log("[Electron] Update available:", info.version);
});
autoUpdater.on("update-not-available", (info) => {
sendToRenderer("update-status", { status: "not-available", version: info.version });
console.log("[Electron] No update available");
});
autoUpdater.on("download-progress", (progress) => {
sendToRenderer("update-status", {
status: "downloading",
percent: Math.round(progress.percent),
transferred: progress.transferred,
total: progress.total,
});
});
autoUpdater.on("update-downloaded", (info) => {
sendToRenderer("update-status", { status: "downloaded", version: info.version });
console.log("[Electron] Update downloaded:", info.version);
if (Notification.isSupported()) {
const notification = new Notification({
title: "OmniRoute Update Ready",
body: `Version ${info.version} is ready to install. Click to restart.`,
});
notification.on("click", () => {
autoUpdater.quitAndInstall();
});
notification.show();
}
});
autoUpdater.on("error", (error) => {
sendToRenderer("update-status", { status: "error", message: error.message });
console.error("[Electron] Update error:", error);
});
}
async function checkForUpdates(silent = false) {
if (isDev) {
console.log("[Electron] Dev mode — skipping auto-update");
if (!silent) {
sendToRenderer("update-status", { status: "error", message: "Updates disabled in dev mode" });
}
return;
}
await autoUpdater.checkForUpdates();
}
async function downloadUpdate() {
await autoUpdater.downloadUpdate();
}
function installUpdate() {
if (nextServer) {
nextServer.kill("SIGTERM");
nextServer = null;
}
autoUpdater.quitAndInstall();
}
// ── Content Security Policy (#15) ──────────────────────────
function setupContentSecurityPolicy() {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
@@ -236,6 +314,11 @@ function createTray() {
],
},
{ type: "separator" },
{
label: "Check for Updates",
click: () => checkForUpdates(false),
},
{ type: "separator" },
{
label: "Quit",
click: () => {
@@ -391,6 +474,36 @@ function setupIpcHandlers() {
});
ipcMain.on("window-close", () => mainWindow?.close());
// Auto-update IPC handlers
ipcMain.handle("check-for-updates", async () => {
try {
await checkForUpdates(false);
return { success: true };
} catch (error) {
console.error("[Electron] Check for updates failed:", error);
sendToRenderer("update-status", { status: "error", message: error.message });
return { success: false, error: error.message };
}
});
ipcMain.handle("download-update", async () => {
try {
await downloadUpdate();
return { success: true };
} catch (error) {
console.error("[Electron] Download update failed:", error);
sendToRenderer("update-status", { status: "error", message: error.message });
return { success: false, error: error.message };
}
});
ipcMain.handle("install-update", () => {
installUpdate();
// No return value — app will quit and restart
});
ipcMain.handle("get-app-version", () => app.getVersion());
}
// ── App Lifecycle ──────────────────────────────────────────
@@ -407,6 +520,14 @@ app.whenReady().then(async () => {
createWindow();
createTray();
setupIpcHandlers();
setupAutoUpdater();
// Check for updates after a short delay (don't block startup)
if (!isDev) {
setTimeout(() => {
checkForUpdates(true);
}, 3000);
}
// macOS: recreate window when dock icon clicked
app.on("activate", () => {
+8 -1
View File
@@ -15,7 +15,9 @@
"build:linux": "electron-builder --linux",
"pack": "electron-builder --dir"
},
"dependencies": {},
"dependencies": {
"electron-updater": "^6.8.3"
},
"devDependencies": {
"electron": "^40.6.1",
"electron-builder": "^25.1.8"
@@ -28,6 +30,11 @@
"output": "dist-electron",
"buildResources": "assets"
},
"publish": {
"provider": "github",
"owner": "diegosouzapw",
"repo": "OmniRoute"
},
"files": [
"main.js",
"preload.js",
+18 -2
View File
@@ -13,9 +13,18 @@ const { contextBridge, ipcRenderer } = require("electron");
// ── Channel Whitelist ──────────────────────────────────────
const VALID_CHANNELS = {
invoke: ["get-app-info", "open-external", "get-data-dir", "restart-server"],
invoke: [
"get-app-info",
"open-external",
"get-data-dir",
"restart-server",
"check-for-updates",
"download-update",
"install-update",
"get-app-version",
],
send: ["window-minimize", "window-maximize", "window-close"],
receive: ["server-status", "port-changed"],
receive: ["server-status", "port-changed", "update-status"],
};
// ── Fix #16: Generic IPC wrappers ──────────────────────────
@@ -48,6 +57,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
openExternal: (url) => safeInvoke("open-external", url),
getDataDir: () => safeInvoke("get-data-dir"),
restartServer: () => safeInvoke("restart-server"),
getAppVersion: () => safeInvoke("get-app-version"),
// ── Auto-Update ──────────────────────────────────────────
checkForUpdates: () => safeInvoke("check-for-updates"),
downloadUpdate: () => safeInvoke("download-update"),
installUpdate: () => safeInvoke("install-update"),
// ── Send (fire-and-forget) ───────────────────────────────
minimizeWindow: () => safeSend("window-minimize"),
@@ -58,6 +73,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
// Fix #6: Returns a disposer function for precise cleanup
onServerStatus: (callback) => safeOn("server-status", callback),
onPortChanged: (callback) => safeOn("port-changed", callback),
onUpdateStatus: (callback) => safeOn("update-status", callback),
// ── Static Properties ────────────────────────────────────
isElectron: true,
+1 -1
View File
@@ -6,7 +6,7 @@ const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
const nextConfig = {
turbopack: {},
output: "standalone",
serverExternalPackages: ["better-sqlite3"],
serverExternalPackages: ["better-sqlite3", "zod"],
transpilePackages: ["@omniroute/open-sse"],
allowedDevOrigins: ["192.168.*"],
typescript: {
+264
View File
@@ -0,0 +1,264 @@
/**
* CLI Fingerprint Definitions
*
* Defines per-provider "fingerprints" that control the exact ordering of HTTP headers
* and JSON body fields to match the native CLI tools exactly.
*
* When `cliCompatMode` is enabled for a provider, OmniRoute reorders outgoing requests
* to be indistinguishable from the real CLI binary, reducing account flagging risk.
*
* Header order and body field order were captured via mitmproxy traffic analysis.
*/
export interface CliFingerprint {
/** Ordered list of header names (case-sensitive). Unlisted headers are appended. */
headerOrder: string[];
/** Ordered list of top-level JSON body fields. Unlisted fields are appended. */
bodyFieldOrder: string[];
/** User-Agent string to inject (overrides default) */
userAgent?: string;
/** Extra headers to add */
extraHeaders?: Record<string, string>;
}
/**
* Fingerprint registry - keyed by provider alias (lowercase).
* Based on mitmproxy traffic captures from native CLI tools.
*/
export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
codex: {
headerOrder: [
"Host",
"Content-Type",
"Authorization",
"Accept",
"User-Agent",
"Accept-Encoding",
],
bodyFieldOrder: [
"model",
"messages",
"temperature",
"top_p",
"max_tokens",
"stream",
"tools",
"tool_choice",
"response_format",
"n",
"stop",
],
userAgent: "codex-cli",
},
claude: {
headerOrder: [
"Host",
"Content-Type",
"x-api-key",
"anthropic-version",
"Accept",
"User-Agent",
"Accept-Encoding",
],
bodyFieldOrder: [
"model",
"max_tokens",
"messages",
"system",
"temperature",
"top_p",
"top_k",
"stream",
"tools",
"tool_choice",
"metadata",
],
userAgent: "claude-code",
},
github: {
headerOrder: [
"Host",
"Authorization",
"X-Request-Id",
"Vscode-Sessionid",
"Vscode-Machineid",
"Editor-Version",
"Editor-Plugin-Version",
"Copilot-Integration-Id",
"Openai-Organization",
"Openai-Intent",
"Content-Type",
"User-Agent",
"Accept",
"Accept-Encoding",
],
bodyFieldOrder: [
"messages",
"model",
"temperature",
"top_p",
"max_tokens",
"n",
"stream",
"intent",
"intent_threshold",
"intent_content",
],
userAgent: "GitHubCopilotChat",
},
antigravity: {
headerOrder: [
"Host",
"Content-Type",
"Authorization",
"User-Agent",
"Accept",
"Accept-Encoding",
],
bodyFieldOrder: ["project", "model", "userAgent", "requestType", "requestId", "request"],
userAgent: "antigravity",
},
};
/**
* Reorder an object's keys according to a specified order.
* Keys not in the order list are appended at the end in their original order.
*/
export function orderFields<T extends Record<string, unknown>>(obj: T, fieldOrder: string[]): T {
if (!fieldOrder?.length || !obj || typeof obj !== "object") return obj;
const result: Record<string, unknown> = {};
const remaining = new Set(Object.keys(obj));
// First, add fields in the specified order
for (const key of fieldOrder) {
if (key in obj) {
result[key] = obj[key];
remaining.delete(key);
}
}
// Then append remaining fields in original order
for (const key of remaining) {
result[key] = obj[key];
}
return result as T;
}
/**
* Reorder HTTP headers according to a fingerprint.
* Returns a new object with headers in the specified order.
*/
export function orderHeaders(
headers: Record<string, string>,
headerOrder: string[]
): Record<string, string> {
if (!headerOrder?.length || !headers) return headers;
const result: Record<string, string> = {};
const remaining = new Map<string, string>();
// Build case-insensitive lookup
const headerMap = new Map<string, [string, string]>();
for (const [key, value] of Object.entries(headers)) {
headerMap.set(key.toLowerCase(), [key, value]);
}
// Add ordered headers first
for (const orderedKey of headerOrder) {
const entry = headerMap.get(orderedKey.toLowerCase());
if (entry) {
result[entry[0]] = entry[1];
headerMap.delete(orderedKey.toLowerCase());
}
}
// Add remaining headers
for (const [, [key, value]] of headerMap) {
result[key] = value;
}
return result;
}
/**
* Apply a CLI fingerprint to headers and body.
* Returns { headers, bodyString } with the correct ordering.
*/
export function applyFingerprint(
provider: string,
headers: Record<string, string>,
body: unknown
): { headers: Record<string, string>; bodyString: string } {
const fingerprint = CLI_FINGERPRINTS[provider?.toLowerCase()];
if (!fingerprint) {
return { headers, bodyString: JSON.stringify(body) };
}
// Apply user agent override
if (fingerprint.userAgent) {
headers["User-Agent"] = fingerprint.userAgent;
}
// Apply extra headers
if (fingerprint.extraHeaders) {
Object.assign(headers, fingerprint.extraHeaders);
}
// Reorder headers
const orderedHeaders = orderHeaders(headers, fingerprint.headerOrder);
// Reorder body fields
const orderedBody =
body && typeof body === "object" && !Array.isArray(body)
? orderFields(body as Record<string, unknown>, fingerprint.bodyFieldOrder)
: body;
return {
headers: orderedHeaders,
bodyString: JSON.stringify(orderedBody),
};
}
/**
* Runtime cache for CLI compat providers set via Settings UI.
* Updated by the settings API when users toggle providers.
*/
let _cliCompatProviders: Set<string> = new Set();
/**
* Update the runtime cache of CLI-compat-enabled providers.
* Called from the settings API when cliCompatProviders is updated.
*/
export function setCliCompatProviders(providers: string[]): void {
_cliCompatProviders = new Set((providers || []).map((p) => p.toLowerCase()));
}
/**
* Get the current list of CLI-compat-enabled providers.
*/
export function getCliCompatProviders(): string[] {
return Array.from(_cliCompatProviders);
}
/**
* Check if CLI compatibility mode is enabled for a provider.
* Reads from: 1) Runtime cache (Settings UI), 2) Environment variables.
*/
export function isCliCompatEnabled(provider: string): boolean {
const key = provider?.toLowerCase().replace(/[^a-z0-9]/g, "_");
// 1. Check runtime cache (set via Settings UI)
if (_cliCompatProviders.has(provider?.toLowerCase())) return true;
// 2. Check environment variable: CLI_COMPAT_<PROVIDER>=1
const envKey = `CLI_COMPAT_${key?.toUpperCase()}`;
if (process.env[envKey] === "1" || process.env[envKey] === "true") return true;
// 3. Global enable: CLI_COMPAT_ALL=1
if (process.env.CLI_COMPAT_ALL === "1" || process.env.CLI_COMPAT_ALL === "true") return true;
return false;
}
+13 -2
View File
@@ -1,4 +1,5 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
type JsonRecord = Record<string, unknown>;
@@ -192,10 +193,20 @@ export class BaseExecutor {
? mergeAbortSignals(signal, timeoutSignal)
: signal || timeoutSignal;
// Apply CLI fingerprint ordering if enabled for this provider
let finalHeaders = headers;
let bodyString = JSON.stringify(transformedBody);
if (isCliCompatEnabled(this.provider)) {
const fingerprinted = applyFingerprint(this.provider, headers, transformedBody);
finalHeaders = fingerprinted.headers;
bodyString = fingerprinted.bodyString;
}
const fetchOptions: RequestInit = {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
headers: finalHeaders,
body: bodyString,
};
if (combinedSignal) fetchOptions.signal = combinedSignal;
+50 -6
View File
@@ -30,9 +30,23 @@ import {
* @param {object} options.body - Request body
* @param {object} options.credentials - Provider credentials { apiKey, accessToken }
* @param {object} options.log - Logger
* @param {string} [options.resolvedProvider] - Pre-resolved provider ID (from route layer custom model resolution)
*/
export async function handleImageGeneration({ body, credentials, log }) {
const { provider, model } = parseImageModel(body.model);
export async function handleImageGeneration({ body, credentials, log, resolvedProvider = null }) {
let provider, model;
if (resolvedProvider) {
// Provider was already resolved by the route layer (custom model from DB)
// Extract model name from the full "provider/model" string
provider = resolvedProvider;
const modelStr = body.model || "";
model = modelStr.startsWith(provider + "/") ? modelStr.slice(provider.length + 1) : modelStr;
} else {
// Standard path: resolve from built-in image registry
const parsed = parseImageModel(body.model);
provider = parsed.provider;
model = parsed.model;
}
if (!provider) {
return {
@@ -43,12 +57,42 @@ export async function handleImageGeneration({ body, credentials, log }) {
}
const providerConfig = getImageProvider(provider);
// For custom models without a built-in provider config, use OpenAI-compatible handler
// with a synthetic config based on the provider's credentials
if (!providerConfig) {
return {
success: false,
status: 400,
error: `Unknown image provider: ${provider}`,
if (!resolvedProvider) {
return {
success: false,
status: 400,
error: `Unknown image provider: ${provider}`,
};
}
// Custom model: use OpenAI-compatible format with provider's base URL
// The credentials were already resolved by the route layer
if (log) {
log.info("IMAGE", `Custom model ${provider}/${model} — using OpenAI-compatible handler`);
}
const syntheticConfig = {
id: provider,
baseUrl:
credentials?.baseUrl ||
`https://generativelanguage.googleapis.com/v1beta/openai/images/generations`,
authType: "apikey",
authHeader: "bearer",
format: "openai",
};
return handleOpenAIImageGeneration({
model,
provider,
providerConfig: syntheticConfig,
body,
credentials,
log,
});
}
// Route to format-specific handler
+19 -16
View File
@@ -3,6 +3,7 @@
*/
import { PROVIDERS } from "../config/constants.ts";
import { safePercentage } from "@/shared/utils/formatting";
// GitHub API config
const GITHUB_CONFIG = {
@@ -34,6 +35,7 @@ const CLAUDE_CONFIG = {
oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage",
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
settingsUrl: "https://api.anthropic.com/v1/settings",
apiVersion: "2023-06-01",
};
type JsonRecord = Record<string, unknown>;
@@ -469,7 +471,7 @@ async function getClaudeUsage(accessToken) {
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-beta": "oauth-2025-04-20",
"anthropic-version": "2023-06-01",
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
});
@@ -477,36 +479,34 @@ async function getClaudeUsage(accessToken) {
const data = await oauthResponse.json();
const quotas: Record<string, any> = {};
// utilization = percentage USED (e.g., 22 means 22% used, 78% remaining)
// utilization = percentage REMAINING (e.g., 90 means 90% remaining, 10% used)
const hasUtilization = (window: any) =>
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
const createQuotaObject = (window: any) => {
const used = window?.utilization ?? 0;
const remaining = 100 - used;
const remaining = safePercentage(window.utilization) as number;
const used = 100 - remaining;
return {
used,
total: 100,
remaining,
resetAt: parseResetTime(window?.resets_at),
resetAt: parseResetTime(window.resets_at),
remainingPercentage: remaining,
unlimited: false,
};
};
if (data.five_hour && typeof data.five_hour === "object") {
if (hasUtilization(data.five_hour)) {
quotas["session (5h)"] = createQuotaObject(data.five_hour);
}
if (data.seven_day && typeof data.seven_day === "object") {
if (hasUtilization(data.seven_day)) {
quotas["weekly (7d)"] = createQuotaObject(data.seven_day);
}
// Parse model-specific weekly windows (e.g., seven_day_sonnet, seven_day_opus)
for (const [key, value] of Object.entries(data)) {
if (
key.startsWith("seven_day_") &&
key !== "seven_day" &&
value &&
typeof value === "object"
) {
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) {
const modelName = key.replace("seven_day_", "");
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value);
}
@@ -519,7 +519,10 @@ async function getClaudeUsage(accessToken) {
};
}
// Fallback: Try legacy settings/org endpoint (for API key users with org admin access)
// Fallback: OAuth endpoint returned non-OK, try legacy settings/org endpoint
console.warn(
`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`
);
return await getClaudeUsageLegacy(accessToken);
} catch (error) {
return { message: `Claude connected. Unable to fetch usage: ${(error as any).message}` };
@@ -536,7 +539,7 @@ async function getClaudeUsageLegacy(accessToken) {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": "2023-06-01",
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
});
@@ -550,7 +553,7 @@ async function getClaudeUsageLegacy(accessToken) {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"anthropic-version": "2023-06-01",
"anthropic-version": CLAUDE_CONFIG.apiVersion,
},
}
);
+9 -5
View File
@@ -185,15 +185,19 @@ export function prepareClaudeRequest(body, provider = null) {
}
}
// 3. Tools: remove all cache_control, add only to last tool with ttl 1h
// 3. Tools: remove all cache_control, add only to last non-deferred tool with ttl 1h
// Tools with defer_loading=true cannot have cache_control (API rejects it)
if (body.tools && Array.isArray(body.tools)) {
body.tools = body.tools.map((tool, i) => {
body.tools = body.tools.map((tool) => {
const { cache_control, ...rest } = tool;
if (i === body.tools.length - 1) {
return { ...rest, cache_control: { type: "ephemeral", ttl: "1h" } };
}
return rest;
});
for (let i = body.tools.length - 1; i >= 0; i--) {
if (!body.tools[i].defer_loading) {
body.tools[i].cache_control = { type: "ephemeral", ttl: "1h" };
break;
}
}
}
return body;
@@ -275,30 +275,11 @@ export function openaiToOpenAIResponsesRequest(
// Convert assistant messages
if (role === "assistant") {
// Add reasoning content before assistant output
if (msg.reasoning_content) {
input.push({
type: "reasoning",
id: `reasoning_${input.length}`,
summary: [{ type: "summary_text", text: toString(msg.reasoning_content) }],
});
}
// Skip reasoning_content — OpenAI Responses API requires server-generated
// rs_* IDs for reasoning items. Synthesizing client-side IDs (e.g. reasoning_N)
// causes 400 errors from Responses-compatible upstreams. (#224)
// Handle thinking blocks in array content
if (Array.isArray(msg.content)) {
for (const blockValue of msg.content) {
const block = toRecord(blockValue);
if (block.type === "thinking" || block.type === "redacted_thinking") {
input.push({
type: "reasoning",
id: `reasoning_${input.length}`,
summary: [
{ type: "summary_text", text: toString(block.thinking || block.data, "...") },
],
});
}
}
}
// Skip thinking blocks in array content — same rs_* ID constraint applies
// Build assistant output content
const outputContent: unknown[] = [];
@@ -175,8 +175,13 @@ export function openaiToClaudeRequest(model, body, stream) {
};
});
if (result.tools.length > 0) {
result.tools[result.tools.length - 1].cache_control = { type: "ephemeral", ttl: "1h" };
// 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--) {
if (!result.tools[i].defer_loading) {
result.tools[i].cache_control = { type: "ephemeral", ttl: "1h" };
break;
}
}
}
+6 -15
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "2.0.1",
"version": "2.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "2.0.1",
"version": "2.0.11",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -6596,12 +6596,12 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz",
"integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@@ -6613,15 +6613,6 @@
"express": ">= 4.11"
}
},
"node_modules/express-rate-limit/node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/fast-copy": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "2.0.2",
"version": "2.0.12",
"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": {
@@ -0,0 +1,371 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, Button, Input } from "@/shared/components";
import { useTranslations } from "next-intl";
import { AI_PROVIDERS } from "@/shared/constants/config";
interface AgentInfo {
id: string;
name: string;
binary: string;
version: string | null;
installed: boolean;
protocol: string;
isCustom?: boolean;
}
interface AgentSummary {
total: number;
installed: number;
notFound: number;
builtIn: number;
custom: number;
}
export default function AgentsPage() {
const [agents, setAgents] = useState<AgentInfo[]>([]);
const [summary, setSummary] = useState<AgentSummary | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const [addLoading, setAddLoading] = useState(false);
const [settings, setSettings] = useState<Record<string, any>>({});
const [newAgent, setNewAgent] = useState({
name: "",
binary: "",
versionCommand: "",
spawnArgs: "",
});
const t = useTranslations("agents");
const ts = useTranslations("settings");
const fetchAgents = useCallback(async () => {
try {
const res = await fetch("/api/acp/agents");
const data = await res.json();
setAgents(data.agents || []);
setSummary(data.summary || null);
} catch (err) {
console.error("Failed to fetch agents:", err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAgents();
// Also fetch settings for CLI fingerprint
fetch("/api/settings")
.then((r) => r.json())
.then((d) => setSettings(d))
.catch(() => {});
}, [fetchAgents]);
const updateSetting = async (key: string, value: any) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ [key]: value }),
});
if (res.ok) setSettings((prev) => ({ ...prev, [key]: value }));
} catch (err) {
console.error("Failed to update setting:", err);
}
};
const handleRefresh = async () => {
setRefreshing(true);
try {
const res = await fetch("/api/acp/agents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "refresh" }),
});
const data = await res.json();
setAgents(data.agents || []);
await fetchAgents();
} catch (err) {
console.error("Failed to refresh:", err);
} finally {
setRefreshing(false);
}
};
const handleAddAgent = async (e: React.FormEvent) => {
e.preventDefault();
setAddLoading(true);
try {
const id = newAgent.name.toLowerCase().replace(/[^a-z0-9]/g, "-");
const res = await fetch("/api/acp/agents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id,
name: newAgent.name,
binary: newAgent.binary,
versionCommand: newAgent.versionCommand || `${newAgent.binary} --version`,
spawnArgs: newAgent.spawnArgs ? newAgent.spawnArgs.split(",").map((s) => s.trim()) : [],
protocol: "stdio",
}),
});
if (res.ok) {
setNewAgent({ name: "", binary: "", versionCommand: "", spawnArgs: "" });
setShowAddForm(false);
await fetchAgents();
}
} catch (err) {
console.error("Failed to add agent:", err);
} finally {
setAddLoading(false);
}
};
const handleRemoveAgent = async (agentId: string) => {
try {
const res = await fetch(`/api/acp/agents?id=${agentId}`, { method: "DELETE" });
if (res.ok) {
await fetchAgents();
}
} catch (err) {
console.error("Failed to remove agent:", err);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent" />
</div>
);
}
return (
<div className="flex flex-col gap-6 p-6 max-w-5xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{t("title")}</h1>
<p className="text-text-muted mt-1">{t("description")}</p>
</div>
<Button variant="secondary" onClick={handleRefresh} loading={refreshing}>
<span className="material-symbols-outlined text-[16px] mr-1">refresh</span>
{t("refresh")}
</Button>
</div>
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="rounded-xl border border-border/50 bg-card p-4 text-center">
<div className="text-2xl font-bold text-primary">{summary.installed}</div>
<div className="text-xs text-text-muted mt-1">{t("installed")}</div>
</div>
<div className="rounded-xl border border-border/50 bg-card p-4 text-center">
<div className="text-2xl font-bold text-text-muted">{summary.notFound}</div>
<div className="text-xs text-text-muted mt-1">{t("notFound")}</div>
</div>
<div className="rounded-xl border border-border/50 bg-card p-4 text-center">
<div className="text-2xl font-bold">{summary.builtIn}</div>
<div className="text-xs text-text-muted mt-1">{t("builtIn")}</div>
</div>
<div className="rounded-xl border border-border/50 bg-card p-4 text-center">
<div className="text-2xl font-bold text-amber-500">{summary.custom}</div>
<div className="text-xs text-text-muted mt-1">{t("custom")}</div>
</div>
</div>
)}
{/* CLI Fingerprint Matching */}
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
fingerprint
</span>
</div>
<h3 className="text-lg font-semibold">{ts("cliFingerprint")}</h3>
</div>
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">{ts("cliFingerprintDesc")}</p>
<div className="flex flex-wrap gap-2">
{(["codex", "claude", "github", "antigravity"] as const).map((providerId) => {
const providerMeta = Object.values(AI_PROVIDERS).find(
(p: any) => p.id === providerId
) as any;
const isEnabled = (settings.cliCompatProviders || []).includes(providerId);
const displayName = providerMeta?.name || providerId;
const icon = providerMeta?.icon || "terminal";
const color = providerMeta?.color || "#888";
return (
<button
key={providerId}
onClick={() => {
const current: string[] = settings.cliCompatProviders || [];
const updated = current.includes(providerId)
? current.filter((p) => p !== providerId)
: [...current, providerId];
updateSetting("cliCompatProviders", updated);
}}
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-all border ${
isEnabled
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400"
: "bg-black/[0.02] dark:bg-white/[0.02] border-transparent text-text-muted hover:bg-black/[0.05] dark:hover:bg-white/[0.05]"
}`}
>
<span
className="material-symbols-outlined text-[14px]"
style={{ color: isEnabled ? undefined : color }}
>
{isEnabled ? "fingerprint" : icon}
</span>
{displayName}
{isEnabled && (
<span className="material-symbols-outlined text-[12px] text-emerald-500">
check
</span>
)}
</button>
);
})}
</div>
{(settings.cliCompatProviders || []).length > 0 && (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">verified</span>
{ts("cliFingerprintEnabled", {
count: (settings.cliCompatProviders || []).length,
})}
</p>
)}
</div>
</Card>
{/* Agent Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{agents.map((agent) => (
<Card key={agent.id}>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div
className={`p-2 rounded-lg ${
agent.installed
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
: "bg-zinc-500/10 text-zinc-400"
}`}
>
<span className="material-symbols-outlined text-[20px]">
{agent.installed ? "smart_toy" : "block"}
</span>
</div>
<div>
<div className="font-semibold text-sm flex items-center gap-1.5">
{agent.name}
{agent.isCustom && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400 font-medium">
{t("custom")}
</span>
)}
</div>
<code className="text-xs text-text-muted">{agent.binary}</code>
</div>
</div>
<div className="flex items-center gap-1">
{agent.installed ? (
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-medium">
<span className="material-symbols-outlined text-[12px]">check_circle</span>
{agent.version || t("installed")}
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full bg-zinc-500/10 text-zinc-500 font-medium">
<span className="material-symbols-outlined text-[12px]">cancel</span>
{t("notFound")}
</span>
)}
</div>
</div>
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border/30">
<span className="inline-flex items-center gap-1 text-[10px] px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-500 font-mono">
{agent.protocol}
</span>
{agent.isCustom && (
<button
onClick={() => handleRemoveAgent(agent.id)}
className="text-xs text-red-500 hover:text-red-400 transition-colors flex items-center gap-0.5"
title={t("remove")}
>
<span className="material-symbols-outlined text-[14px]">delete</span>
{t("remove")}
</button>
)}
</div>
</Card>
))}
</div>
{/* Add Custom Agent */}
<Card>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400">
<span className="material-symbols-outlined text-[20px]">add_circle</span>
</div>
<div>
<h3 className="text-lg font-semibold">{t("addCustomAgent")}</h3>
<p className="text-sm text-text-muted">{t("addCustomAgentDesc")}</p>
</div>
</div>
<Button variant="secondary" onClick={() => setShowAddForm(!showAddForm)}>
<span className="material-symbols-outlined text-[16px]">
{showAddForm ? "expand_less" : "expand_more"}
</span>
</Button>
</div>
{showAddForm && (
<form
onSubmit={handleAddAgent}
className="flex flex-col gap-4 pt-4 border-t border-border/50"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t("agentName")}
placeholder="e.g. My Custom CLI"
value={newAgent.name}
onChange={(e) => setNewAgent({ ...newAgent, name: e.target.value })}
required
/>
<Input
label={t("binaryName")}
placeholder="e.g. mycli"
value={newAgent.binary}
onChange={(e) => setNewAgent({ ...newAgent, binary: e.target.value })}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t("versionCommand")}
placeholder="e.g. mycli --version"
value={newAgent.versionCommand}
onChange={(e) => setNewAgent({ ...newAgent, versionCommand: e.target.value })}
/>
<Input
label={t("spawnArgs")}
placeholder="e.g. --quiet, --json"
value={newAgent.spawnArgs}
onChange={(e) => setNewAgent({ ...newAgent, spawnArgs: e.target.value })}
/>
</div>
<div className="flex justify-end">
<Button type="submit" variant="primary" loading={addLoading}>
<span className="material-symbols-outlined text-[16px] mr-1">add</span>
{t("addAgent")}
</Button>
</div>
</form>
)}
</Card>
</div>
);
}
@@ -0,0 +1,400 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
interface ModelInfo {
id: string;
object: string;
owned_by: string;
}
interface ProviderOption {
value: string;
label: string;
}
const ENDPOINT_OPTIONS = [
{ value: "chat", label: "Chat Completions" },
{ value: "responses", label: "Responses" },
{ value: "images", label: "Image Generation" },
{ value: "embeddings", label: "Embeddings" },
];
const DEFAULT_BODIES: Record<string, object> = {
chat: {
model: "",
messages: [{ role: "user", content: "Hello! Say hi in one sentence." }],
max_tokens: 100,
stream: false,
},
responses: {
model: "",
input: "Hello! Say hi in one sentence.",
stream: false,
},
images: {
model: "",
prompt: "A beautiful sunset over mountains",
n: 1,
size: "1024x1024",
},
embeddings: {
model: "",
input: "Hello world",
encoding_format: "float",
},
};
const ENDPOINT_PATHS: Record<string, string> = {
chat: "/v1/chat/completions",
responses: "/v1/responses",
images: "/v1/images/generations",
embeddings: "/v1/embeddings",
};
export default function PlaygroundPage() {
const [models, setModels] = useState<ModelInfo[]>([]);
const [providers, setProviders] = useState<ProviderOption[]>([]);
const [selectedProvider, setSelectedProvider] = useState("");
const [selectedModel, setSelectedModel] = useState("");
const [selectedEndpoint, setSelectedEndpoint] = useState("chat");
const [requestBody, setRequestBody] = useState("");
const [responseBody, setResponseBody] = useState("");
const [loading, setLoading] = useState(false);
const [responseStatus, setResponseStatus] = useState<number | null>(null);
const [responseDuration, setResponseDuration] = useState<number | null>(null);
const abortRef = useRef<AbortController | null>(null);
// Fetch models
useEffect(() => {
fetch("/v1/models")
.then((res) => res.json())
.then((data) => {
const modelList = (data?.data || []) as ModelInfo[];
setModels(modelList);
// Extract unique providers from model ids (provider/model format)
const providerSet = new Set<string>();
modelList.forEach((m) => {
const parts = m.id.split("/");
if (parts.length >= 2) providerSet.add(parts[0]);
});
const providerOpts = Array.from(providerSet)
.sort()
.map((p) => ({ value: p, label: p }));
setProviders(providerOpts);
if (providerOpts.length > 0) setSelectedProvider(providerOpts[0].value);
})
.catch(() => {});
}, []);
// Filter models by selected provider
const filteredModels = models
.filter((m) => !selectedProvider || m.id.startsWith(selectedProvider + "/"))
.map((m) => ({ value: m.id, label: m.id }));
// Helper to generate default body for a given endpoint and model
const generateDefaultBody = (endpoint: string, model: string) => {
const template = { ...DEFAULT_BODIES[endpoint] };
if ("model" in template) {
(template as any).model = model;
}
return JSON.stringify(template, null, 2);
};
// When provider changes, auto-select first model and reset body
const handleProviderChange = (newProvider: string) => {
setSelectedProvider(newProvider);
const providerModels = models
.filter((m) => !newProvider || m.id.startsWith(newProvider + "/"))
.map((m) => m.id);
const firstModel = providerModels[0] || "";
setSelectedModel(firstModel);
setRequestBody(generateDefaultBody(selectedEndpoint, firstModel));
setResponseBody("");
setResponseStatus(null);
setResponseDuration(null);
};
// When model changes, update body
const handleModelChange = (newModel: string) => {
setSelectedModel(newModel);
setRequestBody(generateDefaultBody(selectedEndpoint, newModel));
setResponseBody("");
setResponseStatus(null);
setResponseDuration(null);
};
// When endpoint changes, update body
const handleEndpointChange = (newEndpoint: string) => {
setSelectedEndpoint(newEndpoint);
setRequestBody(generateDefaultBody(newEndpoint, selectedModel));
setResponseBody("");
setResponseStatus(null);
setResponseDuration(null);
};
const handleSend = useCallback(async () => {
if (!requestBody.trim()) return;
setLoading(true);
setResponseBody("");
setResponseStatus(null);
setResponseDuration(null);
const controller = new AbortController();
abortRef.current = controller;
const startTime = Date.now();
try {
const parsed = JSON.parse(requestBody);
const path = ENDPOINT_PATHS[selectedEndpoint];
const res = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(parsed),
signal: controller.signal,
});
setResponseStatus(res.status);
setResponseDuration(Date.now() - startTime);
const contentType = res.headers.get("content-type") || "";
if (contentType.includes("text/event-stream")) {
// Handle streaming
const reader = res.body?.getReader();
const decoder = new TextDecoder();
let accumulated = "";
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
accumulated += decoder.decode(value, { stream: true });
setResponseBody(accumulated);
}
}
} else {
const data = await res.json();
setResponseBody(JSON.stringify(data, null, 2));
}
} catch (err: any) {
if (err.name === "AbortError") {
setResponseBody(JSON.stringify({ cancelled: true }, null, 2));
} else {
setResponseBody(JSON.stringify({ error: err.message }, null, 2));
}
setResponseDuration(Date.now() - startTime);
}
setLoading(false);
}, [requestBody, selectedEndpoint]);
const handleCancel = () => {
if (abortRef.current) {
abortRef.current.abort();
}
};
const handleCopy = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
} catch {
/* silent */
}
};
return (
<div className="space-y-5">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
science
</span>
<div>
<p className="font-medium text-text-main mb-0.5">Model Playground</p>
<p>
Test any model directly from the dashboard. Pick a provider, model, and endpoint type,
then send a request to see the raw response.
</p>
</div>
</div>
{/* Controls */}
<Card>
<div className="p-4 flex flex-col sm:flex-row items-end gap-4">
{/* Provider */}
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
Provider
</label>
<Select
value={selectedProvider}
onChange={(e: any) => handleProviderChange(e.target.value)}
options={providers}
className="w-full"
/>
</div>
{/* Model */}
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
Model
</label>
<Select
value={selectedModel}
onChange={(e: any) => handleModelChange(e.target.value)}
options={filteredModels}
className="w-full"
/>
</div>
{/* Endpoint */}
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
Endpoint
</label>
<Select
value={selectedEndpoint}
onChange={(e: any) => handleEndpointChange(e.target.value)}
options={ENDPOINT_OPTIONS}
className="w-full"
/>
</div>
{/* Send Button */}
<div className="shrink-0">
{loading ? (
<Button icon="stop" variant="secondary" onClick={handleCancel}>
Cancel
</Button>
) : (
<Button
icon="send"
onClick={handleSend}
disabled={!requestBody.trim() || !selectedModel}
>
Send
</Button>
)}
</div>
</div>
</Card>
{/* Split Editor View */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Request Panel */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-text-muted">
upload
</span>
<h3 className="text-sm font-semibold text-text-main">Request</h3>
<Badge variant="info" size="sm">
POST {ENDPOINT_PATHS[selectedEndpoint]}
</Badge>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCopy(requestBody)}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title="Copy"
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
<button
onClick={() => {
const template = { ...DEFAULT_BODIES[selectedEndpoint] };
if ("model" in template) (template as any).model = selectedModel;
setRequestBody(JSON.stringify(template, null, 2));
}}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title="Reset to default"
>
<span className="material-symbols-outlined text-[16px]">restart_alt</span>
</button>
</div>
</div>
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="400px"
defaultLanguage="json"
value={requestBody}
onChange={(value: string | undefined) => setRequestBody(value || "")}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
formatOnPaste: true,
}}
/>
</div>
</div>
</Card>
{/* Response Panel */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-text-muted">
download
</span>
<h3 className="text-sm font-semibold text-text-main">Response</h3>
{responseStatus !== null && (
<Badge
variant={responseStatus >= 200 && responseStatus < 300 ? "success" : "error"}
size="sm"
>
{responseStatus}
</Badge>
)}
{responseDuration !== null && (
<span className="text-xs text-text-muted">{responseDuration}ms</span>
)}
{loading && (
<span className="material-symbols-outlined text-[14px] text-primary animate-spin">
progress_activity
</span>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCopy(responseBody)}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title="Copy"
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
</div>
</div>
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="400px"
defaultLanguage="json"
value={responseBody}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
readOnly: true,
}}
/>
</div>
</div>
</Card>
</div>
</div>
);
}
@@ -150,10 +150,9 @@ export default function ProviderLimitCard({
{!loading && !error && !message && quotas?.length > 0 && (
<div className="space-y-4">
{quotas.map((quota, index) => {
// For Antigravity, use remainingPercentage if available, otherwise calculate
const percentage =
quota.remainingPercentage !== undefined
? Math.round(((quota.total - quota.used) / quota.total) * 100)
? Math.round(quota.remainingPercentage)
: calculatePercentage(quota.used, quota.total);
const unlimited = quota.total === 0 || quota.total === null;
@@ -1,4 +1,5 @@
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
import { safePercentage } from "@/shared/utils/formatting";
/**
* Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit)
@@ -110,7 +111,7 @@ export function parseQuotaData(provider, data) {
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
remainingPercentage: safePercentage(quota.remainingPercentage),
});
});
}
@@ -159,7 +160,7 @@ export function parseQuotaData(provider, data) {
used: quota.used || 0,
total: quota.total || 0,
resetAt: quota.resetAt || null,
remainingPercentage: quota.remainingPercentage,
remainingPercentage: safePercentage(quota.remainingPercentage),
});
});
}
+121
View File
@@ -0,0 +1,121 @@
import { NextResponse } from "next/server";
import {
detectInstalledAgents,
refreshAgentCache,
setCustomAgents,
getCustomAgentDefs,
type CustomAgentDef,
} from "@/lib/acp/registry";
import { getSettings, updateSettings } from "@/lib/localDb";
export async function GET() {
try {
// Load custom agents from settings on each GET to stay in sync
const settings = await getSettings();
if (settings.customAgents) {
setCustomAgents(settings.customAgents as CustomAgentDef[]);
}
const agents = detectInstalledAgents();
const installed = agents.filter((a) => a.installed).length;
const total = agents.length;
return NextResponse.json({
agents,
summary: {
total,
installed,
notFound: total - installed,
builtIn: agents.filter((a) => !a.isCustom).length,
custom: agents.filter((a) => a.isCustom).length,
},
});
} catch (error) {
console.error("Error detecting agents:", error);
return NextResponse.json({ error: "Failed to detect agents" }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
if (body.action === "refresh") {
const agents = refreshAgentCache();
return NextResponse.json({ agents, refreshed: true });
}
// Add custom agent
const { id, name, binary, versionCommand, providerAlias, spawnArgs, protocol } = body;
if (!id || !name || !binary || !versionCommand) {
return NextResponse.json(
{ error: "Missing required fields: id, name, binary, versionCommand" },
{ status: 400 }
);
}
const newAgent: CustomAgentDef = {
id: id.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
name,
binary,
versionCommand,
providerAlias: providerAlias || id,
spawnArgs: spawnArgs || [],
protocol: protocol || "stdio",
};
// Load current, append, save
const settings = await getSettings();
const current: CustomAgentDef[] = (settings.customAgents as CustomAgentDef[]) || [];
// Avoid duplicates
if (current.some((a) => a.id === newAgent.id)) {
return NextResponse.json(
{ error: `Agent with id '${newAgent.id}' already exists` },
{ status: 409 }
);
}
const updated = [...current, newAgent];
await updateSettings({ customAgents: updated });
setCustomAgents(updated);
// Refresh cache to detect the new agent
const agents = refreshAgentCache();
return NextResponse.json({ agents, added: newAgent });
} catch (error) {
console.error("Error adding custom agent:", error);
return NextResponse.json({ error: "Failed to add agent" }, { status: 500 });
}
}
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const agentId = searchParams.get("id");
if (!agentId) {
return NextResponse.json({ error: "Missing agent id" }, { status: 400 });
}
const settings = await getSettings();
const current: CustomAgentDef[] = (settings.customAgents as CustomAgentDef[]) || [];
const updated = current.filter((a) => a.id !== agentId);
if (updated.length === current.length) {
return NextResponse.json(
{ error: `Agent '${agentId}' not found in custom agents` },
{ status: 404 }
);
}
await updateSettings({ customAgents: updated });
setCustomAgents(updated);
const agents = refreshAgentCache();
return NextResponse.json({ agents, removed: agentId });
} catch (error) {
console.error("Error removing custom agent:", error);
return NextResponse.json({ error: "Failed to remove agent" }, { status: 500 });
}
}
+27 -9
View File
@@ -221,9 +221,15 @@ export async function POST(
let connection: any;
if (tokenData.email) {
const existing = await getProviderConnections({ provider });
const match = existing.find(
(c: any) => c.email === tokenData.email && c.authType === "oauth"
);
const match = existing.find((c: any) => {
if (c.email !== tokenData.email || c.authType !== "oauth") return false;
// For Codex, also check workspaceId to avoid overwriting different workspace connections
if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) {
const existingWorkspace = c.providerSpecificData?.workspaceId;
return existingWorkspace === tokenData.providerSpecificData.workspaceId;
}
return true;
});
const matchId = typeof match?.id === "string" ? match.id : null;
if (matchId) {
connection = await updateProviderConnection(matchId, {
@@ -285,9 +291,15 @@ export async function POST(
let connection: any;
if (result.tokens.email) {
const existing = await getProviderConnections({ provider });
const match = existing.find(
(c: any) => c.email === result.tokens.email && c.authType === "oauth"
);
const match = existing.find((c: any) => {
if (c.email !== result.tokens.email || c.authType !== "oauth") return false;
// For Codex, also check workspaceId to avoid overwriting different workspace connections
if (provider === "codex" && result.tokens.providerSpecificData?.workspaceId) {
const existingWorkspace = c.providerSpecificData?.workspaceId;
return existingWorkspace === result.tokens.providerSpecificData.workspaceId;
}
return true;
});
const matchId = typeof match?.id === "string" ? match.id : null;
if (matchId) {
connection = await updateProviderConnection(matchId, {
@@ -399,9 +411,15 @@ export async function POST(
let connection: any;
if (tokenData.email) {
const existing = await getProviderConnections({ provider });
const match = existing.find(
(c: any) => c.email === tokenData.email && c.authType === "oauth"
);
const match = existing.find((c: any) => {
if (c.email !== tokenData.email || c.authType !== "oauth") return false;
// For Codex, also check workspaceId to avoid overwriting different workspace connections
if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) {
const existingWorkspace = c.providerSpecificData?.workspaceId;
return existingWorkspace === tokenData.providerSpecificData.workspaceId;
}
return true;
});
const matchId = typeof match?.id === "string" ? match.id : null;
if (matchId) {
connection = await updateProviderConnection(matchId, {
+11
View File
@@ -5,12 +5,18 @@ import bcrypt from "bcryptjs";
import { getRuntimePorts } from "@/lib/runtime/ports";
import { updateSettingsSchema } from "@/shared/validation/settingsSchemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { setCliCompatProviders } from "../../../../open-sse/config/cliFingerprints";
export async function GET() {
try {
const settings = await getSettings();
const { password, ...safeSettings } = settings;
// Sync CLI fingerprint providers to runtime cache on load
if (settings.cliCompatProviders) {
setCliCompatProviders(settings.cliCompatProviders as string[]);
}
const enableRequestLogs = process.env.ENABLE_REQUEST_LOGS === "true";
const runtimePorts = getRuntimePorts();
@@ -74,6 +80,11 @@ export async function PATCH(request) {
clearHealthCheckLogCache();
}
// Sync CLI fingerprint providers to runtime cache
if ("cliCompatProviders" in body) {
setCliCompatProviders(body.cliCompatProviders || []);
}
const { password, ...safeSettings } = settings;
return NextResponse.json(safeSettings);
} catch (error) {
+11
View File
@@ -4,6 +4,11 @@ import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
import { syncToCloud } from "@/lib/cloudSync";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { setQuotaCache } from "@/domain/quotaCache";
function isRecord(value: unknown): value is Record<string, any> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* Sync to cloud if enabled
@@ -147,6 +152,12 @@ export async function GET(request: Request, { params }: { params: Promise<{ conn
const usage = await runWithProxyContext(proxyInfo?.proxy || null, () =>
getUsageForProvider(connection)
);
// Populate quota cache for quota-aware account selection
if (isRecord(usage?.quotas)) {
setQuotaCache(connectionId, connection.provider, usage.quotas);
}
return Response.json(usage);
} catch (error) {
console.error("[Usage API] Error fetching usage:", error);
+39 -2
View File
@@ -107,7 +107,30 @@ export async function POST(request) {
if (policy.rejection) return policy.rejection;
// Parse model to get provider
const { provider } = parseImageModel(body.model);
let { provider } = parseImageModel(body.model);
let isCustomModel = false;
// If not in built-in registry, check custom models tagged for images
if (!provider) {
try {
const customModelsMap = (await getAllCustomModels()) as Record<string, any>;
for (const [providerId, models] of Object.entries(customModelsMap)) {
if (!Array.isArray(models)) continue;
for (const model of models) {
if (!model?.id || !Array.isArray(model.supportedEndpoints)) continue;
if (!model.supportedEndpoints.includes("images")) continue;
const fullId = `${providerId}/${model.id}`;
if (fullId === body.model) {
provider = providerId;
isCustomModel = true;
break;
}
}
if (provider) break;
}
} catch {}
}
if (!provider) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
@@ -128,9 +151,23 @@ export async function POST(request) {
`No credentials for image provider: ${provider}`
);
}
} else if (isCustomModel) {
// Custom models need credentials from the provider connection
credentials = await getProviderCredentials(provider);
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
`No credentials for custom image provider: ${provider}`
);
}
}
const result = await handleImageGeneration({ body, credentials, log });
const result = await handleImageGeneration({
body,
credentials,
log,
...(isCustomModel && { resolvedProvider: provider }),
});
if (result.success) {
return new Response(JSON.stringify((result as any).data), {
+3
View File
@@ -6,6 +6,9 @@
directives ensure all utility classes in route groups are included. */
@source "../app/(dashboard)";
@source "../../open-sse";
@source not "../../*.sqlite*";
@source not "../../.claude*";
@source not "../../.claude-memory";
@custom-variant dark (&:where(.dark, .dark *));
+264
View File
@@ -0,0 +1,264 @@
/**
* Quota Cache Domain Layer
*
* In-memory cache of provider quota data per connectionId.
* Populated by:
* - Dashboard usage endpoint (GET /api/usage/[connectionId])
* - 429 responses marking account as exhausted
*
* Background refresh runs every 1 minute:
* - Active accounts (quota > 0%): refetch every 5 minutes
* - Exhausted accounts: refetch every 5 minutes (or immediately after resetAt passes)
*
* @module domain/quotaCache
*/
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
import { getProviderConnectionById, resolveProxyForConnection } from "@/lib/localDb";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { safePercentage } from "@/shared/utils/formatting";
// ─── Types ──────────────────────────────────────────────────────────────────
interface QuotaInfo {
remainingPercentage: number;
resetAt: string | null;
}
interface QuotaCacheEntry {
connectionId: string;
provider: string;
quotas: Record<string, QuotaInfo>;
fetchedAt: number;
exhausted: boolean;
nextResetAt: string | null;
}
// ─── Constants ──────────────────────────────────────────────────────────────
const ACTIVE_TTL_MS = 5 * 60 * 1000; // 5 minutes for active accounts
const EXHAUSTED_TTL_MS = 5 * 60 * 1000; // 5 minutes for 429-sourced entries (no resetAt)
const EXHAUSTED_REFRESH_MS = 5 * 60 * 1000; // 5 minutes: recheck exhausted accounts (aligned with TTL)
const REFRESH_INTERVAL_MS = 60 * 1000; // Background tick every 1 minute
// ─── State ──────────────────────────────────────────────────────────────────
const cache = new Map<string, QuotaCacheEntry>();
const MAX_CONCURRENT_REFRESHES = 5;
let refreshTimer: ReturnType<typeof setInterval> | null = null;
let tickRunning = false;
// ─── Helpers ────────────────────────────────────────────────────────────────
function isExhausted(quotas: Record<string, QuotaInfo>): boolean {
const entries = Object.values(quotas);
if (entries.length === 0) return false;
return entries.every((q) => q.remainingPercentage <= 0);
}
function parseDate(value: string): number | null {
const ms = new Date(value).getTime();
return Number.isNaN(ms) ? null : ms;
}
function earliestResetAt(quotas: Record<string, QuotaInfo>): string | null {
let earliest: string | null = null;
let earliestMs = Infinity;
for (const q of Object.values(quotas)) {
if (!q.resetAt) continue;
const ms = parseDate(q.resetAt);
if (ms !== null && ms < earliestMs) {
earliestMs = ms;
earliest = q.resetAt;
}
}
return earliest;
}
function normalizeQuotas(rawQuotas: Record<string, any>): Record<string, QuotaInfo> {
const result: Record<string, QuotaInfo> = {};
for (const [key, q] of Object.entries(rawQuotas)) {
if (q && typeof q === "object") {
result[key] = {
remainingPercentage:
safePercentage(q.remainingPercentage) ??
(q.total > 0 ? Math.round(((q.total - (q.used || 0)) / q.total) * 100) : 0),
resetAt: q.resetAt || null,
};
}
}
return result;
}
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Store quota data for a connection (called by usage endpoint and background refresh).
*/
export function setQuotaCache(
connectionId: string,
provider: string,
rawQuotas: Record<string, any>
) {
const quotas = normalizeQuotas(rawQuotas);
const exhausted = isExhausted(quotas);
cache.set(connectionId, {
connectionId,
provider,
quotas,
fetchedAt: Date.now(),
exhausted,
nextResetAt: exhausted ? earliestResetAt(quotas) : null,
});
}
/**
* Get cached quota entry (returns null if not cached).
*/
export function getQuotaCache(connectionId: string): QuotaCacheEntry | null {
return cache.get(connectionId) || null;
}
/**
* Check if an account's quota is exhausted based on cached data.
* Returns false if no cache entry exists (unknown = assume available).
*/
export function isAccountQuotaExhausted(connectionId: string): boolean {
const entry = cache.get(connectionId);
if (!entry) return false;
if (!entry.exhausted) return false;
// If resetAt has passed, assume available until refresh confirms
if (entry.nextResetAt) {
const resetMs = parseDate(entry.nextResetAt);
if (resetMs !== null && resetMs <= Date.now()) return false;
}
// Exhausted entries without resetAt expire after fixed TTL
const age = Date.now() - entry.fetchedAt;
if (!entry.nextResetAt && age > EXHAUSTED_TTL_MS) return false;
return true;
}
/**
* Mark an account as quota-exhausted from a 429 response (no quota data available).
* Uses 5-minute fixed TTL since we don't know the actual resetAt.
*/
export function markAccountExhaustedFrom429(connectionId: string, provider: string) {
cache.set(connectionId, {
connectionId,
provider,
quotas: {},
fetchedAt: Date.now(),
exhausted: true,
nextResetAt: null,
});
}
// ─── Background Refresh ─────────────────────────────────────────────────────
const refreshingSet = new Set<string>();
async function refreshEntry(entry: QuotaCacheEntry) {
if (refreshingSet.has(entry.connectionId)) return;
refreshingSet.add(entry.connectionId);
try {
const connection = await getProviderConnectionById(entry.connectionId);
if (!connection || connection.authType !== "oauth" || !connection.isActive) {
cache.delete(entry.connectionId);
return;
}
const proxyInfo = await resolveProxyForConnection(entry.connectionId);
const usage = await runWithProxyContext(proxyInfo?.proxy || null, () =>
getUsageForProvider(connection)
);
if (usage?.quotas) {
setQuotaCache(entry.connectionId, entry.provider, usage.quotas);
}
} catch (err) {
console.warn(
`[QuotaCache] Refresh failed for ${entry.connectionId.slice(0, 8)}:`,
(err as any)?.message || err
);
} finally {
refreshingSet.delete(entry.connectionId);
}
}
function needsRefresh(entry: QuotaCacheEntry, now: number): boolean {
const age = now - entry.fetchedAt;
if (entry.exhausted) {
if (entry.nextResetAt) {
const resetMs = parseDate(entry.nextResetAt);
if (resetMs !== null && resetMs <= now) return true;
}
return age >= EXHAUSTED_REFRESH_MS;
}
return age >= ACTIVE_TTL_MS;
}
async function backgroundRefreshTick() {
if (tickRunning) return;
tickRunning = true;
try {
const now = Date.now();
const pending = [...cache.values()].filter((e) => needsRefresh(e, now));
// Refresh in batches to avoid thundering herd
for (let i = 0; i < pending.length; i += MAX_CONCURRENT_REFRESHES) {
const batch = pending.slice(i, i + MAX_CONCURRENT_REFRESHES);
await Promise.allSettled(batch.map(refreshEntry));
}
} finally {
tickRunning = false;
}
}
/**
* Start the background refresh timer.
*/
export function startBackgroundRefresh() {
if (refreshTimer) return;
refreshTimer = setInterval(backgroundRefreshTick, REFRESH_INTERVAL_MS);
refreshTimer?.unref?.();
}
/**
* Stop the background refresh timer.
*/
export function stopBackgroundRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
/**
* Get cache stats (for debugging/dashboard).
*/
export function getQuotaCacheStats() {
const entries: Array<{
connectionId: string;
provider: string;
exhausted: boolean;
nextResetAt: string | null;
ageMs: number;
}> = [];
for (const entry of cache.values()) {
entries.push({
connectionId: entry.connectionId.slice(0, 8) + "...",
provider: entry.provider,
exhausted: entry.exhausted,
nextResetAt: entry.nextResetAt,
ageMs: Date.now() - entry.fetchedAt,
});
}
return { total: cache.size, entries };
}
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "بنفسجي ساحر",
"themeOrange": "أورانج (Orange)",
"themeCyan": "كيان",
"endpoints": "نقاط النهاية"
"endpoints": "نقاط النهاية",
"playground": "ملعب النماذج",
"agents": "وكلاء",
"cliToolsShort": "أدوات"
},
"themesPage": {
"title": "المواضيع",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "الرموز المميزة المستخدمة لإنشاء إدخالات ذاكرة التخزين المؤقت (الرجوع إلى معدل الإدخال)",
"customPricingNote": "يمكنك تجاوز التسعير الافتراضي لنماذج محددة. تحظى التجاوزات المخصصة بالأولوية على الأسعار التي يتم اكتشافها تلقائيًا.",
"editPricing": "تحرير التسعير",
"viewFullDetails": "عرض التفاصيل الكاملة"
"viewFullDetails": "عرض التفاصيل الكاملة",
"themeCoral": "مرجاني"
},
"translator": {
"title": "مترجم",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Виолетово",
"themeOrange": "Оранжево",
"themeCyan": "Циан",
"endpoints": "Крайни точки"
"endpoints": "Крайни точки",
"playground": "Площадка",
"agents": "Агенти",
"cliToolsShort": "Инструменти"
},
"themesPage": {
"title": "Теми",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Токени, използвани за създаване на записи в кеша (резервен към скоростта на въвеждане)",
"customPricingNote": "Можете да замените цените по подразбиране за конкретни модели. Персонализираните замени имат приоритет пред автоматично разпознатото ценообразуване.",
"editPricing": "Редактиране на цените",
"viewFullDetails": "Вижте пълните подробности"
"viewFullDetails": "Вижте пълните подробности",
"themeCoral": "Корал"
},
"translator": {
"title": "Преводач",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Turkis",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Legeplads",
"agents": "Agenter",
"cliToolsShort": "Værktøjer"
},
"themesPage": {
"title": "Temaer",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokens, der bruges til at oprette cacheposter (tilbage til inputhastighed)",
"customPricingNote": "Du kan tilsidesætte standardpriser for specifikke modeller. Tilpassede tilsidesættelser har prioritet frem for automatisk registrerede priser.",
"editPricing": "Rediger prissætning",
"viewFullDetails": "Se alle detaljer"
"viewFullDetails": "Se alle detaljer",
"themeCoral": "Koral"
},
"translator": {
"title": "Oversætter",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violett",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpunkte"
"endpoints": "Endpunkte",
"playground": "Spielwiese",
"agents": "Agenten",
"cliToolsShort": "Werkzeuge"
},
"themesPage": {
"title": "Themen",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Token, die zum Erstellen von Cache-Einträgen verwendet werden (Fallback auf Eingaberate)",
"customPricingNote": "Sie können die Standardpreise für bestimmte Modelle überschreiben. Benutzerdefinierte Überschreibungen haben Vorrang vor automatisch erkannten Preisen.",
"editPricing": "Preise bearbeiten",
"viewFullDetails": "Vollständige Details anzeigen"
"viewFullDetails": "Vollständige Details anzeigen",
"themeCoral": "Koralle"
},
"translator": {
"title": "Übersetzer",
+28 -2
View File
@@ -72,6 +72,8 @@
"media": "Media",
"settings": "Settings",
"translator": "Translator",
"playground": "Playground",
"agents": "Agents",
"docs": "Docs",
"issues": "Issues",
"endpoints": "Endpoints",
@@ -100,7 +102,8 @@
"themeGreen": "Green",
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan"
"themeCyan": "Cyan",
"cliToolsShort": "Tools"
},
"themesPage": {
"title": "Themes",
@@ -1492,6 +1495,11 @@
"providersBlocked": "{count} provider(s) blocked from /models",
"blockProviderTitle": "Block {provider}",
"unblockProviderTitle": "Unblock {provider}",
"cliFingerprint": "CLI Fingerprint Matching",
"cliFingerprintDesc": "Match native CLI binary signatures when proxying requests. Reorders headers and body fields to look identical to the official CLI tools. Your proxy IP is preserved.",
"cliFingerprintEnabled": "{count} provider(s) with CLI fingerprint active",
"enableFingerprintTitle": "Enable fingerprint for {provider}",
"disableFingerprintTitle": "Disable fingerprint for {provider}",
"routingStrategy": "Routing Strategy",
"fillFirst": "Fill First",
"fillFirstDesc": "Use accounts in priority order",
@@ -1742,7 +1750,8 @@
"cacheCreationTokenDesc": "Tokens used to create cache entries (fallback to input rate)",
"customPricingNote": "You can override default pricing for specific models. Custom overrides take priority over auto-detected pricing.",
"editPricing": "Edit Pricing",
"viewFullDetails": "View Full Details"
"viewFullDetails": "View Full Details",
"themeCoral": "Coral"
},
"translator": {
"title": "Translator",
@@ -2415,5 +2424,22 @@
"termsSection5Text": "OmniRoute is provided \"as is\" without warranty of any kind. We are not responsible for any costs incurred through API usage, service disruptions, or data loss. Always maintain backups of your configuration.",
"termsSection6Title": "6. Open Source",
"termsSection6Text": "OmniRoute is open-source software. You are free to inspect, modify, and distribute it under the terms of its license."
},
"agents": {
"title": "CLI Agents",
"description": "Discover installed CLI agents on your system. Add custom agents for auto-detection.",
"refresh": "Refresh",
"installed": "Installed",
"notFound": "Not Found",
"builtIn": "Built-in",
"custom": "Custom",
"remove": "Remove",
"addCustomAgent": "Add Custom Agent",
"addCustomAgentDesc": "Register any CLI tool for detection. It will be scanned automatically on refresh.",
"agentName": "Agent Name",
"binaryName": "Binary Name",
"versionCommand": "Version Command",
"spawnArgs": "Spawn Args",
"addAgent": "Add Agent"
}
}
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violeta",
"themeOrange": "Naranja",
"themeCyan": "cian",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Playground",
"agents": "Agentes",
"cliToolsShort": "Herramientas"
},
"themesPage": {
"title": "Temas",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokens utilizados para crear entradas de caché (retroceso a la tasa de entrada)",
"customPricingNote": "Puede anular los precios predeterminados para modelos específicos. Las anulaciones personalizadas tienen prioridad sobre los precios detectados automáticamente.",
"editPricing": "Editar precios",
"viewFullDetails": "Ver todos los detalles"
"viewFullDetails": "Ver todos los detalles",
"themeCoral": "Coral"
},
"translator": {
"title": "Traductor",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violetti",
"themeOrange": "Oranssi",
"themeCyan": "Syaani",
"endpoints": "Päätepisteet"
"endpoints": "Päätepisteet",
"playground": "Leikkipaikka",
"agents": "Agentit",
"cliToolsShort": "Työkalut"
},
"themesPage": {
"title": "Teemat",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Välimuistimerkintöjen luomiseen käytetyt tunnukset (varausarvo syöttönopeuteen)",
"customPricingNote": "Voit ohittaa tiettyjen mallien oletushinnoittelun. Mukautetut ohitukset ovat etusijalla automaattisesti tunnistettuihin hinnoitteluun nähden.",
"editPricing": "Muokkaa hinnoittelua",
"viewFullDetails": "Näytä täydelliset tiedot"
"viewFullDetails": "Näytä täydelliset tiedot",
"themeCoral": "Koralli"
},
"translator": {
"title": "Kääntäjä",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violette",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Points d'accès"
"endpoints": "Points d'accès",
"playground": "Playground",
"agents": "Agents",
"cliToolsShort": "Outils"
},
"themesPage": {
"title": "Thèmes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Jetons utilisés pour créer des entrées de cache (repli sur le débit d'entrée)",
"customPricingNote": "Vous pouvez remplacer le prix par défaut pour des modèles spécifiques. Les remplacements personnalisés ont la priorité sur les prix détectés automatiquement.",
"editPricing": "Modifier le prix",
"viewFullDetails": "Afficher tous les détails"
"viewFullDetails": "Afficher tous les détails",
"themeCoral": "Corail"
},
"translator": {
"title": "Traducteur",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "נקודות קצה"
"endpoints": "נקודות קצה",
"playground": "Playground",
"agents": "סוכנים",
"cliToolsShort": "כלים"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "אסימונים המשמשים ליצירת ערכי מטמון (חזרה לקצב קלט)",
"customPricingNote": "אתה יכול לעקוף את תמחור ברירת המחדל עבור דגמים ספציפיים. עקיפות מותאמות אישית מקבלות עדיפות על פני תמחור שזוהה אוטומטית.",
"editPricing": "ערוך תמחור",
"viewFullDetails": "צפה בפרטים המלאים"
"viewFullDetails": "צפה בפרטים המלאים",
"themeCoral": "אלמוג"
},
"translator": {
"title": "מתרגם",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "narancssárga",
"themeCyan": "Cián",
"endpoints": "Végpontok"
"endpoints": "Végpontok",
"playground": "Játszótér",
"agents": "Ügynökök",
"cliToolsShort": "Eszközök"
},
"themesPage": {
"title": "Témák",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "A gyorsítótár bejegyzéseinek létrehozására használt tokenek (vissza a beviteli sebességre)",
"customPricingNote": "Egyes modelleknél felülbírálhatja az alapértelmezett árazást. Az egyéni felülbírálások elsőbbséget élveznek az automatikusan észlelt árképzéssel szemben.",
"editPricing": "Árak szerkesztése",
"viewFullDetails": "Teljes részletek megtekintése"
"viewFullDetails": "Teljes részletek megtekintése",
"themeCoral": "Korall"
},
"translator": {
"title": "Fordító",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground",
"agents": "Agen",
"cliToolsShort": "Alat"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Token yang digunakan untuk membuat entri cache (pengembalian ke tingkat input)",
"customPricingNote": "Anda dapat mengganti harga default untuk model tertentu. Penggantian khusus lebih diprioritaskan dibandingkan harga yang terdeteksi otomatis.",
"editPricing": "Sunting Harga",
"viewFullDetails": "Lihat Detail Lengkap"
"viewFullDetails": "Lihat Detail Lengkap",
"themeCoral": "Koral"
},
"translator": {
"title": "Penerjemah",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground",
"agents": "एजेंट",
"cliToolsShort": "उपकरण"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "कैश प्रविष्टियाँ बनाने के लिए उपयोग किए जाने वाले टोकन (इनपुट दर पर फ़ॉलबैक)",
"customPricingNote": "आप विशिष्ट मॉडलों के लिए डिफ़ॉल्ट मूल्य निर्धारण को ओवरराइड कर सकते हैं। कस्टम ओवरराइड्स को स्वतः-पता लगाए गए मूल्य-निर्धारण पर प्राथमिकता दी जाती है।",
"editPricing": "मूल्य निर्धारण संपादित करें",
"viewFullDetails": "पूर्ण विवरण देखें"
"viewFullDetails": "पूर्ण विवरण देखें",
"themeCoral": "कोरल"
},
"translator": {
"title": "अनुवादक",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoint"
"endpoints": "Endpoint",
"playground": "Playground",
"agents": "Agenti",
"cliToolsShort": "Strumenti"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Token utilizzati per creare voci nella cache (fallback alla velocità di input)",
"customPricingNote": "Puoi sostituire i prezzi predefiniti per modelli specifici. Le sostituzioni personalizzate hanno la priorità sui prezzi rilevati automaticamente.",
"editPricing": "Modifica prezzi",
"viewFullDetails": "Visualizza i dettagli completi"
"viewFullDetails": "Visualizza i dettagli completi",
"themeCoral": "Corallo"
},
"translator": {
"title": "Traduttore",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "エンドポイント"
"endpoints": "エンドポイント",
"playground": "プレイグラウンド",
"agents": "エージェント",
"cliToolsShort": "ツール"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "キャッシュ エントリの作成に使用されるトークン (入力レートへのフォールバック)",
"customPricingNote": "特定のモデルのデフォルトの価格をオーバーライドできます。カスタム オーバーライドは、自動検出された価格設定よりも優先されます。",
"editPricing": "価格の編集",
"viewFullDetails": "詳細を表示"
"viewFullDetails": "詳細を表示",
"themeCoral": "コーラル"
},
"translator": {
"title": "翻訳者",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "엔드포인트"
"endpoints": "엔드포인트",
"playground": "플레이그라운드",
"agents": "에이전트",
"cliToolsShort": "도구"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "캐시 항목을 생성하는 데 사용되는 토큰(입력 속도로 대체)",
"customPricingNote": "특정 모델의 기본 가격을 재정의할 수 있습니다. 맞춤 재정의는 자동 감지된 가격보다 우선 적용됩니다.",
"editPricing": "가격 편집",
"viewFullDetails": "전체 세부정보 보기"
"viewFullDetails": "전체 세부정보 보기",
"themeCoral": "코랄"
},
"translator": {
"title": "번역기",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Titik Akhir"
"endpoints": "Titik Akhir",
"playground": "Playground",
"agents": "Ejen",
"cliToolsShort": "Alat"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Token yang digunakan untuk mencipta entri cache (sandar kepada kadar input)",
"customPricingNote": "Anda boleh mengatasi harga lalai untuk model tertentu. Penggantian tersuai diutamakan berbanding harga yang dikesan secara automatik.",
"editPricing": "Edit Harga",
"viewFullDetails": "Lihat Butiran Penuh"
"viewFullDetails": "Lihat Butiran Penuh",
"themeCoral": "Koral"
},
"translator": {
"title": "Penterjemah",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Eindpunten"
"endpoints": "Eindpunten",
"playground": "Speeltuin",
"agents": "Agenten",
"cliToolsShort": "Gereedschap"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokens die worden gebruikt om cache-items te maken (terugval op invoersnelheid)",
"customPricingNote": "U kunt de standaardprijzen voor specifieke modellen overschrijven. Aangepaste overschrijvingen hebben voorrang op automatisch gedetecteerde prijzen.",
"editPricing": "Prijzen bewerken",
"viewFullDetails": "Bekijk volledige details"
"viewFullDetails": "Bekijk volledige details",
"themeCoral": "Koraal"
},
"translator": {
"title": "Vertaler",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endepunkter"
"endpoints": "Endepunkter",
"playground": "Lekeplass",
"agents": "Agenter",
"cliToolsShort": "Verktøy"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokens som brukes til å lage cache-oppføringer (tilbake til inngangshastighet)",
"customPricingNote": "Du kan overstyre standardpriser for spesifikke modeller. Egendefinerte overstyringer prioriteres fremfor automatisk oppdagede priser.",
"editPricing": "Rediger priser",
"viewFullDetails": "Se alle detaljer"
"viewFullDetails": "Se alle detaljer",
"themeCoral": "Korall"
},
"translator": {
"title": "Oversetter",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Mga Endpoint"
"endpoints": "Mga Endpoint",
"playground": "Playground",
"agents": "Mga Agent",
"cliToolsShort": "Mga Tool"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Mga token na ginamit upang lumikha ng mga entry sa cache (fallback sa rate ng pag-input)",
"customPricingNote": "Maaari mong i-override ang default na pagpepresyo para sa mga partikular na modelo. Mas inuuna ang mga custom na override kaysa sa awtomatikong natukoy na pagpepresyo.",
"editPricing": "I-edit ang Pagpepresyo",
"viewFullDetails": "Tingnan ang Buong Detalye"
"viewFullDetails": "Tingnan ang Buong Detalye",
"themeCoral": "Coral"
},
"translator": {
"title": "Tagasalin",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Punkty końcowe"
"endpoints": "Punkty końcowe",
"playground": "Plac zabaw",
"agents": "Agenci",
"cliToolsShort": "Narzędzia"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokeny używane do tworzenia wpisów w pamięci podręcznej (powrót do szybkości wprowadzania)",
"customPricingNote": "Możesz zastąpić domyślne ceny dla określonych modeli. Zastąpienia niestandardowe mają pierwszeństwo przed automatycznie wykrytymi cenami.",
"editPricing": "Edytuj ceny",
"viewFullDetails": "Zobacz pełne szczegóły"
"viewFullDetails": "Zobacz pełne szczegóły",
"themeCoral": "Koral"
},
"translator": {
"title": "Tłumacz",
+28 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Endpoints"
"endpoints": "Endpoints",
"playground": "Playground",
"agents": "Agentes",
"cliToolsShort": "Ferramentas"
},
"themesPage": {
"title": "Themes",
@@ -1480,6 +1483,11 @@
"providersBlocked": "{count} provedor(es) bloqueado(s) do /models",
"blockProviderTitle": "Bloquear {provider}",
"unblockProviderTitle": "Desbloquear {provider}",
"cliFingerprint": "Assinatura Digital CLI",
"cliFingerprintDesc": "Reproduz a assinatura digital dos CLIs nativos ao fazer proxy. Reorganiza headers e campos do body para parecer idêntico às ferramentas CLI oficiais. O IP do proxy é preservado.",
"cliFingerprintEnabled": "{count} provider(s) com fingerprint CLI ativo",
"enableFingerprintTitle": "Ativar fingerprint para {provider}",
"disableFingerprintTitle": "Desativar fingerprint para {provider}",
"routingStrategy": "Estratégia de Roteamento",
"fillFirst": "Preencher Primeiro",
"fillFirstDesc": "Usar contas em ordem de prioridade",
@@ -1730,7 +1738,8 @@
"cacheCreationTokenDesc": "Tokens usados para criar entradas de cache (fallback para taxa de input)",
"customPricingNote": "Você pode sobrescrever preços padrão para modelos específicos. Sobrescritas personalizadas têm prioridade sobre preços detectados automaticamente.",
"editPricing": "Editar Preços",
"viewFullDetails": "Ver Detalhes Completos"
"viewFullDetails": "Ver Detalhes Completos",
"themeCoral": "Coral"
},
"translator": {
"title": "Tradutor",
@@ -2415,5 +2424,22 @@
"featureWebhooks": "Configuração de webhooks e assinaturas de eventos",
"featureSwagger": "Geração automática de specs OpenAPI / Swagger",
"featureAuth": "Gestão de chaves API e escopos OAuth por endpoint"
},
"agents": {
"title": "Agentes CLI",
"description": "Descubra agentes CLI instalados no seu sistema. Adicione agentes customizados para auto-detecção.",
"refresh": "Atualizar",
"installed": "Instalado",
"notFound": "Não encontrado",
"builtIn": "Nativo",
"custom": "Customizado",
"remove": "Remover",
"addCustomAgent": "Adicionar Agente Customizado",
"addCustomAgentDesc": "Registre qualquer ferramenta CLI para detecção. Ela será verificada automaticamente ao atualizar.",
"agentName": "Nome do Agente",
"binaryName": "Nome do Binário",
"versionCommand": "Comando de Versão",
"spawnArgs": "Argumentos",
"addAgent": "Adicionar Agente"
}
}
+6 -2
View File
@@ -100,7 +100,10 @@
"themeGreen": "Verde",
"themeViolet": "Violeta",
"themeOrange": "Laranja",
"themeCyan": "Ciano"
"themeCyan": "Ciano",
"playground": "Playground",
"agents": "Agentes",
"cliToolsShort": "Ferramentas"
},
"themesPage": {
"title": "Themes",
@@ -1742,7 +1745,8 @@
"cacheCreationTokenDesc": "Tokens usados para criar entradas de cache (fallback para taxa de entrada)",
"customPricingNote": "Você pode substituir o preço padrão de modelos específicos. As substituições personalizadas têm prioridade sobre os preços detectados automaticamente.",
"editPricing": "Editar preços",
"viewFullDetails": "Ver detalhes completos"
"viewFullDetails": "Ver detalhes completos",
"themeCoral": "Coral"
},
"translator": {
"title": "Tradutor",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Puncte finale"
"endpoints": "Puncte finale",
"playground": "Playground",
"agents": "Agenți",
"cliToolsShort": "Instrumente"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Jetoane utilizate pentru a crea intrări în cache (retur la rata de intrare)",
"customPricingNote": "Puteți suprascrie prețurile implicite pentru anumite modele. Anulările personalizate au prioritate față de prețurile detectate automat.",
"editPricing": "Editați prețul",
"viewFullDetails": "Vezi detalii complete"
"viewFullDetails": "Vezi detalii complete",
"themeCoral": "Coral"
},
"translator": {
"title": "Traducător",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Фиолетовый",
"themeOrange": "Оранжевый",
"themeCyan": "Голубой",
"endpoints": "Конечные точки"
"endpoints": "Конечные точки",
"playground": "Площадка",
"agents": "Агенты",
"cliToolsShort": "Инструменты"
},
"themesPage": {
"title": "Темы",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Токены, используемые для создания записей в кэше (возврат к скорости ввода)",
"customPricingNote": "Вы можете переопределить цены по умолчанию для определенных моделей. Пользовательские переопределения имеют приоритет над ценами, определяемыми автоматически.",
"editPricing": "Изменить цену",
"viewFullDetails": "Посмотреть полную информацию"
"viewFullDetails": "Посмотреть полную информацию",
"themeCoral": "Коралл"
},
"translator": {
"title": "Переводчик",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Koncové body"
"endpoints": "Koncové body",
"playground": "Ihrisko",
"agents": "Agenti",
"cliToolsShort": "Nástroje"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokeny používané na vytváranie záznamov vo vyrovnávacej pamäti (záložná rýchlosť vstupu)",
"customPricingNote": "Predvolené ceny pre konkrétne modely môžete prepísať. Vlastné prepísania majú prednosť pred automaticky zistenými cenami.",
"editPricing": "Upraviť ceny",
"viewFullDetails": "Zobraziť úplné podrobnosti"
"viewFullDetails": "Zobraziť úplné podrobnosti",
"themeCoral": "Korál"
},
"translator": {
"title": "Prekladateľ",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Ändpunkter"
"endpoints": "Ändpunkter",
"playground": "Lekplats",
"agents": "Agenter",
"cliToolsShort": "Verktyg"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Tokens som används för att skapa cacheposter (återgång till inmatningshastighet)",
"customPricingNote": "Du kan åsidosätta standardpriser för specifika modeller. Anpassade åsidosättningar har prioritet framför automatiskt identifierade priser.",
"editPricing": "Redigera prissättning",
"viewFullDetails": "Visa fullständiga detaljer"
"viewFullDetails": "Visa fullständiga detaljer",
"themeCoral": "Korall"
},
"translator": {
"title": "Översättare",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "จุดปลายทาง"
"endpoints": "จุดปลายทาง",
"playground": "Playground",
"agents": "เอเจนต์",
"cliToolsShort": "เครื่องมือ"
},
"themesPage": {
"title": "ธีมส์",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "โทเค็นที่ใช้ในการสร้างรายการแคช (สำรองไปยังอัตราการป้อนข้อมูล)",
"customPricingNote": "คุณสามารถแทนที่ราคาเริ่มต้นสำหรับรุ่นเฉพาะได้ การแทนที่แบบกำหนดเองจะมีลำดับความสำคัญมากกว่าการกำหนดราคาที่ตรวจพบอัตโนมัติ",
"editPricing": "แก้ไขราคา",
"viewFullDetails": "ดูรายละเอียดทั้งหมด"
"viewFullDetails": "ดูรายละเอียดทั้งหมด",
"themeCoral": "คอรัล"
},
"translator": {
"title": "นักแปล",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Кінцеві точки"
"endpoints": "Кінцеві точки",
"playground": "Playground",
"agents": "Агенти",
"cliToolsShort": "Інструменти"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Токени, що використовуються для створення записів кешу (резервний вихід до швидкості введення)",
"customPricingNote": "Ви можете змінити ціни за умовчанням для певних моделей. Спеціальні зміни мають пріоритет над автоматично визначеними цінами.",
"editPricing": "Редагувати ціни",
"viewFullDetails": "Переглянути повну інформацію"
"viewFullDetails": "Переглянути повну інформацію",
"themeCoral": "Корал"
},
"translator": {
"title": "Перекладач",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "Điểm cuối"
"endpoints": "Điểm cuối",
"playground": "Playground",
"agents": "Tác nhân",
"cliToolsShort": "Công cụ"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "Mã thông báo được sử dụng để tạo mục nhập bộ đệm (dự phòng tốc độ đầu vào)",
"customPricingNote": "Bạn có thể ghi đè giá mặc định cho các kiểu máy cụ thể. Ghi đè tùy chỉnh được ưu tiên hơn giá được tự động phát hiện.",
"editPricing": "Chỉnh sửa giá",
"viewFullDetails": "Xem chi tiết đầy đủ"
"viewFullDetails": "Xem chi tiết đầy đủ",
"themeCoral": "San hô"
},
"translator": {
"title": "Người phiên dịch",
+6 -2
View File
@@ -100,7 +100,10 @@
"themeViolet": "Violet",
"themeOrange": "Orange",
"themeCyan": "Cyan",
"endpoints": "端点"
"endpoints": "端点",
"playground": "模型试验场",
"agents": "代理",
"cliToolsShort": "工具"
},
"themesPage": {
"title": "Themes",
@@ -1730,7 +1733,8 @@
"cacheCreationTokenDesc": "用于创建缓存条目的令牌(回退到输入速率)",
"customPricingNote": "您可以覆盖特定型号的默认定价。自定义覆盖优先于自动检测的定价。",
"editPricing": "编辑定价",
"viewFullDetails": "查看完整详情"
"viewFullDetails": "查看完整详情",
"themeCoral": "珊瑚色"
},
"translator": {
"title": "翻译者",
+7
View File
@@ -39,6 +39,13 @@ export async function register() {
const { initApiBridgeServer } = await import("@/lib/apiBridgeServer");
initApiBridgeServer();
// Quota cache: start background refresh for quota-aware account selection
// Dynamic import required — quotaCache depends on better-sqlite3 (Node-only),
// and instrumentation.ts is bundled for all runtimes including Edge.
const { startBackgroundRefresh } = await import("@/domain/quotaCache");
startBackgroundRefresh();
console.log("[STARTUP] Quota cache background refresh started");
// Compliance: Initialize audit_log table + cleanup expired logs
try {
const { initAuditLog, cleanupExpiredLogs } = await import("@/lib/compliance/index");
+11
View File
@@ -0,0 +1,11 @@
/**
* ACP Module Public API
*
* Re-exports the registry and manager for convenient imports.
*/
export { detectInstalledAgents, getAgentById, getAvailableAgents } from "./registry";
export type { CliAgentInfo } from "./registry";
export { AcpManager, acpManager } from "./manager";
export type { AcpSession } from "./manager";
+197
View File
@@ -0,0 +1,197 @@
/**
* ACP (Agent Client Protocol) Process Spawner & Manager
*
* Spawns CLI agents as child processes and manages their lifecycle.
* Communication happens via stdin/stdout (JSON-RPC style) or piped HTTP.
*
* This module provides a "CLI-as-backend" transport: instead of intercepting
* HTTP API calls, OmniRoute spawns the CLI directly and feeds prompts through
* its native interface.
*/
import { spawn, ChildProcess } from "child_process";
import { EventEmitter } from "events";
export interface AcpSession {
/** Unique session ID */
id: string;
/** Agent ID (e.g., "codex", "claude") */
agentId: string;
/** Child process handle */
process: ChildProcess;
/** Whether the process is alive */
alive: boolean;
/** Accumulated stdout buffer */
stdoutBuffer: string;
/** Accumulated stderr buffer */
stderrBuffer: string;
/** Created timestamp */
createdAt: Date;
}
/**
* ACP Session Manager
*
* Manages the lifecycle of CLI agent processes.
* Each session represents one running CLI agent instance.
*/
export class AcpManager extends EventEmitter {
private sessions: Map<string, AcpSession> = new Map();
/**
* Spawn a new CLI agent process.
*/
spawn(
agentId: string,
binary: string,
args: string[] = [],
env: Record<string, string> = {}
): AcpSession {
const sessionId = `acp-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const child = spawn(binary, args, {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...env },
shell: false,
});
const session: AcpSession = {
id: sessionId,
agentId,
process: child,
alive: true,
stdoutBuffer: "",
stderrBuffer: "",
createdAt: new Date(),
};
child.stdout?.on("data", (chunk: Buffer) => {
session.stdoutBuffer += chunk.toString();
this.emit("stdout", { sessionId, data: chunk.toString() });
});
child.stderr?.on("data", (chunk: Buffer) => {
session.stderrBuffer += chunk.toString();
this.emit("stderr", { sessionId, data: chunk.toString() });
});
child.on("exit", (code, signal) => {
session.alive = false;
this.emit("exit", { sessionId, code, signal });
});
child.on("error", (err) => {
session.alive = false;
this.emit("error", { sessionId, error: err });
});
this.sessions.set(sessionId, session);
return session;
}
/**
* Send input to a running session's stdin.
*/
sendInput(sessionId: string, input: string): boolean {
const session = this.sessions.get(sessionId);
if (!session?.alive || !session.process.stdin?.writable) return false;
session.process.stdin.write(input);
return true;
}
/**
* Send a prompt to a CLI agent and collect the response.
* This is a higher-level method that handles the send/receive cycle.
*/
async sendPrompt(sessionId: string, prompt: string, timeoutMs: number = 120000): Promise<string> {
const session = this.sessions.get(sessionId);
if (!session?.alive) throw new Error(`Session ${sessionId} is not alive`);
// Clear buffer before sending
session.stdoutBuffer = "";
// Send prompt
this.sendInput(sessionId, prompt + "\n");
// Wait for response (collect until process goes idle or timeout)
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`ACP timeout after ${timeoutMs}ms`));
}, timeoutMs);
let idleTimer: ReturnType<typeof setTimeout>;
const onData = ({ sessionId: sid }: { sessionId: string }) => {
if (sid !== sessionId) return;
// Reset idle timer on new data
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
clearTimeout(timer);
this.removeListener("stdout", onData);
this.removeListener("exit", onExit);
resolve(session.stdoutBuffer);
}, 2000); // 2s idle = response complete
};
const onExit = ({ sessionId: sid }: { sessionId: string }) => {
if (sid !== sessionId) return;
clearTimeout(timer);
clearTimeout(idleTimer);
this.removeListener("stdout", onData);
this.removeListener("exit", onExit);
resolve(session.stdoutBuffer);
};
this.on("stdout", onData);
this.on("exit", onExit);
});
}
/**
* Kill a session and clean up.
*/
kill(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
if (!session) return false;
if (session.alive) {
session.process.kill("SIGTERM");
// Force kill after 5s
setTimeout(() => {
if (session.alive) {
session.process.kill("SIGKILL");
}
}, 5000);
}
this.sessions.delete(sessionId);
return true;
}
/**
* Get all active sessions.
*/
getActiveSessions(): AcpSession[] {
return Array.from(this.sessions.values()).filter((s) => s.alive);
}
/**
* Get a specific session.
*/
getSession(sessionId: string): AcpSession | undefined {
return this.sessions.get(sessionId);
}
/**
* Kill all sessions.
*/
killAll(): void {
for (const [id] of this.sessions) {
this.kill(id);
}
}
}
// Singleton manager instance
export const acpManager = new AcpManager();
+280
View File
@@ -0,0 +1,280 @@
/**
* ACP (Agent Client Protocol) CLI Agent Registry
*
* Discovers installed CLI tools on the system by checking standard paths
* and running version commands. Used to offer ACP transport as an alternative
* to the HTTP proxy method.
*
* Supports 14 built-in agents + user-defined custom agents from settings.
*
* Reference: https://github.com/iOfficeAI/AionUi (auto-detects CLI agents)
*/
import { execSync } from "child_process";
export interface CliAgentInfo {
/** Agent identifier (e.g., "codex", "claude", "goose") */
id: string;
/** Display name */
name: string;
/** Binary name to spawn */
binary: string;
/** Version detection command */
versionCommand: string;
/** Detected version (null if not installed) */
version: string | null;
/** Whether the agent is installed and available */
installed: boolean;
/** Provider ID that this agent maps to in OmniRoute */
providerAlias: string;
/** Arguments to pass when spawning for ACP */
spawnArgs: string[];
/** Protocol used for communication */
protocol: "stdio" | "http";
/** Whether this is a user-defined custom agent */
isCustom?: boolean;
}
/** Shape stored in settings DB for custom agents */
export interface CustomAgentDef {
id: string;
name: string;
binary: string;
versionCommand: string;
providerAlias: string;
spawnArgs: string[];
protocol: "stdio" | "http";
}
/**
* Registry of known CLI agents that support ACP or similar protocols.
*/
const AGENT_DEFINITIONS: Omit<CliAgentInfo, "version" | "installed">[] = [
{
id: "codex",
name: "OpenAI Codex CLI",
binary: "codex",
versionCommand: "codex --version",
providerAlias: "codex",
spawnArgs: ["--quiet"],
protocol: "stdio",
},
{
id: "claude",
name: "Claude Code CLI",
binary: "claude",
versionCommand: "claude --version",
providerAlias: "claude",
spawnArgs: ["--print", "--output-format", "json"],
protocol: "stdio",
},
{
id: "goose",
name: "Goose CLI",
binary: "goose",
versionCommand: "goose --version",
providerAlias: "goose",
spawnArgs: [],
protocol: "stdio",
},
{
id: "gemini-cli",
name: "Gemini CLI",
binary: "gemini",
versionCommand: "gemini --version",
providerAlias: "gemini-cli",
spawnArgs: [],
protocol: "stdio",
},
{
id: "openclaw",
name: "OpenClaw",
binary: "openclaw",
versionCommand: "openclaw --version",
providerAlias: "openclaw",
spawnArgs: [],
protocol: "stdio",
},
{
id: "aider",
name: "Aider",
binary: "aider",
versionCommand: "aider --version",
providerAlias: "aider",
spawnArgs: ["--no-auto-commits"],
protocol: "stdio",
},
{
id: "opencode",
name: "OpenCode",
binary: "opencode",
versionCommand: "opencode --version",
providerAlias: "opencode",
spawnArgs: [],
protocol: "stdio",
},
{
id: "cline",
name: "Cline",
binary: "cline",
versionCommand: "cline --version",
providerAlias: "cline",
spawnArgs: [],
protocol: "stdio",
},
{
id: "qwen-code",
name: "Qwen Code",
binary: "qwen",
versionCommand: "qwen --version",
providerAlias: "qwen",
spawnArgs: [],
protocol: "stdio",
},
{
id: "forge",
name: "ForgeCode",
binary: "forge",
versionCommand: "forge --version",
providerAlias: "forge",
spawnArgs: [],
protocol: "stdio",
},
{
id: "amazon-q",
name: "Amazon Q Developer",
binary: "q",
versionCommand: "q --version",
providerAlias: "amazon-q",
spawnArgs: [],
protocol: "stdio",
},
{
id: "interpreter",
name: "Open Interpreter",
binary: "interpreter",
versionCommand: "interpreter --version",
providerAlias: "interpreter",
spawnArgs: [],
protocol: "stdio",
},
{
id: "cursor-cli",
name: "Cursor CLI",
binary: "cursor",
versionCommand: "cursor --version",
providerAlias: "cursor",
spawnArgs: [],
protocol: "stdio",
},
{
id: "warp",
name: "Warp AI",
binary: "warp",
versionCommand: "warp --version",
providerAlias: "warp",
spawnArgs: [],
protocol: "stdio",
},
];
// ---------------------------------------------------------------------------
// Detection cache (60 seconds)
// ---------------------------------------------------------------------------
let _cachedAgents: CliAgentInfo[] | null = null;
let _cacheTimestamp = 0;
const CACHE_TTL_MS = 60_000;
/** Custom agents loaded from settings */
let _customAgentDefs: CustomAgentDef[] = [];
/**
* Set custom agent definitions from settings.
*/
export function setCustomAgents(agents: CustomAgentDef[]): void {
_customAgentDefs = agents || [];
_cachedAgents = null; // invalidate cache
}
/**
* Get current custom agent definitions.
*/
export function getCustomAgentDefs(): CustomAgentDef[] {
return _customAgentDefs;
}
/**
* Detect a single agent by running its version command.
*/
function detectAgent(
def: Omit<CliAgentInfo, "version" | "installed">,
isCustom = false
): CliAgentInfo {
let version: string | null = null;
let installed = false;
try {
const output = execSync(def.versionCommand, {
timeout: 5000,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
// Extract version number from output
const versionMatch = output.match(/(\d+\.\d+\.\d+(?:-\w+)?)/);
version = versionMatch ? versionMatch[1] : output.split("\n")[0];
installed = true;
} catch {
// Not installed or not runnable
}
return { ...def, version, installed, isCustom };
}
/**
* Detect installed CLI agents on the system.
* Results are cached for 60 seconds.
*/
export function detectInstalledAgents(): CliAgentInfo[] {
const now = Date.now();
if (_cachedAgents && now - _cacheTimestamp < CACHE_TTL_MS) {
return _cachedAgents;
}
// Merge built-in + custom definitions
const allDefs = [
...AGENT_DEFINITIONS.map((d) => ({ ...d, _custom: false })),
..._customAgentDefs.map((d) => ({ ...d, _custom: true })),
];
_cachedAgents = allDefs.map((def) => {
const { _custom, ...rest } = def;
return detectAgent(rest, _custom);
});
_cacheTimestamp = now;
return _cachedAgents;
}
/**
* Force refresh detection cache.
*/
export function refreshAgentCache(): CliAgentInfo[] {
_cachedAgents = null;
return detectInstalledAgents();
}
/**
* Get a specific agent by ID.
*/
export function getAgentById(id: string): CliAgentInfo | undefined {
const agents = detectInstalledAgents();
return agents.find((a) => a.id === id);
}
/**
* Get agents that are installed and available for ACP.
*/
export function getAvailableAgents(): CliAgentInfo[] {
return detectInstalledAgents().filter((a) => a.installed);
}
+8 -2
View File
@@ -79,6 +79,7 @@ const SCHEMA_SQL = `
token_type TEXT,
consecutive_use_count INTEGER DEFAULT 0,
rate_limit_protection INTEGER DEFAULT 0,
last_used_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
@@ -311,6 +312,10 @@ function ensureProviderConnectionsColumns(db: SqliteDatabase) {
);
console.log("[DB] Added provider_connections.rate_limit_protection column");
}
if (!columnNames.has("last_used_at")) {
db.exec("ALTER TABLE provider_connections ADD COLUMN last_used_at TEXT");
console.log("[DB] Added provider_connections.last_used_at column");
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.warn("[DB] Failed to verify provider_connections schema:", message);
@@ -483,7 +488,7 @@ function migrateFromJson(db: SqliteDatabase, jsonPath: string) {
rate_limited_until, health_check_interval, last_health_check_at,
last_tested, api_key, id_token, provider_specific_data,
expires_in, display_name, global_priority, default_model,
token_type, consecutive_use_count, rate_limit_protection, created_at, updated_at
token_type, consecutive_use_count, rate_limit_protection, last_used_at, created_at, updated_at
) VALUES (
@id, @provider, @authType, @name, @email, @priority, @isActive,
@accessToken, @refreshToken, @expiresAt, @tokenExpiresAt,
@@ -492,7 +497,7 @@ function migrateFromJson(db: SqliteDatabase, jsonPath: string) {
@rateLimitedUntil, @healthCheckInterval, @lastHealthCheckAt,
@lastTested, @apiKey, @idToken, @providerSpecificData,
@expiresIn, @displayName, @globalPriority, @defaultModel,
@tokenType, @consecutiveUseCount, @rateLimitProtection, @createdAt, @updatedAt
@tokenType, @consecutiveUseCount, @rateLimitProtection, @lastUsedAt, @createdAt, @updatedAt
)
`);
@@ -533,6 +538,7 @@ function migrateFromJson(db: SqliteDatabase, jsonPath: string) {
defaultModel: conn.defaultModel || null,
tokenType: conn.tokenType || null,
consecutiveUseCount: conn.consecutiveUseCount || 0,
lastUsedAt: conn.lastUsedAt || null,
rateLimitProtection:
conn.rateLimitProtection === true || conn.rateLimitProtection === 1 ? 1 : 0,
createdAt: conn.createdAt || new Date().toISOString(),
+5 -2
View File
@@ -217,7 +217,7 @@ function _insertConnectionRow(db: DbLike, conn: JsonRecord) {
rate_limited_until, health_check_interval, last_health_check_at,
last_tested, api_key, id_token, provider_specific_data,
expires_in, display_name, global_priority, default_model,
token_type, consecutive_use_count, rate_limit_protection, created_at, updated_at
token_type, consecutive_use_count, rate_limit_protection, last_used_at, created_at, updated_at
) VALUES (
@id, @provider, @authType, @name, @email, @priority, @isActive,
@accessToken, @refreshToken, @expiresAt, @tokenExpiresAt,
@@ -226,7 +226,7 @@ function _insertConnectionRow(db: DbLike, conn: JsonRecord) {
@rateLimitedUntil, @healthCheckInterval, @lastHealthCheckAt,
@lastTested, @apiKey, @idToken, @providerSpecificData,
@expiresIn, @displayName, @globalPriority, @defaultModel,
@tokenType, @consecutiveUseCount, @rateLimitProtection, @createdAt, @updatedAt
@tokenType, @consecutiveUseCount, @rateLimitProtection, @lastUsedAt, @createdAt, @updatedAt
)
`
).run({
@@ -267,6 +267,7 @@ function _insertConnectionRow(db: DbLike, conn: JsonRecord) {
consecutiveUseCount: conn.consecutiveUseCount || 0,
rateLimitProtection:
conn.rateLimitProtection === true || conn.rateLimitProtection === 1 ? 1 : 0,
lastUsedAt: conn.lastUsedAt || null,
createdAt: conn.createdAt,
updatedAt: conn.updatedAt,
});
@@ -290,6 +291,7 @@ function _updateConnectionRow(db: DbLike, id: string, data: JsonRecord) {
default_model = @defaultModel, token_type = @tokenType,
consecutive_use_count = @consecutiveUseCount,
rate_limit_protection = @rateLimitProtection,
last_used_at = @lastUsedAt,
updated_at = @updatedAt
WHERE id = @id
`
@@ -331,6 +333,7 @@ function _updateConnectionRow(db: DbLike, id: string, data: JsonRecord) {
consecutiveUseCount: data.consecutiveUseCount || 0,
rateLimitProtection:
data.rateLimitProtection === true || data.rateLimitProtection === 1 ? 1 : 0,
lastUsedAt: data.lastUsedAt || null,
updatedAt: now,
});
}
+27 -6
View File
@@ -18,18 +18,27 @@ const navItemDefs = [
{ href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" },
{ href: "/dashboard/providers", i18nKey: "providers", icon: "dns" },
{ href: "/dashboard/combos", i18nKey: "combos", icon: "layers" },
{ href: "/dashboard/logs", i18nKey: "logs", icon: "description" },
{ href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" },
{ href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" },
{ href: "/dashboard/limits", i18nKey: "limits", icon: "tune" },
{ href: "/dashboard/health", i18nKey: "health", icon: "health_and_safety" },
{ href: "/dashboard/cli-tools", i18nKey: "cliTools", icon: "terminal" },
];
const cliItemDefs = [
{ href: "/dashboard/cli-tools", i18nKey: "cliToolsShort", icon: "terminal" },
{ href: "/dashboard/agents", i18nKey: "agents", icon: "smart_toy" },
];
const debugItemDefs = [
{ href: "/dashboard/translator", i18nKey: "translator", icon: "translate" },
{ href: "/dashboard/playground", i18nKey: "playground", icon: "science" },
{ href: "/dashboard/media", i18nKey: "media", icon: "auto_awesome" },
];
const debugItemDefs = [{ href: "/dashboard/translator", i18nKey: "translator", icon: "translate" }];
const systemItemDefs = [{ href: "/dashboard/settings", i18nKey: "settings", icon: "settings" }];
const systemItemDefs = [
{ href: "/dashboard/health", i18nKey: "health", icon: "health_and_safety" },
{ href: "/dashboard/logs", i18nKey: "logs", icon: "description" },
{ href: "/dashboard/settings", i18nKey: "settings", icon: "settings" },
];
const helpItemDefs = [
{ href: "/docs", i18nKey: "docs", icon: "menu_book" },
@@ -106,6 +115,7 @@ export default function Sidebar({
// Resolve i18n keys → labels
const resolveItems = (defs) => defs.map((d) => ({ ...d, label: t(d.i18nKey) }));
const navItems = resolveItems(navItemDefs);
const cliItems = resolveItems(cliItemDefs);
const debugItems = resolveItems(debugItemDefs);
const systemItems = resolveItems(systemItemDefs);
const helpItems = resolveItems(helpItemDefs);
@@ -234,6 +244,17 @@ export default function Sidebar({
>
{navItems.map(renderNavLink)}
{/* CLI section */}
<div className="pt-4 mt-2">
{!collapsed && (
<p className="px-4 text-xs font-semibold text-text-muted/60 uppercase tracking-wider mb-2">
CLI
</p>
)}
{collapsed && <div className="border-t border-black/5 dark:border-white/5 mb-2" />}
{cliItems.map(renderNavLink)}
</div>
{/* Debug section */}
{showDebug && (
<div className="pt-4 mt-2">
+8
View File
@@ -148,3 +148,11 @@ export function truncateUrl(url, max = 50) {
return url.length > max ? url.slice(0, max) + "…" : url;
}
}
/**
* Safely extract a finite number, returning undefined for invalid values.
* Used by quota normalization in both backend (quotaCache) and frontend (ProviderLimits).
*/
export function safePercentage(value: unknown): number | undefined {
return typeof value === "number" && isFinite(value) ? value : undefined;
}
-912
View File
@@ -1,912 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.dbBackupRestoreSchema =
exports.testComboSchema =
exports.updateComboSchema =
exports.cloudSyncActionSchema =
exports.cloudModelAliasUpdateSchema =
exports.cloudResolveAliasSchema =
exports.cloudCredentialUpdateSchema =
exports.kiroSocialExchangeSchema =
exports.kiroImportSchema =
exports.cursorImportSchema =
exports.oauthPollSchema =
exports.oauthExchangeSchema =
exports.translatorTranslateSchema =
exports.translatorSendSchema =
exports.translatorSaveSchema =
exports.translatorDetectSchema =
exports.testProxySchema =
exports.updateProxyConfigSchema =
exports.removeModelAliasSchema =
exports.addModelAliasSchema =
exports.updateModelAliasesSchema =
exports.updateIpFilterSchema =
exports.updateThinkingBudgetSchema =
exports.updateSystemPromptSchema =
exports.updateRequireLoginSchema =
exports.updateComboDefaultsSchema =
exports.resetStatsActionSchema =
exports.jsonObjectSchema =
exports.updateResilienceSchema =
exports.toggleRateLimitSchema =
exports.updatePricingSchema =
exports.providerModelMutationSchema =
exports.clearModelAvailabilitySchema =
exports.updateModelAliasSchema =
exports.removeFallbackSchema =
exports.registerFallbackSchema =
exports.policyActionSchema =
exports.setBudgetSchema =
exports.v1CountTokensSchema =
exports.providerChatCompletionSchema =
exports.v1RerankSchema =
exports.v1ModerationSchema =
exports.v1AudioSpeechSchema =
exports.v1ImageGenerationSchema =
exports.v1EmbeddingsSchema =
exports.loginSchema =
exports.updateSettingsSchema =
exports.createComboSchema =
exports.createKeySchema =
exports.createProviderSchema =
void 0;
exports.guideSettingsSaveSchema =
exports.codexProfileIdSchema =
exports.codexProfileNameSchema =
exports.cliModelConfigSchema =
exports.cliSettingsEnvSchema =
exports.cliBackupMutationSchema =
exports.cliMitmAliasUpdateSchema =
exports.cliMitmStopSchema =
exports.cliMitmStartSchema =
exports.v1betaGeminiGenerateSchema =
exports.validateProviderApiKeySchema =
exports.providersBatchTestSchema =
exports.updateProviderConnectionSchema =
exports.providerNodeValidateSchema =
exports.updateProviderNodeSchema =
exports.createProviderNodeSchema =
exports.updateKeyPermissionsSchema =
exports.evalRunSuiteSchema =
void 0;
exports.validateBody = validateBody;
var zod_1 = require("zod");
// ──── Provider Schemas ────
exports.createProviderSchema = zod_1.z.object({
provider: zod_1.z.string().min(1).max(100),
apiKey: zod_1.z.string().min(1).max(10000),
name: zod_1.z.string().min(1).max(200),
priority: zod_1.z.number().int().min(1).max(100).optional(),
globalPriority: zod_1.z.number().int().min(1).max(100).nullable().optional(),
defaultModel: zod_1.z.string().max(200).nullable().optional(),
testStatus: zod_1.z.string().max(50).optional(),
});
// ──── API Key Schemas ────
exports.createKeySchema = zod_1.z.object({
name: zod_1.z.string().min(1, "Name is required").max(200),
});
// ──── Combo Schemas ────
// A model entry can be a plain string (legacy) or an object with weight
var comboModelEntry = zod_1.z.union([
zod_1.z.string(),
zod_1.z.object({
model: zod_1.z.string().min(1),
weight: zod_1.z.number().min(0).max(100).default(0),
}),
]);
// Per-combo config overrides
var comboConfigSchema = zod_1.z
.object({
maxRetries: zod_1.z.number().int().min(0).max(10).optional(),
retryDelayMs: zod_1.z.number().int().min(0).max(60000).optional(),
timeoutMs: zod_1.z.number().int().min(1000).max(600000).optional(),
healthCheckEnabled: zod_1.z.boolean().optional(),
})
.optional();
var comboStrategySchema = zod_1.z.enum([
"priority",
"weighted",
"round-robin",
"random",
"least-used",
"cost-optimized",
]);
var comboRuntimeConfigSchema = zod_1.z
.object({
strategy: comboStrategySchema.optional(),
maxRetries: zod_1.z.coerce.number().int().min(0).max(10).optional(),
retryDelayMs: zod_1.z.coerce.number().int().min(0).max(60000).optional(),
timeoutMs: zod_1.z.coerce.number().int().min(1000).max(600000).optional(),
concurrencyPerModel: zod_1.z.coerce.number().int().min(1).max(20).optional(),
queueTimeoutMs: zod_1.z.coerce.number().int().min(1000).max(120000).optional(),
healthCheckEnabled: zod_1.z.boolean().optional(),
healthCheckTimeoutMs: zod_1.z.coerce.number().int().min(100).max(30000).optional(),
maxComboDepth: zod_1.z.coerce.number().int().min(1).max(10).optional(),
trackMetrics: zod_1.z.boolean().optional(),
})
.strict();
exports.createComboSchema = zod_1.z.object({
name: zod_1.z
.string()
.min(1, "Name is required")
.max(100)
.regex(/^[a-zA-Z0-9_/.-]+$/, "Name can only contain letters, numbers, -, _, / and ."),
models: zod_1.z.array(comboModelEntry).optional().default([]),
strategy: comboStrategySchema.optional().default("priority"),
config: comboConfigSchema,
});
// ──── Settings Schemas ────
// FASE-01: Removed .passthrough() — only explicitly listed fields are accepted
exports.updateSettingsSchema = zod_1.z.object({
newPassword: zod_1.z.string().min(1).max(200).optional(),
currentPassword: zod_1.z.string().max(200).optional(),
theme: zod_1.z.string().max(50).optional(),
language: zod_1.z.string().max(10).optional(),
requireLogin: zod_1.z.boolean().optional(),
enableRequestLogs: zod_1.z.boolean().optional(),
enableSocks5Proxy: zod_1.z.boolean().optional(),
instanceName: zod_1.z.string().max(100).optional(),
corsOrigins: zod_1.z.string().max(500).optional(),
logRetentionDays: zod_1.z.number().int().min(1).max(365).optional(),
cloudUrl: zod_1.z.string().max(500).optional(),
baseUrl: zod_1.z.string().max(500).optional(),
setupComplete: zod_1.z.boolean().optional(),
requireAuthForModels: zod_1.z.boolean().optional(),
blockedProviders: zod_1.z.array(zod_1.z.string().max(100)).optional(),
hideHealthCheckLogs: zod_1.z.boolean().optional(),
// Routing settings (#134)
fallbackStrategy: zod_1.z
.enum(["fill-first", "round-robin", "p2c", "random", "least-used", "cost-optimized"])
.optional(),
wildcardAliases: zod_1.z
.array(zod_1.z.object({ pattern: zod_1.z.string(), target: zod_1.z.string() }))
.optional(),
stickyRoundRobinLimit: zod_1.z.number().int().min(0).max(1000).optional(),
});
// ──── Auth Schemas ────
exports.loginSchema = zod_1.z.object({
password: zod_1.z.string().min(1, "Password is required").max(200),
});
// ──── API Route Payload Schemas (T06) ────
var modelIdSchema = zod_1.z.string().trim().min(1, "Model is required").max(200);
var nonEmptyStringSchema = zod_1.z.string().trim().min(1, "Field is required");
var embeddingTokenArraySchema = zod_1.z
.array(zod_1.z.number().int().min(0))
.min(1, "input token array must contain at least one item");
var embeddingInputSchema = zod_1.z.union([
nonEmptyStringSchema,
zod_1.z.array(nonEmptyStringSchema).min(1, "input must contain at least one item"),
embeddingTokenArraySchema,
zod_1.z.array(embeddingTokenArraySchema).min(1, "input must contain at least one item"),
]);
var chatMessageSchema = zod_1.z
.object({
role: zod_1.z.string().trim().min(1, "messages[].role is required"),
content: zod_1.z
.union([nonEmptyStringSchema, zod_1.z.array(zod_1.z.unknown()).min(1), zod_1.z.null()])
.optional(),
})
.catchall(zod_1.z.unknown());
var countTokensMessageSchema = zod_1.z
.object({
content: zod_1.z.union([
nonEmptyStringSchema,
zod_1.z
.array(
zod_1.z
.object({
type: zod_1.z.string().optional(),
text: zod_1.z.string().optional(),
})
.catchall(zod_1.z.unknown())
)
.min(1, "messages[].content must contain at least one item"),
]),
})
.catchall(zod_1.z.unknown());
exports.v1EmbeddingsSchema = zod_1.z
.object({
model: modelIdSchema,
input: embeddingInputSchema,
dimensions: zod_1.z.coerce.number().int().positive().optional(),
encoding_format: zod_1.z.enum(["float", "base64"]).optional(),
})
.catchall(zod_1.z.unknown());
exports.v1ImageGenerationSchema = zod_1.z
.object({
model: modelIdSchema,
prompt: nonEmptyStringSchema,
})
.catchall(zod_1.z.unknown());
exports.v1AudioSpeechSchema = zod_1.z
.object({
model: modelIdSchema,
input: nonEmptyStringSchema,
})
.catchall(zod_1.z.unknown());
exports.v1ModerationSchema = zod_1.z
.object({
model: modelIdSchema.optional(),
input: zod_1.z.unknown().refine(function (value) {
if (value === undefined || value === null) return false;
if (typeof value === "string") return value.trim().length > 0;
if (Array.isArray(value)) return value.length > 0;
return true;
}, "Input is required"),
})
.catchall(zod_1.z.unknown());
exports.v1RerankSchema = zod_1.z
.object({
model: modelIdSchema,
query: nonEmptyStringSchema,
documents: zod_1.z.array(zod_1.z.unknown()).min(1, "documents must contain at least one item"),
})
.catchall(zod_1.z.unknown());
exports.providerChatCompletionSchema = zod_1.z
.object({
model: modelIdSchema,
messages: zod_1.z.array(chatMessageSchema).min(1).optional(),
input: zod_1.z
.union([nonEmptyStringSchema, zod_1.z.array(zod_1.z.unknown()).min(1)])
.optional(),
prompt: nonEmptyStringSchema.optional(),
})
.catchall(zod_1.z.unknown())
.superRefine(function (value, ctx) {
if (value.messages === undefined && value.input === undefined && value.prompt === undefined) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "messages, input or prompt is required",
path: [],
});
}
});
exports.v1CountTokensSchema = zod_1.z
.object({
messages: zod_1.z
.array(countTokensMessageSchema)
.min(1, "messages must contain at least one item"),
})
.catchall(zod_1.z.unknown());
exports.setBudgetSchema = zod_1.z.object({
apiKeyId: zod_1.z.string().trim().min(1, "apiKeyId is required"),
dailyLimitUsd: zod_1.z.coerce.number().positive("dailyLimitUsd must be greater than zero"),
monthlyLimitUsd: zod_1.z.coerce
.number()
.positive("monthlyLimitUsd must be greater than zero")
.optional(),
warningThreshold: zod_1.z.coerce.number().min(0).max(1).optional(),
});
exports.policyActionSchema = zod_1.z
.object({
action: zod_1.z.enum(["unlock"]),
identifier: zod_1.z.string().trim().min(1).optional(),
})
.superRefine(function (value, ctx) {
if (value.action === "unlock" && !value.identifier) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "identifier is required for unlock action",
path: ["identifier"],
});
}
});
var fallbackChainEntrySchema = zod_1.z
.object({
provider: zod_1.z.string().trim().min(1, "provider is required"),
priority: zod_1.z.number().int().min(1).max(100).optional(),
enabled: zod_1.z.boolean().optional(),
})
.catchall(zod_1.z.unknown());
exports.registerFallbackSchema = zod_1.z.object({
model: modelIdSchema,
chain: zod_1.z.array(fallbackChainEntrySchema).min(1, "chain must contain at least one provider"),
});
exports.removeFallbackSchema = zod_1.z.object({
model: modelIdSchema,
});
exports.updateModelAliasSchema = zod_1.z.object({
model: modelIdSchema,
alias: zod_1.z.string().trim().min(1, "Alias is required").max(200),
});
exports.clearModelAvailabilitySchema = zod_1.z.object({
provider: zod_1.z.string().trim().min(1, "provider is required").max(120),
model: modelIdSchema,
});
exports.providerModelMutationSchema = zod_1.z.object({
provider: zod_1.z.string().trim().min(1, "provider is required").max(120),
modelId: zod_1.z.string().trim().min(1, "modelId is required").max(240),
modelName: zod_1.z.string().trim().max(240).optional(),
source: zod_1.z.string().trim().max(80).optional(),
});
var pricingFieldsSchema = zod_1.z
.object({
input: zod_1.z.number().min(0).optional(),
output: zod_1.z.number().min(0).optional(),
cached: zod_1.z.number().min(0).optional(),
reasoning: zod_1.z.number().min(0).optional(),
cache_creation: zod_1.z.number().min(0).optional(),
})
.strict();
exports.updatePricingSchema = zod_1.z.record(
zod_1.z.string().trim().min(1),
zod_1.z.record(zod_1.z.string().trim().min(1), pricingFieldsSchema)
);
exports.toggleRateLimitSchema = zod_1.z.object({
connectionId: zod_1.z.string().trim().min(1, "connectionId is required"),
enabled: zod_1.z.boolean(),
});
var resilienceProfileSchema = zod_1.z.object({
transientCooldown: zod_1.z.number().min(0),
rateLimitCooldown: zod_1.z.number().min(0),
maxBackoffLevel: zod_1.z.number().int().min(0),
circuitBreakerThreshold: zod_1.z.number().int().min(0),
circuitBreakerReset: zod_1.z.number().min(0),
});
var resilienceDefaultsSchema = zod_1.z
.object({
requestsPerMinute: zod_1.z.number().int().min(1).optional(),
minTimeBetweenRequests: zod_1.z.number().int().min(1).optional(),
concurrentRequests: zod_1.z.number().int().min(1).optional(),
})
.strict();
exports.updateResilienceSchema = zod_1.z
.object({
profiles: zod_1.z
.object({
oauth: resilienceProfileSchema.optional(),
apikey: resilienceProfileSchema.optional(),
})
.strict()
.optional(),
defaults: resilienceDefaultsSchema.optional(),
})
.superRefine(function (value, ctx) {
if (!value.profiles && !value.defaults) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "Must provide profiles or defaults",
path: [],
});
}
});
exports.jsonObjectSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
exports.resetStatsActionSchema = zod_1.z.object({
action: zod_1.z.literal("reset-stats"),
});
exports.updateComboDefaultsSchema = zod_1.z
.object({
comboDefaults: comboRuntimeConfigSchema.optional(),
providerOverrides: zod_1.z
.record(zod_1.z.string().trim().min(1), comboRuntimeConfigSchema)
.optional(),
})
.superRefine(function (value, ctx) {
if (!value.comboDefaults && !value.providerOverrides) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "Nothing to update",
path: [],
});
}
});
exports.updateRequireLoginSchema = zod_1.z
.object({
requireLogin: zod_1.z.boolean().optional(),
password: zod_1.z.string().min(4, "Password must be at least 4 characters").optional(),
})
.superRefine(function (value, ctx) {
if (value.requireLogin === undefined && !value.password) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.updateSystemPromptSchema = zod_1.z
.object({
prompt: zod_1.z.string().max(50000).optional(),
enabled: zod_1.z.boolean().optional(),
})
.strict()
.superRefine(function (value, ctx) {
if (value.prompt === undefined && value.enabled === undefined) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.updateThinkingBudgetSchema = zod_1.z
.object({
mode: zod_1.z.enum(["passthrough", "auto", "custom", "adaptive"]).optional(),
customBudget: zod_1.z.coerce.number().int().min(0).max(131072).optional(),
effortLevel: zod_1.z.enum(["none", "low", "medium", "high"]).optional(),
baseBudget: zod_1.z.coerce.number().int().min(0).max(131072).optional(),
complexityMultiplier: zod_1.z.coerce.number().min(0).optional(),
})
.strict()
.superRefine(function (value, ctx) {
if (
value.mode === undefined &&
value.customBudget === undefined &&
value.effortLevel === undefined &&
value.baseBudget === undefined &&
value.complexityMultiplier === undefined
) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
var ipFilterModeSchema = zod_1.z.enum(["blacklist", "whitelist"]);
var tempBanSchema = zod_1.z.object({
ip: zod_1.z.string().trim().min(1),
durationMs: zod_1.z.coerce.number().int().min(1).optional(),
reason: zod_1.z.string().max(200).optional(),
});
exports.updateIpFilterSchema = zod_1.z
.object({
enabled: zod_1.z.boolean().optional(),
mode: ipFilterModeSchema.optional(),
blacklist: zod_1.z.array(zod_1.z.string()).optional(),
whitelist: zod_1.z.array(zod_1.z.string()).optional(),
addBlacklist: zod_1.z.string().optional(),
removeBlacklist: zod_1.z.string().optional(),
addWhitelist: zod_1.z.string().optional(),
removeWhitelist: zod_1.z.string().optional(),
tempBan: tempBanSchema.optional(),
removeBan: zod_1.z.string().optional(),
})
.strict()
.superRefine(function (value, ctx) {
if (Object.keys(value).length === 0) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.updateModelAliasesSchema = zod_1.z.object({
aliases: zod_1.z.record(zod_1.z.string().trim().min(1), zod_1.z.string().trim().min(1)),
});
exports.addModelAliasSchema = zod_1.z.object({
from: zod_1.z.string().trim().min(1),
to: zod_1.z.string().trim().min(1),
});
exports.removeModelAliasSchema = zod_1.z.object({
from: zod_1.z.string().trim().min(1),
});
var proxyConfigSchema = zod_1.z
.object({
type: zod_1.z
.preprocess(
function (value) {
return typeof value === "string" ? value.trim().toLowerCase() : value;
},
zod_1.z.enum(["http", "https", "socks5"])
)
.optional(),
host: zod_1.z.string().trim().min(1).optional(),
port: zod_1.z.coerce.number().int().min(1).max(65535).optional(),
username: zod_1.z.string().optional(),
password: zod_1.z.string().optional(),
})
.strict();
exports.updateProxyConfigSchema = zod_1.z
.object({
proxy: proxyConfigSchema.nullable().optional(),
global: proxyConfigSchema.nullable().optional(),
providers: zod_1.z
.record(zod_1.z.string().trim().min(1), proxyConfigSchema.nullable())
.optional(),
combos: zod_1.z.record(zod_1.z.string().trim().min(1), proxyConfigSchema.nullable()).optional(),
keys: zod_1.z.record(zod_1.z.string().trim().min(1), proxyConfigSchema.nullable()).optional(),
level: zod_1.z.enum(["global", "provider", "combo", "key"]).optional(),
id: zod_1.z.string().optional(),
})
.strict()
.superRefine(function (value, ctx) {
var _a;
var hasPayload =
value.proxy !== undefined ||
value.global !== undefined ||
value.providers !== undefined ||
value.combos !== undefined ||
value.keys !== undefined ||
value.level !== undefined;
if (!hasPayload) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
if (value.level !== undefined && value.proxy === undefined) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "proxy is required when level is provided",
path: ["proxy"],
});
}
if (
value.level &&
value.level !== "global" &&
!((_a = value.id) === null || _a === void 0 ? void 0 : _a.trim())
) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "id is required for provider/combo/key level updates",
path: ["id"],
});
}
});
exports.testProxySchema = zod_1.z.object({
proxy: zod_1.z.object({
type: zod_1.z.string().optional(),
host: zod_1.z.string().trim().min(1, "proxy.host is required"),
port: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]),
username: zod_1.z.string().optional(),
password: zod_1.z.string().optional(),
}),
});
var jsonRecordSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
var nonEmptyJsonRecordSchema = jsonRecordSchema.refine(function (value) {
return Object.keys(value).length > 0;
}, "Body must be a non-empty object");
var translatorLogFileSchema = zod_1.z.enum([
"1_req_client.json",
"2_req_source.json",
"3_req_openai.json",
"4_req_target.json",
"5_res_provider.txt",
]);
exports.translatorDetectSchema = zod_1.z.object({
body: nonEmptyJsonRecordSchema,
});
exports.translatorSaveSchema = zod_1.z.object({
file: translatorLogFileSchema,
content: zod_1.z.string().min(1, "Content is required").max(1000000, "Content is too large"),
});
exports.translatorSendSchema = zod_1.z.object({
provider: zod_1.z.string().trim().min(1, "Provider is required"),
body: nonEmptyJsonRecordSchema,
});
exports.translatorTranslateSchema = zod_1.z
.object({
step: zod_1.z.union([zod_1.z.number().int().min(1).max(4), zod_1.z.literal("direct")]),
provider: zod_1.z.string().trim().min(1).optional(),
body: nonEmptyJsonRecordSchema,
sourceFormat: zod_1.z.string().optional(),
targetFormat: zod_1.z.string().optional(),
})
.superRefine(function (value, ctx) {
if (value.step !== "direct" && !value.provider) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "Step and provider are required",
path: ["provider"],
});
}
});
exports.oauthExchangeSchema = zod_1.z.object({
code: zod_1.z.string().trim().min(1),
redirectUri: zod_1.z.string().trim().min(1),
codeVerifier: zod_1.z.string().trim().min(1),
state: zod_1.z.string().optional(),
});
exports.oauthPollSchema = zod_1.z.object({
deviceCode: zod_1.z.string().trim().min(1),
codeVerifier: zod_1.z.string().optional(),
extraData: zod_1.z.unknown().optional(),
});
exports.cursorImportSchema = zod_1.z.object({
accessToken: zod_1.z.string().trim().min(1, "Access token is required"),
machineId: zod_1.z.string().trim().min(1, "Machine ID is required"),
});
exports.kiroImportSchema = zod_1.z.object({
refreshToken: zod_1.z.string().trim().min(1, "Refresh token is required"),
});
exports.kiroSocialExchangeSchema = zod_1.z.object({
code: zod_1.z.string().trim().min(1, "Code is required"),
codeVerifier: zod_1.z.string().trim().min(1, "Code verifier is required"),
provider: zod_1.z.enum(["google", "github"]),
});
exports.cloudCredentialUpdateSchema = zod_1.z.object({
provider: zod_1.z.string().trim().min(1, "Provider is required"),
credentials: zod_1.z
.object({
accessToken: zod_1.z.string().optional(),
refreshToken: zod_1.z.string().optional(),
expiresIn: zod_1.z.coerce.number().positive().optional(),
})
.strict()
.superRefine(function (value, ctx) {
if (
value.accessToken === undefined &&
value.refreshToken === undefined &&
value.expiresIn === undefined
) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "At least one credential field must be provided",
path: [],
});
}
}),
});
exports.cloudResolveAliasSchema = zod_1.z.object({
alias: zod_1.z.string().trim().min(1, "Missing alias"),
});
exports.cloudModelAliasUpdateSchema = zod_1.z.object({
model: zod_1.z.string().trim().min(1, "Model and alias required"),
alias: zod_1.z.string().trim().min(1, "Model and alias required"),
});
exports.cloudSyncActionSchema = zod_1.z.object({
action: zod_1.z.enum(["enable", "sync", "disable"]),
});
exports.updateComboSchema = zod_1.z
.object({
name: zod_1.z
.string()
.min(1, "Name is required")
.max(100)
.regex(/^[a-zA-Z0-9_/.-]+$/, "Name can only contain letters, numbers, -, _, / and .")
.optional(),
models: zod_1.z.array(comboModelEntry).optional(),
strategy: comboStrategySchema.optional(),
config: comboRuntimeConfigSchema.optional(),
isActive: zod_1.z.boolean().optional(),
})
.superRefine(function (value, ctx) {
if (
value.name === undefined &&
value.models === undefined &&
value.strategy === undefined &&
value.config === undefined &&
value.isActive === undefined
) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.testComboSchema = zod_1.z.object({
comboName: zod_1.z.string().trim().min(1, "comboName is required"),
});
exports.dbBackupRestoreSchema = zod_1.z.object({
backupId: zod_1.z.string().trim().min(1, "backupId is required"),
});
exports.evalRunSuiteSchema = zod_1.z.object({
suiteId: zod_1.z.string().trim().min(1, "suiteId is required"),
outputs: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
});
exports.updateKeyPermissionsSchema = zod_1.z
.object({
allowedModels: zod_1.z.array(zod_1.z.string().trim().min(1)).max(1000).optional(),
noLog: zod_1.z.boolean().optional(),
})
.superRefine(function (value, ctx) {
if (value.allowedModels === undefined && value.noLog === undefined) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.createProviderNodeSchema = zod_1.z
.object({
name: zod_1.z.string().trim().min(1, "Name is required"),
prefix: zod_1.z.string().trim().min(1, "Prefix is required"),
apiType: zod_1.z.enum(["chat", "responses"]).optional(),
baseUrl: zod_1.z.string().trim().min(1).optional(),
type: zod_1.z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
})
.superRefine(function (value, ctx) {
var nodeType = value.type || "openai-compatible";
if (nodeType === "openai-compatible" && !value.apiType) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "Invalid OpenAI compatible API type",
path: ["apiType"],
});
}
});
exports.updateProviderNodeSchema = zod_1.z.object({
name: zod_1.z.string().trim().min(1, "Name is required"),
prefix: zod_1.z.string().trim().min(1, "Prefix is required"),
apiType: zod_1.z.enum(["chat", "responses"]).optional(),
baseUrl: zod_1.z.string().trim().min(1, "Base URL is required"),
});
exports.providerNodeValidateSchema = zod_1.z.object({
baseUrl: zod_1.z.string().trim().min(1, "Base URL and API key required"),
apiKey: zod_1.z.string().trim().min(1, "Base URL and API key required"),
type: zod_1.z.enum(["openai-compatible", "anthropic-compatible"]).optional(),
});
exports.updateProviderConnectionSchema = zod_1.z
.object({
name: zod_1.z.string().max(200).optional(),
priority: zod_1.z.coerce.number().int().min(1).max(100).optional(),
globalPriority: zod_1.z
.union([zod_1.z.coerce.number().int().min(1).max(100), zod_1.z.null()])
.optional(),
defaultModel: zod_1.z.union([zod_1.z.string().max(200), zod_1.z.null()]).optional(),
isActive: zod_1.z.boolean().optional(),
apiKey: zod_1.z.string().max(10000).optional(),
testStatus: zod_1.z.string().max(50).optional(),
lastError: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
lastErrorAt: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
lastErrorType: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
lastErrorSource: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
errorCode: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
rateLimitedUntil: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
lastTested: zod_1.z.union([zod_1.z.string(), zod_1.z.null()]).optional(),
healthCheckInterval: zod_1.z.coerce.number().int().min(0).optional(),
})
.superRefine(function (value, ctx) {
if (Object.keys(value).length === 0) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "No valid fields to update",
path: [],
});
}
});
exports.providersBatchTestSchema = zod_1.z
.object({
mode: zod_1.z.enum(["provider", "oauth", "free", "apikey", "compatible", "all"]),
providerId: zod_1.z.string().trim().min(1).optional(),
})
.superRefine(function (value, ctx) {
if (value.mode === "provider" && !value.providerId) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "providerId is required when mode=provider",
path: ["providerId"],
});
}
});
exports.validateProviderApiKeySchema = zod_1.z.object({
provider: zod_1.z.string().trim().min(1, "Provider and API key required"),
apiKey: zod_1.z.string().trim().min(1, "Provider and API key required"),
});
var geminiPartSchema = zod_1.z
.object({
text: zod_1.z.string().optional(),
})
.catchall(zod_1.z.unknown());
var geminiContentSchema = zod_1.z
.object({
role: zod_1.z.string().optional(),
parts: zod_1.z.array(geminiPartSchema).optional(),
})
.catchall(zod_1.z.unknown());
exports.v1betaGeminiGenerateSchema = zod_1.z
.object({
contents: zod_1.z.array(geminiContentSchema).optional(),
systemInstruction: zod_1.z
.object({
parts: zod_1.z.array(geminiPartSchema).optional(),
})
.catchall(zod_1.z.unknown())
.optional(),
generationConfig: zod_1.z
.object({
stream: zod_1.z.boolean().optional(),
maxOutputTokens: zod_1.z.coerce.number().int().min(1).optional(),
temperature: zod_1.z.coerce.number().optional(),
topP: zod_1.z.coerce.number().optional(),
})
.catchall(zod_1.z.unknown())
.optional(),
})
.catchall(zod_1.z.unknown())
.superRefine(function (value, ctx) {
if (!value.contents && !value.systemInstruction) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "contents or systemInstruction is required",
path: [],
});
}
});
exports.cliMitmStartSchema = zod_1.z.object({
apiKey: zod_1.z.string().trim().min(1, "Missing apiKey"),
sudoPassword: zod_1.z.string().optional(),
});
exports.cliMitmStopSchema = zod_1.z.object({
sudoPassword: zod_1.z.string().optional(),
});
exports.cliMitmAliasUpdateSchema = zod_1.z.object({
tool: zod_1.z.string().trim().min(1, "tool and mappings required"),
mappings: zod_1.z.record(zod_1.z.string(), zod_1.z.string().optional()),
});
exports.cliBackupMutationSchema = zod_1.z
.object({
tool: zod_1.z.string().trim().min(1).optional(),
toolId: zod_1.z.string().trim().min(1).optional(),
backupId: zod_1.z.string().trim().min(1, "tool and backupId are required"),
})
.superRefine(function (value, ctx) {
if (!value.tool && !value.toolId) {
ctx.addIssue({
code: zod_1.z.ZodIssueCode.custom,
message: "tool and backupId are required",
path: ["tool"],
});
}
});
var envKeySchema = zod_1.z
.string()
.trim()
.min(1, "Environment key is required")
.max(120)
.regex(/^[A-Z_][A-Z0-9_]*$/, "Invalid environment key format");
var envValueSchema = zod_1.z
.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()])
.transform(function (value) {
return String(value);
})
.refine(function (value) {
return value.length > 0;
}, "Environment value is required")
.refine(function (value) {
return value.length <= 10000;
}, "Environment value is too long");
exports.cliSettingsEnvSchema = zod_1.z.object({
env: zod_1.z.record(envKeySchema, envValueSchema).refine(function (value) {
return Object.keys(value).length > 0;
}, "env must contain at least one key"),
});
exports.cliModelConfigSchema = zod_1.z.object({
baseUrl: zod_1.z.string().trim().min(1, "baseUrl and model are required"),
apiKey: zod_1.z.string().optional(),
model: zod_1.z.string().trim().min(1, "baseUrl and model are required"),
});
exports.codexProfileNameSchema = zod_1.z.object({
name: zod_1.z.string().trim().min(1, "Profile name is required"),
});
exports.codexProfileIdSchema = zod_1.z.object({
profileId: zod_1.z.string().trim().min(1, "profileId is required"),
});
exports.guideSettingsSaveSchema = zod_1.z.object({
baseUrl: zod_1.z.string().trim().min(1).optional(),
apiKey: zod_1.z.string().optional(),
model: zod_1.z.string().trim().min(1, "Model is required"),
});
// ──── Helper ────
/**
* Parse and validate request body with a Zod schema.
* Returns { success: true, data } or { success: false, error }.
*/
function validateBody(schema, body) {
var _a;
var result = schema.safeParse(body);
if (result.success) {
return { success: true, data: result.data };
}
var issues = Array.isArray((_a = result.error) === null || _a === void 0 ? void 0 : _a.issues)
? result.error.issues
: [];
return {
success: false,
error: {
message: "Invalid request",
details: issues.map(function (e) {
return {
field: e.path.join("."),
message: e.message,
};
}),
},
};
}
+16
View File
@@ -34,4 +34,20 @@ export const updateSettingsSchema = z.object({
mcpEnabled: z.boolean().optional(),
mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(),
a2aEnabled: z.boolean().optional(),
// CLI Fingerprint compatibility (per-provider)
cliCompatProviders: z.array(z.string().max(100)).optional(),
// Custom CLI agent definitions for ACP
customAgents: z
.array(
z.object({
id: z.string().max(50),
name: z.string().max(100),
binary: z.string().max(200),
versionCommand: z.string().max(300),
providerAlias: z.string().max(50),
spawnArgs: z.array(z.string().max(200)),
protocol: z.enum(["stdio", "http"]),
})
)
.optional(),
});
+30 -11
View File
@@ -31,7 +31,12 @@ import { sanitizeRequest } from "../../shared/utils/inputSanitizer";
// Pipeline integration — wired modules
import { getCircuitBreaker, CircuitBreakerOpenError } from "../../shared/utils/circuitBreaker";
import { isModelAvailable, setModelUnavailable } from "../../domain/modelAvailability";
import {
isModelAvailable,
setModelUnavailable,
clearModelUnavailability,
} from "../../domain/modelAvailability";
import { markAccountExhaustedFrom429 } from "../../domain/quotaCache";
import { RequestTelemetry, recordTelemetry } from "../../shared/utils/requestTelemetry";
import { generateRequestId } from "../../shared/utils/requestId";
import { recordCost } from "../../domain/costRules";
@@ -127,7 +132,10 @@ export async function handleChat(request: any, clientRawRequest: any = null) {
telemetry.startPhase("policy");
const policy = await enforceApiKeyPolicy(request, modelStr);
if (policy.rejection) {
log.warn("POLICY", `API key policy rejected: ${modelStr} (key=${policy.apiKeyInfo?.id || "unknown"})`);
log.warn(
"POLICY",
`API key policy rejected: ${modelStr} (key=${policy.apiKeyInfo?.id || "unknown"})`
);
return policy.rejection;
}
const apiKeyInfo = policy.apiKeyInfo;
@@ -243,6 +251,13 @@ async function handleSingleModelChat(
const credentials = await getProviderCredentials(provider, excludeConnectionId);
if (!credentials || credentials.allRateLimited) {
if (lastStatus === 429 || lastStatus === 503) {
setModelUnavailable(provider, model, 60000, `HTTP ${lastStatus}`);
log.info(
"AVAILABILITY",
`${provider}/${model} marked unavailable — all accounts exhausted (HTTP ${lastStatus})`
);
}
return handleNoCredentials(
credentials,
excludeConnectionId,
@@ -296,22 +311,19 @@ async function handleSingleModelChat(
});
if (result.success) {
clearModelUnavailability(provider, model);
recordCostIfNeeded(apiKeyInfo, result);
if (telemetry) telemetry.startPhase("finalize");
if (telemetry) telemetry.endPhase();
return result.response;
}
// Pipeline: Mark model unavailable on repeated failures
if (result.status === 429 || result.status === 503) {
setModelUnavailable(provider, model, 60000, `HTTP ${result.status}`);
log.info(
"AVAILABILITY",
`${provider}/${model} marked unavailable for 60s (HTTP ${result.status})`
);
// 6. Mark account as quota-exhausted on 429 response
if (result.status === 429) {
markAccountExhaustedFrom429(credentials.connectionId, provider);
}
// 6. Fallback to next account
// 7. Fallback to next account
const { shouldFallback } = await markAccountUnavailable(
credentials.connectionId,
result.status,
@@ -357,7 +369,14 @@ async function resolveModelOrError(modelStr: string, body: any) {
const { provider, model } = modelInfo;
const sourceFormat = detectFormat(body);
const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider);
// If the custom model specifies apiFormat="responses", override targetFormat
// to route through the Responses API translator instead of Chat Completions
let targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider);
if ((modelInfo as any).apiFormat === "responses") {
targetFormat = "openai-responses";
log.info("ROUTING", `Custom model apiFormat=responses → targetFormat=openai-responses`);
}
if (modelStr !== `${provider}/${model}`) {
log.info("ROUTING", `${modelStr}${provider}/${model}`);

Some files were not shown because too many files have changed in this diff Show More