Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2306081dab | |||
| 8963c62adb | |||
| e4a11bd6d0 | |||
| 2f6e63771f | |||
| cbd60c853e | |||
| 2d8091340f | |||
| 2025c16c82 | |||
| 811fb7f9b2 | |||
| a2bd32e76c | |||
| 89c07f4e8a | |||
| ac89069671 | |||
| 7cb420d8e6 | |||
| d3919d441f | |||
| 4b5824babc | |||
| fb87df14fd | |||
| da9e4e929b | |||
| 10b23b15ae | |||
| 30fba39b35 | |||
| 5a75ff67c9 | |||
| 358828b617 | |||
| e080c4a16a | |||
| 04b7e38baf | |||
| 7ee23fbe19 | |||
| c49bdb4ebb | |||
| 0f7efed8d5 | |||
| d07bc6dcf3 | |||
| d607d46fa3 | |||
| 2225dd14aa | |||
| f6c0e7bbbe | |||
| c4675c5219 | |||
| 2d977a3c4d | |||
| 9405918258 | |||
| a69d7dd4b5 | |||
| 428e6cb53f | |||
| c9a2955d28 | |||
| 7aefcd3437 | |||
| 79f4f79c46 | |||
| c11c275678 | |||
| bbcd1d3a08 | |||
| 3342d5b931 | |||
| f96ee44213 | |||
| bc53fe0cd9 | |||
| 97a67b5d3e | |||
| 1ffa58be76 | |||
| a5cf51c0b9 | |||
| 3d38cbf70f | |||
| 196a4e037c | |||
| bfe495931f | |||
| 11bcdd810a |
@@ -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.
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -117,3 +117,8 @@ icon.iconset/
|
||||
|
||||
# VS Code Extension (independent Git repo)
|
||||
vscode-extension/
|
||||
|
||||
# SQLite residual files
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
*.sqlite-journal
|
||||
|
||||
+210
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
| ------------------------------------ | -------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 בחינם לקידוד בלתי מוגבל._
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 経由で接続します。_
|
||||
|
||||
@@ -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 도구를 연결하세요._
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
### 📊 Наблюдаемость и аналитика
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 ฟรีสำหรับการเข้ารหัสไม่จำกัด_
|
||||
|
||||
@@ -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 для необмеженого програмування._
|
||||
|
||||
@@ -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._
|
||||
|
||||
@@ -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 网关,无限编程。_
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+6
-15
@@ -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
@@ -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),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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), {
|
||||
|
||||
@@ -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 *));
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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": "مترجم",
|
||||
|
||||
@@ -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": "Преводач",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ä",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "מתרגם",
|
||||
|
||||
@@ -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ó",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "अनुवादक",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "翻訳者",
|
||||
|
||||
@@ -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": "번역기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Переводчик",
|
||||
|
||||
@@ -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ľ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "นักแปล",
|
||||
|
||||
@@ -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": "Перекладач",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "翻译者",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user