Compare commits

..

29 Commits

Author SHA1 Message Date
diegosouzapw a864258cb8 feat(ui): integrate FSM, adaptive routing, and provider diversity
Build Electron Desktop App / Validate version (push) Failing after 35s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
Build Electron Desktop App / Publish to npm (push) Has been skipped
2026-03-30 12:58:45 -03:00
Diego Rodrigues de Sa e Souza 8a9c15c874 Merge pull request #819 from diegosouzapw/release/v3.3.4
Release v3.3.4
2026-03-30 11:26:17 -03:00
diegosouzapw 7a666526b7 chore(release): bump version to 3.3.4 2026-03-30 11:23:59 -03:00
diegosouzapw 3fc1cac015 docs(i18n): update CHANGELOG, README and sync multi-language FEATURES docs 2026-03-30 11:21:47 -03:00
Diego Rodrigues de Sa e Souza 04a0b07bf6 Merge pull request #793 from igormorais123/feat/provider-diversity-scoring
feat(sse): add provider diversity scoring via Shannon entropy
2026-03-30 11:07:03 -03:00
Diego Rodrigues de Sa e Souza 59e48ca91a Merge pull request #794 from igormorais123/feat/adaptive-volume-routing
feat(sse): add adaptive volume/complexity detector for routing strategy override
2026-03-30 11:07:00 -03:00
Diego Rodrigues de Sa e Souza 8ff562c5af Merge pull request #795 from igormorais123/feat/provider-expiration-tracking
feat(domain): add provider expiration tracking with proactive alerts
2026-03-30 11:06:56 -03:00
Diego Rodrigues de Sa e Souza b502a93728 Merge pull request #796 from igormorais123/feat/config-audit-trail
feat(domain): add configuration audit trail with diff detection and rollback
2026-03-30 11:06:53 -03:00
Diego Rodrigues de Sa e Souza b6afa6c2c7 Merge pull request #803 from igormorais123/feat/graceful-degradation-wrapper
feat(domain): add graceful degradation framework with multi-layer fallback
2026-03-30 11:06:50 -03:00
Diego Rodrigues de Sa e Souza 5887da0229 Merge pull request #805 from igormorais123/feat/fsm-workflow-orchestrator
feat(sse): add deterministic FSM orchestrator for multi-step workflows
2026-03-30 11:06:46 -03:00
Diego Rodrigues de Sa e Souza a7d833d96a Merge pull request #817 from diegosouzapw/feat/auto-disable-banned-accounts-setting
Feat/auto disable banned accounts setting
2026-03-30 11:06:42 -03:00
Diego Rodrigues de Sa e Souza db3753d611 Merge PR 811: fix UI fallbacks and Electron release workflow
fix: UI fallbacks and Electron release workflow
2026-03-30 11:04:02 -03:00
diegosouzapw f810b13bca fix: complete bugfixes for UI, OAuth fallbacks, cliRuntime Windows constraints and Codex non-streaming integration 2026-03-30 11:01:55 -03:00
diegosouzapw 5ad687c6d8 fix(ui/ci): use ProviderIcon for Provider header breadcrumbs and add permissions to electron-release.yml (#745, #761)
- Use ProviderIcon for internal .png paths solving SVG provider 404 images (#745).
- Add id-token: write and packages: write permissions to .github/workflows/electron-release.yml to fix permissions denied failure when calling the reusable workflow npm-publish.yml (#761).
- Fix tests and ESM resolution for autoUpdate.ts override logic.
2026-03-30 07:38:30 -03:00
Diego Rodrigues de Sa e Souza 6ad0910790 Merge pull request #810 from oyi77/main
feat(settings): add debug toggle and sidebar visibility toggle
2026-03-30 07:07:54 -03:00
Diego Rodrigues de Sa e Souza 4d8c0546cf Merge pull request #783 from rdself/coder/cloudflared-exit1-fix
Fix cloudflared quick tunnel startup in Docker
2026-03-30 07:07:39 -03:00
oyi77 35f96d4a40 feat(settings): add debug toggle and sidebar visibility toggle
feat(ui): replace hide-sidebar toggle with dynamic visibility toggle
2026-03-30 15:15:02 +07:00
igormorais123 ae96fb6f63 feat(sse): add deterministic FSM orchestrator for multi-step workflows
Risk-based phase skipping: high=all 9 phases, medium=skip planner, low=execute+test.
Veto authority, pause/resume, retry limits, full audit trail.

Closes #800, closes #802
2026-03-30 01:28:45 -03:00
igormorais123 67592d80aa feat(domain): add graceful degradation framework with multi-layer fallback
Add a standardized degradation pattern for services depending on external
systems. withDegradation() tries primary → fallback → safe default,
tracking status in a global registry for dashboard visibility.

Features:
- Async and sync variants
- Global registry with per-feature status tracking
- Degradation levels: full → reduced → minimal → default
- Summary and report APIs for dashboard integration
- Reason tracking for debugging

Example: Rate limiting degrades from Redis → in-memory → permissive
instead of crashing when Redis is unavailable.

Closes #799
2026-03-30 01:23:10 -03:00
igormorais123 94a5e43e5d feat(domain): add configuration audit trail with diff detection and rollback
Add configAudit module that records every change to provider connections,
combos, and routing policies with:

- Before/after state snapshots
- Structured diff (added, removed, changed keys)
- Source tracking (dashboard, API, sync, auto-healing)
- Filtered retrieval with pagination
- Rollback state extraction
- Configuration snapshot export for backup

Enables traceability and quick rollback when config changes cause issues.

Closes #791
2026-03-30 00:49:22 -03:00
igormorais123 26958f8f70 feat(domain): add provider expiration tracking with proactive alerts
Add providerExpiration module to track OAuth token, subscription, and
API credit expiration dates per provider connection. Provides:

- setExpiration() / getExpiration() for CRUD operations
- getExpiringSoon() for proactive alerts
- getExpirationSummary() for dashboard health display
- detectExpirationFromResponse() for auto-detection from HTTP headers
- Status classification: active → expiring_soon → expired

Prevents silent failures from expired credentials by alerting operators
before tokens/subscriptions expire.

Closes #790
2026-03-30 00:48:06 -03:00
igormorais123 a427d215e3 feat(sse): add adaptive volume/complexity detector for routing strategy override
Add volumeDetector module that analyzes request characteristics (batch
size, token count, tool usage, browser signals, complexity keywords)
and recommends routing strategy overrides.

Rules:
- Batch >= 50 items → round-robin with economy models
- Critical complexity (many tools, browser, deploy) → priority premium-first
- Browser/UI interaction → force premium priority
- Short requests (<200 tokens) → flag for economy tier

Closes #789
2026-03-30 00:46:55 -03:00
igormorais123 271cf37b8a feat(sse): add provider diversity scoring via Shannon entropy
Add a providerDiversity module that tracks provider usage distribution
using a rolling time window and calculates Shannon entropy normalized
to [0..1]. This enables the auto-combo scoring engine to factor in
provider diversity — boosting underrepresented providers to reduce
single-point-of-failure risk.

Key features:
- Rolling window with configurable size and TTL
- Shannon entropy calculation normalized to [0..1]
- Per-provider diversity boost for auto-combo integration
- Diversity report for dashboard display
- Full test coverage

Closes #788
2026-03-30 00:45:17 -03:00
R.D. 179c03e79d Isolate cloudflared runtime environment 2026-03-29 22:30:07 -04:00
Diego Rodrigues de Sa e Souza 0a1b68639b Merge pull request #782 from diegosouzapw/release/v3.3.3
chore(release): v3.3.3 — UI bugfixes and AutoUpdate repairs
2026-03-29 22:51:52 -03:00
diegosouzapw d69e7ec850 chore(release): v3.3.3 — Core UI bugfixes and AutoUpdate repairs 2026-03-29 21:18:07 -03:00
diegosouzapw d0c172830c feat(ui): add AutoDisableCard to Resilience settings (#765) 2026-03-29 15:57:19 -03:00
oyi77 d5bf0d1199 fix: address reviewer comments for auto-disable (use getCachedSettings, immediate disable on permanent bans) 2026-03-30 01:47:28 +07:00
oyi77 82dd4aa403 feat: auto-disable banned accounts setting with UI toggle
Add a configurable setting to automatically disable provider accounts
that return permanent/terminal errors (403 banned, ToS violation, etc.)

Changes:
- open-sse/services/accountFallback.ts: extend ACCOUNT_DEACTIVATED_SIGNALS
  with AG-specific ban messages ('verify your account', 'service disabled
  for violation')
- src/app/api/settings/auto-disable-accounts/route.ts: new GET/PUT endpoint
  for the setting (enabled bool + threshold int)
- src/shared/validation/schemas.ts: updateAutoDisableAccountsSchema
- src/sse/services/auth.ts: in markAccountUnavailable(), capture result.permanent
  from checkFallbackError() and — when autoDisableBannedAccounts is enabled and
  backoffLevel >= threshold — set isActive=false on the connection

Default: disabled (backward-compatible). Enable via Settings UI or PUT
/api/settings/auto-disable-accounts { "enabled": true, "threshold": 3 }

Fixes: antigravity accounts with 403/Verify-your-account errors being
retried indefinitely in the rotation pool.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
2026-03-29 23:24:27 +07:00
113 changed files with 3532 additions and 373 deletions
+14 -4
View File
@@ -19,11 +19,21 @@ This workflow fetches all open issues from the project's GitHub repository, clas
### 2. Fetch All Open Issues
// turbo
// turbo-all
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title,labels,body,comments,createdAt,author`
- Parse the JSON output to get a list of **all** open issues
- Sort by oldest first (FIFO)
**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below to guarantee **all** issues are fetched.
**Step 2a — Get Issue numbers only** (small output, never truncated):
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- This outputs one issue number per line. Count them and confirm total.
**Step 2b — Fetch full metadata for each Issue** (one call per issue):
- For each issue number from step 2a, run:
`gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author`
- You may batch these into parallel calls (up to 4 at a time).
- Sort by oldest first (FIFO).
### 3. Classify Each Issue
+22 -4
View File
@@ -18,17 +18,35 @@ This workflow fetches all open PRs from the project's GitHub repository, perform
### 2. Fetch Open Pull Requests
// turbo
// turbo-all
**⚠️ CRITICAL**: The JSON output of `gh pr list` can be truncated by the tool, silently hiding PRs. You MUST use the two-step approach below to guarantee **all** PRs are fetched.
**Step 2a — Get PR numbers only** (small output, never truncated):
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number --jq '.[].number'`
- This outputs one PR number per line. Count them and confirm total.
**Step 2b — Fetch full metadata for each PR** (one call per PR):
- For each PR number from step 2a, run:
`gh pr view <NUMBER> --repo <owner>/<repo> --json number,title,author,headRefName,body,createdAt,additions,deletions,files`
- You may batch these into parallel calls (up to 4 at a time).
**Step 2c — Fetch diffs for each PR** (one call per PR, saved to /tmp):
- For each PR number, run:
`gh pr diff <NUMBER> --repo <owner>/<repo> > /tmp/pr<NUMBER>.diff`
- Then read each diff file with `view_file`.
- Run: `gh pr list --repo <owner>/<repo> --state open --limit 500 --json number,title,author,headRefName,body,createdAt,additions,deletions,files`
- This fetches **all** open PRs without restriction. Get the diff for each with:
`gh pr diff <NUMBER> --repo <owner>/<repo>`
- For each open PR, collect:
- PR number, title, author, branch, number of commits, date
- PR description/body
- Files changed (diff)
- Existing review comments (from bots or humans)
**Verification**: Confirm the count of PRs analyzed matches the count from step 2a before proceeding.
### 3. Analyze Each PR — For each open PR, perform the following analysis:
#### 3a. Feature Assessment
+16 -16
View File
@@ -18,8 +18,8 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -36,8 +36,8 @@ jobs:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -55,8 +55,8 @@ jobs:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
@@ -74,8 +74,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
@@ -90,8 +90,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -109,8 +109,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -129,8 +129,8 @@ jobs:
INITIAL_PASSWORD: ci-test-password-for-integration
DATA_DIR: /tmp/omniroute-ci
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -145,8 +145,8 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
+7 -7
View File
@@ -25,24 +25,24 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
- name: Set up QEMU (for multi-arch builds)
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -61,7 +61,7 @@ jobs:
echo "Publishing Docker image: $IMAGE_NAME:$VERSION"
- name: Build and push multi-arch image
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
context: .
target: runner-base
@@ -83,7 +83,7 @@ jobs:
docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}"
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v5
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+9 -7
View File
@@ -13,6 +13,8 @@ on:
permissions:
contents: write
id-token: write
packages: write
jobs:
validate:
@@ -22,7 +24,7 @@ jobs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -70,16 +72,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Cache node_modules
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -146,7 +148,7 @@ jobs:
fi
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: electron-${{ matrix.platform }}
path: release-assets/
@@ -157,12 +159,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
+4 -4
View File
@@ -43,10 +43,10 @@ jobs:
environment: NPM_TOKEN
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
@@ -111,11 +111,11 @@ jobs:
run: |
VERSION="${{ steps.resolve.outputs.version }}"
TAG="${{ steps.resolve.outputs.tag }}"
echo "Configuring for GitHub Packages..."
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc
npm pkg set name="@diegosouzapw/omniroute"
if [ "$TAG" = "latest" ]; then
npm publish --registry=https://npm.pkg.github.com || echo "⚠️ Version ${VERSION} might already be published on GitHub."
else
+34
View File
@@ -2,6 +2,40 @@
## [Unreleased]
---
## [3.3.4] - 2026-03-30
### ✨ New Features
- **A2A Workflows:** Added deterministic FSM orchestrator for multi-step agent workflows.
- **Graceful Degradation:** Added a new multi-layer fallback framework to preserve core functionality during partial system outages.
- **Config Audit:** Added an audit trail with diff detection to track changes and enable configuration rollbacks.
- **Provider Health:** Added provider expiration tracking with proactive UI alerts for expiring API keys.
- **Adaptive Routing:** Added an adaptive volume and complexity detector to override routing strategies dynamically based on load.
- **Provider Diversity:** Implemented provider diversity scoring via Shannon entropy to improve load distribution.
- **Auto-Disable Bounds:** Added an Auto-Disable Banned Accounts setting toggle to the Resilience dashboard.
### 🐛 Bug Fixes
- **Codex & Claude Compatibility:** Fixed UI fallbacks, patched Codex non-streaming integration issues, and resolved CLI runtime detection on Windows.
- **Release Automation:** Expanded permissions required for the Electron App build in GitHub Actions.
- **Cloudflare Runtime:** Addressed correct runtime isolation exit codes for Cloudflared tunnel components.
### 🧪 Tests
- **Test Suite Updates:** Expanded test coverage for volume detectors, provider diversity, configuration audit, and FSM.
---
## [3.3.3] - 2026-03-29
### 🐛 Bug Fixes
- **CI/CD Reliability:** Patched GitHub Actions to stable dependency versions (`actions/checkout@v4`, `actions/upload-artifact@v4`) to mitigate unannounced builder environment deprecations.
- **Image Fallbacks:** Replaced arbitrary fallback chains in `ProviderIcon.tsx` with explicit asset validation to prevent UI loading `<Image>` components for files that don't exist, eliminating `404` errors in dashboard console logs (#745).
- **Admin Updater:** Dynamic source-installation detection for the dashboard Updater. Safely disables the `Update Now` button when OmniRoute is built locally rather than through npm, prompting for `git pull` (#743).
- **Update ERESOLVE Error:** Injected `package.json` overrides for `react`/`react-dom` and enabled `--legacy-peer-deps` within the internal automatic updater scripts to resolve breaking dependency tree conflicts with `@lobehub/ui`.
---
## [3.3.2] - 2026-03-29
+7
View File
@@ -1218,6 +1218,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1248,6 +1251,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -66,8 +66,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+7
View File
@@ -1222,6 +1222,9 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔀 **Model Aliases** | Built-in + custom model aliasing and migration safety |
| ⚡ **Background Degradation** | Route low-priority background tasks to cheaper models |
| 🧪 **Task-Aware Smart Routing** | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 **A2A Agent Workflows** | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 **Adaptive Routing** | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 **Provider Diversity** | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 **System Prompt Injection** | Global behavior controls applied consistently |
| 📄 **Responses API Compatibility** | Full `/v1/responses` support for Codex and advanced agentic workflows |
@@ -1252,6 +1255,10 @@ OmniRoute v2.0 is built as an operational platform, not just a relay proxy.
| 🔏 **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 |
| 📉 **Graceful Degradation** | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 **Config Audit Trail** | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ **Provider Health Sync** | Proactive token expiration monitoring triggering alerts before authorization failures |
| 🚪 **Auto-Disable Banned Accounts** | Operational circuit breaker sealing permanently blocked token accounts automatically |
| 🔑 **API Key Management + Scoping** | Secure key issuance/rotation and model/provider controls |
| 👁️ **Scoped API Key Reveal** 🆕 | Opt-in recovery of API keys via `ALLOW_API_KEY_REVEAL` |
| 🛡️ **Protected `/models`** | Optional auth gating and provider hiding for model catalog |
+2 -2
View File
@@ -68,8 +68,8 @@ Comprehensive settings panel with tabs:
- **Appearance** — Theme selector (dark/light/system), color theme presets and custom colors, health log visibility, sidebar item visibility controls
- **Security** — API endpoint protection, custom provider blocking, IP filtering, session info
- **Routing** — Model aliases, background task degradation
- **Resilience** — Rate limit persistence, circuit breaker tuning
- **Advanced** — Configuration overrides
- **Resilience** — Rate limit persistence, circuit breaker tuning, auto-disable banned accounts, provider expiration monitoring
- **Advanced** — Configuration overrides, configuration audit trail, fallback degradation mode
![Settings Dashboard](screenshots/06-settings.png)
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.3.2
version: 3.3.4
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,
+232 -164
View File
@@ -77,11 +77,13 @@ export function translateNonStreamingResponse(
sourceFormat: string,
toolNameMap?: Map<string, string> | null
): unknown {
// If already in source format (usually OpenAI), return as-is
if (targetFormat === sourceFormat || targetFormat === FORMATS.OPENAI) {
// If already in source format, return as-is
if (targetFormat === sourceFormat) {
return responseBody;
}
let intermediateOpenAI = responseBody;
// Handle OpenAI Responses API format
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
const responseRoot = toRecord(responseBody);
@@ -126,7 +128,7 @@ export function translateNonStreamingResponse(
? itemObj.arguments
: JSON.stringify(itemObj.arguments || {});
const rawName = toString(itemObj.name);
// Strip Claude OAuth proxy_ prefix using toolNameMap (mirrors tool_use fix for #605)
// Strip Claude OAuth proxy_ prefix using toolNameMap
const resolvedName = toolNameMap?.get(rawName) ?? rawName;
toolCalls.push({
id: callId,
@@ -149,7 +151,7 @@ export function translateNonStreamingResponse(
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
}
if (!message.content && !message.tool_calls) {
if (message.content === undefined) {
message.content = "";
}
@@ -212,11 +214,11 @@ export function translateNonStreamingResponse(
}
}
return result;
intermediateOpenAI = result;
}
// Handle Gemini/Antigravity format
if (
else if (
targetFormat === FORMATS.GEMINI ||
targetFormat === FORMATS.ANTIGRAVITY ||
targetFormat === FORMATS.GEMINI_CLI
@@ -224,183 +226,249 @@ export function translateNonStreamingResponse(
const root = toRecord(responseBody);
const response = toRecord(root.response ?? root);
const candidates = Array.isArray(response.candidates) ? response.candidates : [];
if (!candidates[0]) {
return responseBody; // Can't translate, return raw
if (candidates[0]) {
const candidate = toRecord(candidates[0]);
const content = toRecord(candidate.content);
const usage = toRecord(response.usageMetadata ?? root.usageMetadata);
let textContent = "";
const toolCalls: JsonRecord[] = [];
let reasoningContent = "";
if (Array.isArray(content.parts)) {
for (const part of content.parts) {
const partObj = toRecord(part);
if (partObj.thought === true && typeof partObj.text === "string") {
reasoningContent += partObj.text;
} else if (typeof partObj.text === "string") {
textContent += partObj.text;
}
if (partObj.functionCall) {
const fn = toRecord(partObj.functionCall);
toolCalls.push({
id: `call_${toString(fn.name, "unknown")}_${Date.now()}_${toolCalls.length}`,
type: "function",
function: {
name: toString(fn.name),
arguments: JSON.stringify(fn.args || {}),
},
});
}
}
}
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
if (reasoningContent) {
message.reasoning_content = reasoningContent;
}
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
}
if (!message.content && !message.tool_calls) {
message.content = "";
}
let finishReason = toString(candidate.finishReason, "stop").toLowerCase();
if (finishReason === "stop" && toolCalls.length > 0) {
finishReason = "tool_calls";
}
const createdMs = Date.parse(toString(response.createTime));
const created = Number.isFinite(createdMs)
? Math.floor(createdMs / 1000)
: Math.floor(Date.now() / 1000);
const result: JsonRecord = {
id: `chatcmpl-${toString(response.responseId, String(Date.now()))}`,
object: "chat.completion",
created,
model: toString(response.modelVersion, "gemini"),
choices: [
{
index: 0,
message,
finish_reason: finishReason,
},
],
};
if (Object.keys(usage).length > 0) {
result.usage = {
prompt_tokens:
toNumber(usage.promptTokenCount, 0) + toNumber(usage.thoughtsTokenCount, 0),
completion_tokens: toNumber(usage.candidatesTokenCount, 0),
total_tokens: toNumber(usage.totalTokenCount, 0),
};
if (toNumber(usage.thoughtsTokenCount, 0) > 0) {
(result.usage as JsonRecord).completion_tokens_details = {
reasoning_tokens: toNumber(usage.thoughtsTokenCount, 0),
};
}
}
intermediateOpenAI = result;
}
}
const candidate = toRecord(candidates[0]);
const content = toRecord(candidate.content);
const usage = toRecord(response.usageMetadata ?? root.usageMetadata);
// Handle Claude format
else if (targetFormat === FORMATS.CLAUDE) {
const root = toRecord(responseBody);
const contentBlocks = Array.isArray(root.content) ? root.content : [];
if (contentBlocks.length > 0) {
let textContent = "";
let thinkingContent = "";
const toolCalls: JsonRecord[] = [];
// Build message content
let textContent = "";
const toolCalls: JsonRecord[] = [];
let reasoningContent = "";
if (Array.isArray(content.parts)) {
for (const part of content.parts) {
const partObj = toRecord(part);
// Handle thinking/reasoning
if (partObj.thought === true && typeof partObj.text === "string") {
reasoningContent += partObj.text;
}
// Regular text
else if (typeof partObj.text === "string") {
textContent += partObj.text;
}
// Function calls
if (partObj.functionCall) {
const fn = toRecord(partObj.functionCall);
for (const block of contentBlocks) {
const blockObj = toRecord(block);
if (blockObj.type === "text") {
textContent += toString(blockObj.text);
} else if (blockObj.type === "thinking") {
thinkingContent += toString(blockObj.thinking);
} else if (blockObj.type === "tool_use") {
const rawName = toString(blockObj.name);
const strippedName = toolNameMap?.get(rawName) ?? rawName;
toolCalls.push({
id: `call_${toString(fn.name, "unknown")}_${Date.now()}_${toolCalls.length}`,
id: toString(blockObj.id, `call_${Date.now()}_${toolCalls.length}`),
type: "function",
function: {
name: toString(fn.name),
arguments: JSON.stringify(fn.args || {}),
name: strippedName,
arguments: JSON.stringify(blockObj.input || {}),
},
});
}
}
}
// Build OpenAI format message
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
if (reasoningContent) {
message.reasoning_content = reasoningContent;
}
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
}
// If no content at all, set content to empty string
if (!message.content && !message.tool_calls) {
message.content = "";
}
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
if (thinkingContent) {
message.reasoning_content = thinkingContent;
}
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
}
if (message.content === undefined) {
message.content = "";
}
// Determine finish reason
let finishReason = toString(candidate.finishReason, "stop").toLowerCase();
if (finishReason === "stop" && toolCalls.length > 0) {
finishReason = "tool_calls";
}
let finishReason = toString(root.stop_reason, "stop");
if (finishReason === "end_turn") finishReason = "stop";
if (finishReason === "tool_use") finishReason = "tool_calls";
const createdMs = Date.parse(toString(response.createTime));
const created = Number.isFinite(createdMs)
? Math.floor(createdMs / 1000)
: Math.floor(Date.now() / 1000);
const result: JsonRecord = {
id: `chatcmpl-${toString(response.responseId, String(Date.now()))}`,
object: "chat.completion",
created,
model: toString(response.modelVersion, "gemini"),
choices: [
{
index: 0,
message,
finish_reason: finishReason,
},
],
};
// Add usage if available (match streaming translator: add thoughtsTokenCount to prompt_tokens)
if (Object.keys(usage).length > 0) {
result.usage = {
prompt_tokens: toNumber(usage.promptTokenCount, 0) + toNumber(usage.thoughtsTokenCount, 0),
completion_tokens: toNumber(usage.candidatesTokenCount, 0),
total_tokens: toNumber(usage.totalTokenCount, 0),
const result: JsonRecord = {
id: `chatcmpl-${toString(root.id, String(Date.now()))}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: toString(root.model, "claude"),
choices: [
{
index: 0,
message,
finish_reason: finishReason,
},
],
};
if (toNumber(usage.thoughtsTokenCount, 0) > 0) {
(result.usage as JsonRecord).completion_tokens_details = {
reasoning_tokens: toNumber(usage.thoughtsTokenCount, 0),
const usage = toRecord(root.usage);
if (Object.keys(usage).length > 0) {
const promptTokens = toNumber(usage.input_tokens, 0);
const completionTokens = toNumber(usage.output_tokens, 0);
result.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
};
}
}
return result;
intermediateOpenAI = result;
}
}
// Handle Claude format
if (targetFormat === FORMATS.CLAUDE) {
const root = toRecord(responseBody);
const contentBlocks = Array.isArray(root.content) ? root.content : [];
if (contentBlocks.length === 0) {
return responseBody; // Can't translate, return raw
}
let textContent = "";
let thinkingContent = "";
const toolCalls: JsonRecord[] = [];
for (const block of contentBlocks) {
const blockObj = toRecord(block);
if (blockObj.type === "text") {
textContent += toString(blockObj.text);
} else if (blockObj.type === "thinking") {
thinkingContent += toString(blockObj.thinking);
} else if (blockObj.type === "tool_use") {
// Strip Claude OAuth tool name prefix (proxy_) using the map from request translation.
// Fallback to raw name if block wasn't prefixed (disableToolPrefix path).
const rawName = toString(blockObj.name);
const strippedName = toolNameMap?.get(rawName) ?? rawName;
toolCalls.push({
id: toString(blockObj.id, `call_${Date.now()}_${toolCalls.length}`),
type: "function",
function: {
name: strippedName,
arguments: JSON.stringify(blockObj.input || {}),
},
});
}
}
const message: JsonRecord = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
if (thinkingContent) {
message.reasoning_content = thinkingContent;
}
if (toolCalls.length > 0) {
message.tool_calls = toolCalls;
}
if (!message.content && !message.tool_calls) {
message.content = "";
}
let finishReason = toString(root.stop_reason, "stop");
if (finishReason === "end_turn") finishReason = "stop";
if (finishReason === "tool_use") finishReason = "tool_calls";
const result: JsonRecord = {
id: `chatcmpl-${toString(root.id, String(Date.now()))}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: toString(root.model, "claude"),
choices: [
{
index: 0,
message,
finish_reason: finishReason,
},
],
};
const usage = toRecord(root.usage);
if (Object.keys(usage).length > 0) {
const promptTokens = toNumber(usage.input_tokens, 0);
const completionTokens = toNumber(usage.output_tokens, 0);
result.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
};
}
return result;
// Phase 3: Translate from OpenAI back to Client Source format
if (sourceFormat === FORMATS.CLAUDE && sourceFormat !== targetFormat) {
return convertOpenAINonStreamingToClaude(toRecord(intermediateOpenAI));
}
// Unknown format, return as-is
return responseBody;
// Return intermediateOpenAI (which is either the raw response if unknown targetFormat, or an OpenAI compatible payload)
return intermediateOpenAI;
}
/**
* Helper to convert an OpenAI chat.completion JSON object to Claude format for non-streaming.
*/
function convertOpenAINonStreamingToClaude(openaiResponse: JsonRecord): JsonRecord {
const choice = Array.isArray(openaiResponse.choices) ? openaiResponse.choices[0] : null;
if (!choice) return openaiResponse; // If it doesn't look like OpenAI, return as-is
const choiceObj = toRecord(choice);
const messageObj = toRecord(choiceObj.message);
const content = [];
let hasTextOrReasoning = false;
if (messageObj.reasoning_content) {
hasTextOrReasoning = true;
content.push({
type: "thinking",
thinking: toString(messageObj.reasoning_content),
});
}
// Always include text if it exists (even empty string), or if there are no tool calls and no reasoning
const hasToolCalls = Array.isArray(messageObj.tool_calls) && messageObj.tool_calls.length > 0;
if (messageObj.content !== undefined && messageObj.content !== null) {
hasTextOrReasoning = true;
content.push({
type: "text",
text: toString(messageObj.content),
});
} else if (!hasTextOrReasoning) {
// Claude format expects a text block even before tool calls (or if empty)
content.push({
type: "text",
text: "",
});
}
if (Array.isArray(messageObj.tool_calls)) {
for (const tool of messageObj.tool_calls) {
const toolObj = toRecord(tool);
const fn = toRecord(toolObj.function);
content.push({
type: "tool_use",
id: toString(toolObj.id, `call_${Date.now()}`),
name: toString(fn.name),
input:
typeof fn.arguments === "string" ? JSON.parse(fn.arguments || "{}") : fn.arguments || {},
});
}
}
let stopReason = toString(choiceObj.finish_reason, "end_turn");
if (stopReason === "stop") stopReason = "end_turn";
if (stopReason === "tool_calls") stopReason = "tool_use";
const usageSrc = toRecord(openaiResponse.usage);
const claudeResponse: JsonRecord = {
id: toString(openaiResponse.id, `msg_${Date.now()}`),
type: "message",
role: "assistant",
model: toString(openaiResponse.model, "claude"),
content,
stop_reason: stopReason,
stop_sequence: null,
usage: {
input_tokens: toNumber(usageSrc.prompt_tokens, 0),
output_tokens: toNumber(usageSrc.completion_tokens, 0),
},
};
return claudeResponse;
}
@@ -0,0 +1,118 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { detectVolumeSignals, recommendStrategyOverride } from "../volumeDetector";
describe("volumeDetector", async () => {
describe("detectVolumeSignals", async () => {
it("detects simple single-message request", async () => {
const body = {
messages: [{ role: "user", content: "Hello" }],
};
const signals = detectVolumeSignals(body);
assert.equal(signals.batchSize, 1);
assert.ok(signals.estimatedTokens < 100);
assert.equal(signals.toolCount, 0);
assert.equal(signals.hasBrowser, false);
assert.equal(signals.complexity, "trivial");
});
it("detects tool-heavy request as high complexity", async () => {
const body = {
messages: [{ role: "user", content: "Deploy the app to production" }],
tools: [
{ type: "function", function: { name: "run_command" } },
{ type: "function", function: { name: "read_file" } },
{ type: "function", function: { name: "write_file" } },
{ type: "function", function: { name: "browser_action" } },
],
};
const signals = detectVolumeSignals(body);
assert.equal(signals.toolCount, 4);
assert.equal(signals.complexity, "critical");
});
it("detects browser keywords", async () => {
const body = {
messages: [{ role: "user", content: "Navigate to the page and take a screenshot" }],
};
const signals = detectVolumeSignals(body);
assert.equal(signals.hasBrowser, true);
});
it("detects batch from multi-part content", async () => {
const parts = Array.from({ length: 20 }, (_, i) => ({
type: "text",
text: `Item ${i}`,
}));
const body = {
messages: [{ role: "user", content: parts }],
};
const signals = detectVolumeSignals(body);
assert.equal(signals.batchSize, 20);
});
it("detects security keywords as high complexity", async () => {
const body = {
messages: [{ role: "user", content: "Refactor the authentication module for production" }],
};
const signals = detectVolumeSignals(body);
assert.ok(
signals.complexity === "critical" || signals.complexity === "high",
`expected critical or high, got ${signals.complexity}`
);
});
});
describe("recommendStrategyOverride", async () => {
it("recommends round-robin for large batches", async () => {
const signals = detectVolumeSignals({ input: Array(60).fill("item") });
const override = await recommendStrategyOverride(signals, "priority");
assert.equal(override.shouldOverride, true);
assert.equal(override.strategy, "round-robin");
assert.equal(override.preferEconomy, true);
});
it("recommends premium-first for browser tasks", async () => {
const signals = {
batchSize: 1,
estimatedTokens: 500,
toolCount: 2,
hasBrowser: true,
hasImages: false,
complexity: "high" as const,
};
const override = await recommendStrategyOverride(signals, "round-robin");
assert.equal(override.shouldOverride, true);
assert.equal(override.strategy, "priority");
assert.equal(override.forcePremium, true);
});
it("flags economy for tiny requests without changing strategy", async () => {
const signals = {
batchSize: 1,
estimatedTokens: 100,
toolCount: 0,
hasBrowser: false,
hasImages: false,
complexity: "trivial" as const,
};
const override = await recommendStrategyOverride(signals, "priority");
assert.equal(override.shouldOverride, false);
assert.equal(override.preferEconomy, true);
});
it("no override for normal medium requests", async () => {
const signals = {
batchSize: 1,
estimatedTokens: 1000,
toolCount: 0,
hasBrowser: false,
hasImages: false,
complexity: "low" as const,
};
const override = await recommendStrategyOverride(signals, "priority");
assert.equal(override.shouldOverride, false);
assert.equal(override.preferEconomy, false);
});
});
});
@@ -0,0 +1,125 @@
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
recordProviderUsage,
calculateDiversityScore,
getProviderDiversityBoost,
getDiversityReport,
resetDiversity,
configureDiversity,
} from "../providerDiversity";
describe("providerDiversity", () => {
beforeEach(() => {
resetDiversity();
});
describe("calculateDiversityScore", () => {
it("returns 1.0 when no data is recorded", () => {
assert.equal(calculateDiversityScore(), 1.0);
});
it("returns 0.0 when all requests go to one provider", () => {
for (let i = 0; i < 20; i++) {
recordProviderUsage("claude");
}
assert.equal(calculateDiversityScore(), 0.0);
});
it("returns 1.0 for perfectly even distribution across 2 providers", () => {
for (let i = 0; i < 10; i++) {
recordProviderUsage("claude");
recordProviderUsage("openai");
}
assert.equal(calculateDiversityScore(), 1.0);
});
it("returns value between 0 and 1 for uneven distribution", () => {
for (let i = 0; i < 15; i++) recordProviderUsage("claude");
for (let i = 0; i < 5; i++) recordProviderUsage("openai");
const score = calculateDiversityScore();
assert.ok(score > 0, "should be > 0 (not single provider)");
assert.ok(score < 1, "should be < 1 (not perfectly even)");
});
it("higher entropy with more providers", () => {
// 2 providers
resetDiversity();
for (let i = 0; i < 10; i++) {
recordProviderUsage("claude");
recordProviderUsage("openai");
}
const score2 = calculateDiversityScore();
// 4 providers (same total requests)
resetDiversity();
for (let i = 0; i < 5; i++) {
recordProviderUsage("claude");
recordProviderUsage("openai");
recordProviderUsage("google");
recordProviderUsage("together");
}
const score4 = calculateDiversityScore();
// Both should be 1.0 (perfectly distributed within their pool)
assert.equal(score2, 1.0);
assert.equal(score4, 1.0);
});
});
describe("getProviderDiversityBoost", () => {
it("returns 0.5 when no data is recorded", () => {
assert.equal(getProviderDiversityBoost("claude"), 0.5);
});
it("returns low boost for heavily used provider", () => {
for (let i = 0; i < 18; i++) recordProviderUsage("claude");
for (let i = 0; i < 2; i++) recordProviderUsage("openai");
const claudeBoost = getProviderDiversityBoost("claude");
const openaiBoost = getProviderDiversityBoost("openai");
assert.ok(claudeBoost < openaiBoost, "heavily used provider should have lower boost");
assert.ok(claudeBoost < 0.2, "90% used provider should have very low boost");
assert.ok(openaiBoost > 0.8, "10% used provider should have high boost");
});
it("returns 1.0 for never-used provider", () => {
for (let i = 0; i < 10; i++) recordProviderUsage("claude");
const boost = getProviderDiversityBoost("google");
assert.equal(boost, 1.0);
});
});
describe("getDiversityReport", () => {
it("returns structured report", () => {
recordProviderUsage("claude");
recordProviderUsage("claude");
recordProviderUsage("openai");
const report = getDiversityReport();
assert.equal(report.totalRequests, 3);
assert.ok(report.score > 0);
assert.ok(report.score < 1);
assert.equal(report.providers["claude"].count, 2);
assert.equal(report.providers["openai"].count, 1);
assert.ok(Math.abs(report.providers["claude"].share - 2 / 3) < 0.01);
});
});
describe("window management", () => {
it("respects windowSize limit", () => {
configureDiversity({ windowSize: 10, ttlMs: 3_600_000 });
for (let i = 0; i < 20; i++) {
recordProviderUsage("claude");
}
const report = getDiversityReport();
assert.ok(report.totalRequests <= 10, "should not exceed window size");
});
});
});
@@ -0,0 +1,170 @@
/**
* Provider Diversity Tracking via Shannon Entropy
*
* Measures and tracks how evenly distributed requests are across providers.
* A system routing 90% of traffic to one provider has a catastrophic single
* point of failure. This module provides a diversity score [0..1] that can
* be used as a scoring factor in auto-combo selection.
*
* Shannon entropy normalized to [0..1]:
* - 0.0 = all requests go to one provider (maximum risk)
* - 1.0 = perfectly even distribution (minimum risk)
*
* @see https://en.wikipedia.org/wiki/Entropy_(information_theory)
*/
/** Rolling window entry for provider usage tracking */
interface UsageEntry {
provider: string;
timestamp: number;
}
/** Configuration for the diversity tracker */
export interface DiversityConfig {
/** Maximum entries in the rolling window (default: 200) */
windowSize: number;
/** Time-to-live in ms for entries — older entries are pruned (default: 1 hour) */
ttlMs: number;
}
const DEFAULT_CONFIG: DiversityConfig = {
windowSize: 200,
ttlMs: 3_600_000, // 1 hour
};
/** In-memory rolling window of recent provider usage */
let usageWindow: UsageEntry[] = [];
let config: DiversityConfig = { ...DEFAULT_CONFIG };
/**
* Configure the diversity tracker.
*/
export function configureDiversity(userConfig: Partial<DiversityConfig>): void {
config = { ...DEFAULT_CONFIG, ...userConfig };
}
/**
* Record that a provider was used for a request.
* Call this after a successful request completes.
*/
export function recordProviderUsage(provider: string): void {
const now = Date.now();
usageWindow.push({ provider, timestamp: now });
// Prune by window size
if (usageWindow.length > config.windowSize) {
usageWindow = usageWindow.slice(-config.windowSize);
}
// Prune by TTL
const cutoff = now - config.ttlMs;
usageWindow = usageWindow.filter((e) => e.timestamp >= cutoff);
}
/**
* Calculate Shannon entropy normalized to [0..1] for the current usage window.
*
* @returns Normalized entropy where 0 = single provider, 1 = perfect distribution
*/
export function calculateDiversityScore(): number {
if (usageWindow.length === 0) return 1.0; // No data = assume diverse
const now = Date.now();
const cutoff = now - config.ttlMs;
const recent = usageWindow.filter((e) => e.timestamp >= cutoff);
if (recent.length === 0) return 1.0;
// Count occurrences per provider
const counts = new Map<string, number>();
for (const entry of recent) {
counts.set(entry.provider, (counts.get(entry.provider) || 0) + 1);
}
const total = recent.length;
const nUnique = counts.size;
if (nUnique <= 1) return 0.0;
// Shannon entropy
let entropy = 0;
for (const count of counts.values()) {
const p = count / total;
entropy -= p * Math.log2(p);
}
// Normalize by maximum possible entropy
const maxEntropy = Math.log2(nUnique);
return maxEntropy > 0 ? entropy / maxEntropy : 0;
}
/**
* Get the diversity score for a specific provider.
* Returns a boost value [0..1] where underrepresented providers score higher.
* This can be used as a per-candidate factor in auto-combo scoring.
*
* @param provider - The provider to score
* @returns Diversity boost where 1.0 = never used (maximum boost), 0.0 = most used
*/
export function getProviderDiversityBoost(provider: string): number {
if (usageWindow.length === 0) return 0.5; // No data = neutral
const now = Date.now();
const cutoff = now - config.ttlMs;
const recent = usageWindow.filter((e) => e.timestamp >= cutoff);
if (recent.length === 0) return 0.5;
const total = recent.length;
const providerCount = recent.filter((e) => e.provider === provider).length;
// Inverse usage share: providers used less get higher boost
const usageShare = providerCount / total;
return Math.max(0, 1 - usageShare);
}
/**
* Get a summary of the current provider distribution.
* Useful for dashboard display and debugging.
*/
export function getDiversityReport(): {
score: number;
totalRequests: number;
providers: Record<string, { count: number; share: number }>;
windowSize: number;
ttlMs: number;
} {
const now = Date.now();
const cutoff = now - config.ttlMs;
const recent = usageWindow.filter((e) => e.timestamp >= cutoff);
const counts = new Map<string, number>();
for (const entry of recent) {
counts.set(entry.provider, (counts.get(entry.provider) || 0) + 1);
}
const providers: Record<string, { count: number; share: number }> = {};
for (const [provider, count] of counts) {
providers[provider] = {
count,
share: recent.length > 0 ? count / recent.length : 0,
};
}
return {
score: calculateDiversityScore(),
totalRequests: recent.length,
providers,
windowSize: config.windowSize,
ttlMs: config.ttlMs,
};
}
/**
* Reset the diversity tracker. Useful for testing.
*/
export function resetDiversity(): void {
usageWindow = [];
config = { ...DEFAULT_CONFIG };
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Volume & Complexity Detector for Adaptive Routing
*
* Detects request characteristics (batch size, token estimate, tool count,
* complexity signals) and recommends routing strategy overrides.
*
* When a request clearly belongs to a different routing profile than the
* combo's default strategy, this module suggests an override. For example:
* - Batch of 500 items round-robin (prevent throttling)
* - 3 tools + browser priority with premium-first (needs best model)
* - 50 tokens keep strategy but flag for economy tier
*/
/** Signals extracted from a request for routing decisions */
export interface VolumeSignals {
/** Number of items in a batch (1 for single requests) */
batchSize: number;
/** Estimated total tokens (input + output) */
estimatedTokens: number;
/** Number of tools defined in the request */
toolCount: number;
/** Whether the request involves browser/UI interaction */
hasBrowser: boolean;
/** Whether the request includes image/screenshot content */
hasImages: boolean;
/** Rough complexity level derived from signals */
complexity: "trivial" | "low" | "medium" | "high" | "critical";
}
/** Strategy override recommendation */
export interface StrategyOverride {
/** Whether an override is recommended */
shouldOverride: boolean;
/** Recommended strategy (null if no override) */
strategy: "priority" | "round-robin" | "cost-optimized" | "weighted" | null;
/** Whether to prefer economy models */
preferEconomy: boolean;
/** Whether to force premium models first */
forcePremium: boolean;
/** Reason for the override (for logging) */
reason: string;
}
// Tool-related keywords that signal browser/UI interaction
const BROWSER_KEYWORDS = [
"browser",
"playwright",
"puppeteer",
"screenshot",
"navigate",
"click",
"form",
"page",
"tab",
"window",
"computer_use",
"computer-use",
];
// Keywords that signal high complexity
const HIGH_COMPLEXITY_KEYWORDS = [
"deploy",
"migration",
"security",
"auth",
"database",
"refactor",
"production",
"incident",
];
/**
* Detect volume and complexity signals from a chat request body.
*
* @param body - The raw request body (OpenAI or Claude format)
* @returns Extracted signals
*/
export function detectVolumeSignals(body: Record<string, unknown>): VolumeSignals {
const messages = (body.messages || body.input || []) as unknown[];
const tools = (body.tools || []) as unknown[];
const toolCount = tools.length;
// Estimate batch size from array structures
let batchSize = 1;
if (Array.isArray(body.input) && body.input.length > 1) {
batchSize = body.input.length;
} else if (Array.isArray(messages)) {
// Check if the last user message contains multiple items (common batch pattern)
const lastMsg = messages[messages.length - 1] as Record<string, unknown> | undefined;
if (lastMsg && Array.isArray(lastMsg.content)) {
const contentParts = lastMsg.content as unknown[];
batchSize = Math.max(1, contentParts.length);
}
}
// Estimate tokens from serialized message size
const serialized = JSON.stringify(messages);
const estimatedTokens = Math.ceil(serialized.length / 4); // rough: 4 chars ≈ 1 token
// Detect browser/UI signals
const lowerSerialized = serialized.toLowerCase();
const hasBrowser = BROWSER_KEYWORDS.some((kw) => lowerSerialized.includes(kw));
// Detect image content
const hasImages =
lowerSerialized.includes("image_url") ||
lowerSerialized.includes("image/") ||
lowerSerialized.includes("base64") ||
lowerSerialized.includes("screenshot");
// Determine complexity
const hasHighKeywords = HIGH_COMPLEXITY_KEYWORDS.some((kw) => lowerSerialized.includes(kw));
let complexity: VolumeSignals["complexity"];
if (toolCount > 3 || (hasBrowser && toolCount > 1) || hasHighKeywords) {
complexity = "critical";
} else if (toolCount > 1 || hasBrowser || hasImages || estimatedTokens > 10000) {
complexity = "high";
} else if (toolCount === 1 || estimatedTokens > 2000) {
complexity = "medium";
} else if (estimatedTokens > 500) {
complexity = "low";
} else {
complexity = "trivial";
}
return {
batchSize,
estimatedTokens,
toolCount,
hasBrowser,
hasImages,
complexity,
};
}
/**
* Recommend a routing strategy override based on detected volume signals.
*
* @param signals - Volume signals from detectVolumeSignals()
* @param currentStrategy - The combo's configured strategy
* @returns Override recommendation
*/
export async function recommendStrategyOverride(
signals: VolumeSignals,
currentStrategy: string
): Promise<StrategyOverride> {
const noOverride: StrategyOverride = {
shouldOverride: false,
strategy: null,
preferEconomy: false,
forcePremium: false,
reason: "no override needed",
};
// Check if adaptive routing is enabled globally
try {
const { getSettings } = await import("@/lib/localDb");
const settings = await getSettings();
if (!settings.adaptiveVolumeRouting) {
return noOverride;
}
} catch (error) {
console.error("Failed to check adaptiveVolumeRouting setting:", error);
return noOverride;
}
// Rule 1: Large batch → round-robin to distribute load
if (signals.batchSize >= 50) {
return {
shouldOverride: true,
strategy: "round-robin",
preferEconomy: true,
forcePremium: false,
reason: `batch size ${signals.batchSize} >= 50: distribute load via round-robin with economy models`,
};
}
// Rule 2: Medium batch with low complexity → cost-optimized
if (signals.batchSize >= 10 && signals.complexity === "low") {
return {
shouldOverride: currentStrategy !== "cost-optimized",
strategy: "cost-optimized",
preferEconomy: true,
forcePremium: false,
reason: `batch size ${signals.batchSize} with low complexity: use cost-optimized routing`,
};
}
// Rule 3: Critical complexity → force priority with premium
if (signals.complexity === "critical") {
return {
shouldOverride: true,
strategy: "priority",
preferEconomy: false,
forcePremium: true,
reason: `critical complexity (tools=${signals.toolCount}, browser=${signals.hasBrowser}): force premium-first priority`,
};
}
// Rule 4: Browser/UI interaction → force priority with premium
if (signals.hasBrowser) {
return {
shouldOverride: currentStrategy !== "priority",
strategy: "priority",
preferEconomy: false,
forcePremium: true,
reason: "browser/UI interaction detected: force premium-first priority",
};
}
// Rule 5: Very short request → flag for economy (but don't change strategy)
if (signals.estimatedTokens <= 200) {
return {
shouldOverride: false,
strategy: null,
preferEconomy: true,
forcePremium: false,
reason: `short request (${signals.estimatedTokens} tokens): prefer economy tier`,
};
}
return noOverride;
}
+103
View File
@@ -0,0 +1,103 @@
/**
* Deterministic FSM for Multi-Step Workflows
*
* Orchestrates plan -> review -> execute -> verify using rules, not LLM decisions.
* Risk-based phase skipping: high=all phases, medium=skip planner, low=execute+test only.
*/
export type Phase = "classify"|"plan"|"plan_review"|"execute"|"code_review"|"quality_review"|"security"|"test"|"output_review"|"done"|"failed"|"paused";
export type RiskLevel = "low"|"medium"|"high";
export type Verdict = "approve"|"approve_with_notes"|"request_changes"|"reject"|"block";
export interface PhaseRecord {
phase: Phase; enteredAt: string; exitedAt: string|null;
verdict: Verdict|null; provider: string|null; model: string|null;
retryCount: number; notes: string|null;
}
export interface WorkflowContext {
id: string; currentPhase: Phase; risk: RiskLevel;
lastVerdict: Verdict|null; retries: Record<string,number>;
maxRetries: number; testsPass: boolean;
history: PhaseRecord[]; createdAt: string;
metadata: Record<string,unknown>;
}
interface Transition { from: Phase; to: Phase; condition: (ctx: WorkflowContext) => boolean; description: string; }
const HIGH_KEYWORDS = ["schema","migration","deploy","delete","drop","env","database","refactor","security","auth","production","secrets","credentials","permission"];
const MED_KEYWORDS = ["endpoint","feature","service","model","api","integration","webhook","middleware","route"];
export function classifyRisk(desc: string): RiskLevel {
const l = desc.toLowerCase();
if (HIGH_KEYWORDS.some(k => l.includes(k))) return "high";
if (MED_KEYWORDS.some(k => l.includes(k))) return "medium";
return "low";
}
const PHASE_ORDER: Phase[] = ["classify","plan","plan_review","execute","code_review","quality_review","security","test","output_review"];
const T: Transition[] = [
{from:"classify",to:"plan",condition:c=>c.risk==="high",description:"High risk -> full planning"},
{from:"classify",to:"execute",condition:c=>c.risk==="medium",description:"Medium risk -> skip planner"},
{from:"classify",to:"execute",condition:c=>c.risk==="low",description:"Low risk -> direct execute"},
{from:"plan",to:"plan_review",condition:()=>true,description:"Plan -> review"},
{from:"plan_review",to:"execute",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Plan approved -> execute"},
{from:"plan_review",to:"plan",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0)<c.maxRetries,description:"Plan rejected -> retry"},
{from:"plan_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0)>=c.maxRetries,description:"Plan rejected max retries"},
{from:"execute",to:"code_review",condition:c=>c.risk!=="low",description:"Non-low -> code review"},
{from:"execute",to:"test",condition:c=>c.risk==="low",description:"Low -> skip reviews"},
{from:"code_review",to:"quality_review",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Code approved -> quality"},
{from:"code_review",to:"execute",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0)<c.maxRetries,description:"Code rejected -> re-execute"},
{from:"code_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0)>=c.maxRetries,description:"Code rejected max retries"},
{from:"quality_review",to:"security",condition:c=>c.risk==="high",description:"High -> security audit"},
{from:"quality_review",to:"test",condition:c=>c.risk!=="high",description:"Non-high -> skip security"},
{from:"security",to:"failed",condition:c=>c.lastVerdict==="block",description:"Security BLOCK -> failed"},
{from:"security",to:"test",condition:c=>c.lastVerdict!=="block",description:"Security passed -> test"},
{from:"test",to:"output_review",condition:c=>c.testsPass,description:"Tests pass -> output review"},
{from:"test",to:"execute",condition:c=>!c.testsPass&&(c.retries["execute"]??0)<c.maxRetries,description:"Tests fail -> re-execute"},
{from:"test",to:"failed",condition:c=>!c.testsPass&&(c.retries["execute"]??0)>=c.maxRetries,description:"Tests fail max retries"},
{from:"output_review",to:"done",condition:()=>true,description:"Output reviewed -> done"},
];
export function createWorkflow(id: string, description: string, opts?: {maxRetries?: number; metadata?: Record<string,unknown>}): WorkflowContext {
const risk = classifyRisk(description);
return {id, currentPhase:"classify", risk, lastVerdict:null, retries:{}, maxRetries:opts?.maxRetries??3, testsPass:false,
history:[{phase:"classify",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:`Risk: ${risk}`}],
createdAt:new Date().toISOString(), metadata:opts?.metadata??{}};
}
export function advance(ctx: WorkflowContext, result?: {verdict?:Verdict;testsPass?:boolean;provider?:string;model?:string;notes?:string}): Phase|null {
if(result?.verdict!=null) ctx.lastVerdict=result.verdict;
if(result?.testsPass!=null) ctx.testsPass=result.testsPass;
const cur=ctx.history[ctx.history.length-1];
if(cur){cur.exitedAt=new Date().toISOString();cur.verdict=result?.verdict??null;cur.provider=result?.provider??null;cur.model=result?.model??null;cur.notes=result?.notes??null;}
for(const t of T){
if(t.from===ctx.currentPhase&&t.condition(ctx)){
const fi=PHASE_ORDER.indexOf(t.from),ti=PHASE_ORDER.indexOf(t.to);
if(ti>=0&&fi>=0&&ti<=fi) ctx.retries[t.to]=(ctx.retries[t.to]??0)+1;
ctx.currentPhase=t.to;
ctx.history.push({phase:t.to,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[t.to]??0,notes:t.description});
return t.to;
}
}
return null;
}
export function pause(ctx: WorkflowContext, reason: string): void {
ctx.currentPhase="paused";
ctx.history.push({phase:"paused",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:reason});
}
export function resume(ctx: WorkflowContext, phase: Phase): void {
const p=ctx.history[ctx.history.length-1];if(p)p.exitedAt=new Date().toISOString();
ctx.currentPhase=phase;
ctx.history.push({phase,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[phase]??0,notes:"Resumed"});
}
export function isTerminated(ctx: WorkflowContext): boolean { return ctx.currentPhase==="done"||ctx.currentPhase==="failed"; }
export function getPhaseSequence(ctx: WorkflowContext): Phase[] { return ctx.history.map(r=>r.phase); }
export function getLLMCallCount(ctx: WorkflowContext): number {
const sys: Phase[]=["classify","paused","done","failed"];
return ctx.history.filter(r=>!sys.includes(r.phase)&&r.exitedAt!=null).length;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.4",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.3.2",
"version": "3.3.4",
"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": {
@@ -162,6 +162,8 @@
},
"overrides": {
"dompurify": "^3.3.2",
"path-to-regexp": "^8.4.0"
"path-to-regexp": "^8.4.0",
"react": "$react",
"react-dom": "$react-dom"
}
}
@@ -546,18 +546,22 @@ export default function HomePageClient({ machineId }) {
<div>
<p className="font-semibold text-sm">Update Available: v{versionInfo.latest}</p>
<p className="text-xs opacity-80 mt-0.5">
{t("updateAvailableDesc") ||
`You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.`}
{versionInfo.autoUpdateSupported
? t("updateAvailableDesc") ||
`You are currently using v${versionInfo.current}. Update to access the latest features and bug fixes.`
: versionInfo.autoUpdateError ||
"Manual update required for this installation type."}
</p>
</div>
</div>
<Button
size="sm"
onClick={handleUpdate}
disabled={updating}
onClick={versionInfo.autoUpdateSupported ? handleUpdate : undefined}
disabled={updating || !versionInfo.autoUpdateSupported}
className="shrink-0 ml-4 font-semibold"
title={versionInfo.autoUpdateError || ""}
>
{t("updateNow") || "Update Now"}
{versionInfo.autoUpdateSupported ? t("updateNow") || "Update Now" : "Manual Update"}
</Button>
</div>
)}
+67 -32
View File
@@ -433,43 +433,78 @@ export default function A2ADashboardPage() {
<th className="text-left py-2 pr-2">{t("tableTask")}</th>
<th className="text-left py-2 pr-2">{t("tableSkill")}</th>
<th className="text-left py-2 pr-2">{t("tableState")}</th>
<th className="text-left py-2 pr-2">{t("tablePhase") || "FSM Status"}</th>
<th className="text-left py-2 pr-2">{t("tableUpdated")}</th>
<th className="text-left py-2">{t("tableActions")}</th>
</tr>
</thead>
<tbody>
{tasksData.tasks.map((task) => (
<tr key={task.id} className="border-b border-border/40">
<td className="py-2 pr-2 font-mono text-xs">{task.id}</td>
<td className="py-2 pr-2">{task.skill}</td>
<td className="py-2 pr-2">
<span className={`text-xs px-2 py-1 rounded-full ${stateClass(task.state)}`}>
{t(`state.${task.state}`)}
</span>
</td>
<td className="py-2 pr-2 text-xs">
{new Date(task.updatedAt).toLocaleString()}
</td>
<td className="py-2 flex gap-2">
<Button size="sm" variant="secondary" onClick={() => handleLoadTask(task.id)}>
{t("view")}
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => handleCancelTask(task.id)}
disabled={
task.state === "completed" ||
task.state === "failed" ||
task.state === "cancelled" ||
actionBusy === "cancel"
}
>
{t("cancel")}
</Button>
</td>
</tr>
))}
{tasksData.tasks.map((task) => {
const fsmPhase =
task.metadata?.fsmPhase || (task.metadata?.workflowFSM as any)?.currentPhase;
let fsmBadgeColor = "bg-gray-500/15 text-gray-500";
if (fsmPhase === "plan" || fsmPhase === "plan_review")
fsmBadgeColor = "bg-purple-500/15 text-purple-500";
else if (fsmPhase === "execute") fsmBadgeColor = "bg-blue-500/15 text-blue-500";
else if (
["code_review", "quality_review", "security", "test", "output_review"].includes(
fsmPhase
)
)
fsmBadgeColor = "bg-amber-500/15 text-amber-500";
else if (fsmPhase === "done") fsmBadgeColor = "bg-green-500/15 text-green-500";
else if (fsmPhase === "failed") fsmBadgeColor = "bg-red-500/15 text-red-500";
return (
<tr key={task.id} className="border-b border-border/40">
<td className="py-2 pr-2 font-mono text-xs">{task.id}</td>
<td className="py-2 pr-2">{task.skill}</td>
<td className="py-2 pr-2">
<span
className={`text-xs px-2 py-1 rounded-full ${stateClass(task.state)}`}
>
{t(`state.${task.state}`)}
</span>
</td>
<td className="py-2 pr-2">
{fsmPhase ? (
<span
className={`text-xs px-2 py-1 rounded border border-current/20 font-medium ${fsmBadgeColor}`}
>
{fsmPhase}
</span>
) : (
<span className="text-xs text-text-muted"></span>
)}
</td>
<td className="py-2 pr-2 text-xs">
{new Date(task.updatedAt).toLocaleString()}
</td>
<td className="py-2 flex gap-2">
<Button
size="sm"
variant="secondary"
onClick={() => handleLoadTask(task.id)}
>
{t("view")}
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => handleCancelTask(task.id)}
disabled={
task.state === "completed" ||
task.state === "failed" ||
task.state === "cancelled" ||
actionBusy === "cancel"
}
>
{t("cancel")}
</Button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
@@ -0,0 +1,272 @@
"use client";
import { useState, useEffect } from "react";
import { useTranslations } from "next-intl";
interface ConfigDiff {
added: string[];
removed: string[];
changed: Array<{ key: string; from: any; to: any }>;
isEmpty: boolean;
}
interface AuditEntry {
id: string;
timestamp: string;
action: string;
target: string;
targetId: string;
targetName: string;
source: string;
before: any;
after: any;
diff: ConfigDiff;
note: string | null;
}
export default function ConfigAuditViewer() {
const t = useTranslations("logs");
const [entries, setEntries] = useState<AuditEntry[]>([]);
const [loading, setLoading] = useState(true);
const [selectedEntry, setSelectedEntry] = useState<AuditEntry | null>(null);
useEffect(() => {
fetchLogs();
}, []);
const fetchLogs = async () => {
setLoading(true);
try {
const res = await fetch("/api/audit");
const data = await res.json();
setEntries(data.entries || []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const getActionColor = (action: string) => {
switch (action) {
case "create":
return "text-green-400 bg-green-400/10 border-green-500/20";
case "update":
return "text-blue-400 bg-blue-400/10 border-blue-500/20";
case "delete":
return "text-red-400 bg-red-400/10 border-red-500/20";
default:
return "text-gray-400 bg-gray-400/10 border-gray-500/20";
}
};
if (loading) {
return (
<div className="flex justify-center p-8">
<div className="w-6 h-6 border-2 border-[var(--accent)] border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
if (entries.length === 0) {
return (
<div className="flex flex-col items-center justify-center p-12 text-[var(--text-muted,#666)]">
<svg
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="w-12 h-12 mb-4 opacity-50"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<p>No Configuration Audit Logs found.</p>
</div>
);
}
return (
<div className="w-full">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-[var(--border,#333)] text-[var(--text-secondary,#aaa)] text-sm">
<th className="px-6 py-4 font-medium">Timestamp</th>
<th className="px-6 py-4 font-medium">Action</th>
<th className="px-6 py-4 font-medium">Target</th>
<th className="px-6 py-4 font-medium">Resource</th>
<th className="px-6 py-4 font-medium">Source</th>
<th className="px-6 py-4 font-medium text-right">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--border,#333)]">
{entries.map((entry) => (
<tr
key={entry.id}
className="hover:bg-[var(--hover-bg,#2a2a3e)] transition-colors group"
>
<td className="px-6 py-3 whitespace-nowrap text-sm text-[var(--text-secondary,#aaa)]">
{new Date(entry.timestamp).toLocaleString()}
</td>
<td className="px-6 py-3 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs rounded-md border capitalize ${getActionColor(entry.action)}`}
>
{entry.action}
</span>
</td>
<td className="px-6 py-3 whitespace-nowrap text-sm text-[var(--text-primary,#fff)] font-medium capitalize">
{entry.target}
</td>
<td className="px-6 py-3 text-sm text-[var(--text-secondary,#aaa)] font-mono">
{entry.targetName}
</td>
<td className="px-6 py-3 whitespace-nowrap text-sm text-[var(--text-muted,#666)] capitalize">
{entry.source}
</td>
<td className="px-6 py-3 whitespace-nowrap text-right">
<button
onClick={() => setSelectedEntry(entry)}
className="px-3 py-1 text-xs font-medium text-[var(--text-primary,#fff)] bg-[var(--accent,#7c3aed)] hover:bg-opacity-80 rounded-md transition-colors invisible group-hover:visible"
>
View Diff
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{selectedEntry && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="bg-[var(--card-bg,#1e1e2e)] border border-[var(--border,#333)] rounded-2xl w-full max-w-4xl max-h-[85vh] flex flex-col shadow-2xl overflow-hidden scale-in">
{/* Modal Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-[var(--border,#333)] bg-[#15151f]">
<div>
<h3 className="text-xl font-semibold text-[var(--text-primary,#fff)] capitalize">
{selectedEntry.action} {selectedEntry.target}
</h3>
<p className="text-sm text-[var(--text-secondary,#aaa)] font-mono mt-1">
ID: {selectedEntry.targetId} {selectedEntry.targetName}
</p>
</div>
<button
onClick={() => setSelectedEntry(null)}
className="p-2 text-[var(--text-muted,#666)] hover:text-white bg-[var(--hover-bg,#2a2a3e)] hover:bg-[#333] rounded-full transition-colors"
title="Close"
>
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" className="w-5 h-5">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Modal Content */}
<div className="p-6 overflow-y-auto custom-scrollbar flex-1 bg-[#1a1a24]">
{selectedEntry.note && (
<div className="mb-6 p-4 bg-yellow-500/10 border border-yellow-500/20 text-yellow-300 rounded-xl text-sm italic">
📝 {selectedEntry.note}
</div>
)}
{selectedEntry.diff?.isEmpty ? (
<div className="text-center p-8 text-[var(--text-muted,#666)]">
No changes detected in Diff
</div>
) : (
<div className="space-y-6">
{/* Added Keys */}
{selectedEntry.diff?.added?.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-green-400 mb-2 uppercase tracking-wider">
++ Added Properties
</h4>
<pre className="bg-[#111116] border border-green-500/20 rounded-xl p-4 overflow-x-auto text-xs font-mono text-green-300 shadow-inner">
{JSON.stringify(
selectedEntry.diff.added.reduce(
(acc, key) => ({ ...acc, [key]: selectedEntry.after?.[key] }),
{}
),
null,
2
)}
</pre>
</div>
)}
{/* Removed Keys */}
{selectedEntry.diff?.removed?.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-red-400 mb-2 uppercase tracking-wider">
-- Removed Properties
</h4>
<pre className="bg-[#111116] border border-red-500/20 rounded-xl p-4 overflow-x-auto text-xs font-mono text-red-300 shadow-inner">
{JSON.stringify(
selectedEntry.diff.removed.reduce(
(acc, key) => ({ ...acc, [key]: selectedEntry.before?.[key] }),
{}
),
null,
2
)}
</pre>
</div>
)}
{/* Changed Keys */}
{selectedEntry.diff?.changed?.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-yellow-400 mb-2 uppercase tracking-wider">
~ Modified Properties
</h4>
<div className="space-y-2">
{selectedEntry.diff.changed.map((change, idx) => (
<div
key={idx}
className="bg-[#111116] border border-yellow-500/20 rounded-xl overflow-hidden shadow-inner text-sm font-mono flex flex-col"
>
<div className="px-4 py-2 bg-[#1b1b22] border-b border-[#2d2d3a] text-yellow-500/80 font-semibold">
{change.key}
</div>
<div className="grid grid-cols-2 divide-x divide-[#2d2d3a]">
<div className="p-4 bg-red-500/5 text-red-300/80">
<div className="text-[10px] text-red-400/50 mb-1 uppercase">
Before
</div>
<pre className="whitespace-pre-wrap break-words">
{JSON.stringify(change.from, null, 2)}
</pre>
</div>
<div className="p-4 bg-green-500/5 text-green-300/80">
<div className="text-[10px] text-green-400/50 mb-1 uppercase">
After
</div>
<pre className="whitespace-pre-wrap break-words">
{JSON.stringify(change.to, null, 2)}
</pre>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,44 @@
"use client";
import { useState, useEffect } from "react";
import ConfigAuditViewer from "./ConfigAuditViewer";
export default function ConfigAuditPage() {
const [summary, setSummary] = useState<any>(null);
useEffect(() => {
fetch("/api/audit?summary=true")
.then((res) => res.json())
.then((data) => setSummary(data))
.catch((err) => console.error(err));
}, []);
return (
<div className="flex flex-col gap-6 w-full max-w-6xl mx-auto">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-[var(--text-primary,#fff)]">
Configuration Audit
</h1>
<p className="text-sm text-[var(--text-secondary,#aaa)] mt-1">
Track and diff changes made to routing policies, combos, and connections.
</p>
</div>
{summary && (
<div className="flex items-center gap-4 text-sm bg-[var(--card-bg,#1e1e2e)] px-4 py-2 rounded-xl border border-[var(--border,#333)]">
<div className="flex flex-col">
<span className="text-[var(--text-muted,#666)]">Total Audits</span>
<span className="font-mono text-[var(--text-primary,#fff)] font-semibold">
{summary.totalEntries}
</span>
</div>
</div>
)}
</div>
<div className="bg-[var(--card-bg,#1e1e2e)] border border-[var(--border,#333)] rounded-xl overflow-hidden shadow-sm">
<ConfigAuditViewer />
</div>
</div>
);
}
@@ -48,6 +48,7 @@ export default function HealthPage() {
const [telemetry, setTelemetry] = useState(null);
const [cache, setCache] = useState(null);
const [signatureCache, setSignatureCache] = useState(null);
const [degradation, setDegradation] = useState(null);
const [resetting, setResetting] = useState(false);
const fetchHealth = useCallback(async () => {
@@ -69,12 +70,14 @@ export default function HealthPage() {
fetch("/api/telemetry/summary").then((r) => r.json()),
fetch("/api/cache/stats").then((r) => r.json()),
fetch("/api/rate-limits").then((r) => r.json()),
fetch("/api/health/degradation").then((r) => r.json()),
]);
if (results[0].status === "fulfilled") setTelemetry(results[0].value);
if (results[1].status === "fulfilled") setCache(results[1].value);
if (results[2].status === "fulfilled" && results[2].value.cacheStats) {
setSignatureCache(results[2].value.cacheStats);
}
if (results[3].status === "fulfilled") setDegradation(results[3].value);
}, []);
useEffect(() => {
@@ -270,6 +273,80 @@ export default function HealthPage() {
</Card>
</div>
{/* Graceful Degradation Status */}
{degradation && degradation.features && degradation.features.length > 0 && (
<Card className="p-5" role="region" aria-label="Graceful Degradation Status">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">healing</span>
Graceful Degradation Status
</h2>
<div className="flex items-center gap-3 text-xs text-text-muted font-medium">
<span className="px-2 py-0.5 rounded bg-green-500/10 text-green-400">
Full: {degradation.summary.full}
</span>
<span className="px-2 py-0.5 rounded bg-amber-500/10 text-amber-500">
Reduced: {degradation.summary.reduced}
</span>
<span className="px-2 py-0.5 rounded bg-orange-500/10 text-orange-500">
Minimal: {degradation.summary.minimal}
</span>
<span className="px-2 py-0.5 rounded bg-red-500/10 text-red-500">
Default: {degradation.summary.default}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{degradation.features.map((feat: any) => {
const bg =
feat.level === "full"
? "bg-green-500/5 border-green-500/10"
: feat.level === "reduced"
? "bg-amber-500/5 border-amber-500/20"
: feat.level === "minimal"
? "bg-orange-500/5 border-orange-500/20"
: "bg-red-500/5 border-red-500/20";
const dot =
feat.level === "full"
? "bg-green-500"
: feat.level === "reduced"
? "bg-amber-500"
: feat.level === "minimal"
? "bg-orange-500"
: "bg-red-500";
return (
<div
key={feat.feature}
className={`rounded-lg p-3 border \${bg} flex flex-col gap-2`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold capitalize flex items-center gap-2 text-[var(--text-primary,#fff)]">
<span className={`w-2 h-2 rounded-full \${dot}`}></span>
{feat.feature}
</span>
<span className="text-xs uppercase tracking-wider font-bold opacity-70">
{feat.level}
</span>
</div>
<div className="text-xs text-[var(--text-secondary,#aaa)]">{feat.capability}</div>
{feat.reason && (
<div
className="text-[10px] text-red-300 mt-1 bg-red-900/20 p-1.5 rounded"
title={feat.reason}
>
{feat.reason.length > 80 ? feat.reason.substring(0, 80) + "..." : feat.reason}
</div>
)}
<div className="text-[10px] text-[var(--text-muted,#666)] text-right mt-1">
Since {new Date(feat.since).toLocaleTimeString()}
</div>
</div>
);
})}
</div>
</Card>
)}
{/* Telemetry Cards — Latency & Prompt Cache */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Latency Card */}
@@ -95,6 +95,7 @@ function getConnectionErrorTag(connection) {
export default function ProvidersPage() {
const [connections, setConnections] = useState<any[]>([]);
const [providerNodes, setProviderNodes] = useState<any[]>([]);
const [expirations, setExpirations] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [showAddCompatibleModal, setShowAddCompatibleModal] = useState(false);
const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] = useState(false);
@@ -108,14 +109,17 @@ export default function ProvidersPage() {
useEffect(() => {
const fetchData = async () => {
try {
const [connectionsRes, nodesRes] = await Promise.all([
const [connectionsRes, nodesRes, expirationsRes] = await Promise.all([
fetch("/api/providers"),
fetch("/api/provider-nodes"),
fetch("/api/providers/expiration"),
]);
const connectionsData = await connectionsRes.json();
const nodesData = await nodesRes.json();
const expirationsData = await expirationsRes.json();
if (connectionsRes.ok) setConnections(connectionsData.connections || []);
if (nodesRes.ok) setProviderNodes(nodesData.nodes || []);
if (expirationsRes.ok && expirationsData) setExpirations(expirationsData);
} catch (error) {
console.log("Error fetching data:", error);
} finally {
@@ -188,7 +192,16 @@ export default function ProvidersPage() {
const errorCode = latestError ? getConnectionErrorTag(latestError) : null;
const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null;
return { connected, error, total, errorCode, errorTime, allDisabled };
// Check expirations
const providerExpirations =
expirations?.list?.filter((e: any) => e.provider === providerId) || [];
const hasExpired = providerExpirations.some((e: any) => e.status === "expired");
const hasExpiringSoon = providerExpirations.some((e: any) => e.status === "expiring_soon");
let expiryStatus = null;
if (hasExpired) expiryStatus = "expired";
else if (hasExpiringSoon) expiryStatus = "expiring_soon";
return { connected, error, total, errorCode, errorTime, allDisabled, expiryStatus };
};
// Toggle all connections for a provider on/off
@@ -289,6 +302,40 @@ export default function ProvidersPage() {
return (
<div className="flex flex-col gap-6">
{/* Expiration Banner */}
{expirations?.summary &&
(expirations.summary.expired > 0 || expirations.summary.expiringSoon > 0) && (
<div
className={`p-4 rounded-xl flex items-start gap-3 border ${
expirations.summary.expired > 0
? "bg-red-500/10 border-red-500/20"
: "bg-amber-500/10 border-amber-500/20"
}`}
>
<span
className={`material-symbols-outlined text-[24px] ${
expirations.summary.expired > 0 ? "text-red-500" : "text-amber-500"
}`}
>
{expirations.summary.expired > 0 ? "error" : "warning"}
</span>
<div className="flex-1">
<h3
className={`font-semibold ${expirations.summary.expired > 0 ? "text-red-500" : "text-amber-500"}`}
>
{expirations.summary.expired > 0
? `${expirations.summary.expired} Provider connection(s) expired`
: `${expirations.summary.expiringSoon} Provider connection(s) expiring soon`}
</h3>
<p className="text-sm mt-1 opacity-80 text-text-main">
{expirations.summary.expired > 0
? "Immediate action required. Expired connections will permanently fail."
: "Please review and renew expiring connections to avoid disruption."}
</p>
</div>
</div>
)}
{/* OAuth Providers */}
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
@@ -582,6 +629,16 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
) : (
<>
{getStatusDisplay(connected, error, errorCode, t)}
{stats.expiryStatus === "expired" && (
<Badge variant="error" size="sm" dot>
Expired
</Badge>
)}
{stats.expiryStatus === "expiring_soon" && (
<Badge variant="warning" size="sm" dot>
Expiring Soon
</Badge>
)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</>
)}
@@ -709,6 +766,16 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
) : (
<>
{getStatusDisplay(connected, error, errorCode, t)}
{stats.expiryStatus === "expired" && (
<Badge variant="error" size="sm" dot>
Expired
</Badge>
)}
{stats.expiryStatus === "expiring_soon" && (
<Badge variant="warning" size="sm" dot>
Expiring Soon
</Badge>
)}
{isCompatible && (
<Badge variant="default" size="sm">
{provider.apiType === "responses" ? t("responses") : t("chat")}
@@ -221,9 +221,7 @@ export default function AppearanceTab() {
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">
{getSettingsLabel("sidebarVisibility", "Hide sidebar items")}
</p>
<p className="font-medium">{t("sidebarVisibilityToggle")}</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"sidebarVisibilityDesc",
@@ -249,7 +247,7 @@ export default function AppearanceTab() {
>
<p className="font-medium">{item.label}</p>
<Toggle
checked={hiddenSidebarSet.has(item.id)}
checked={!hiddenSidebarSet.has(item.id)}
onChange={() => toggleSidebarItem(item.id)}
disabled={loading}
/>
@@ -0,0 +1,132 @@
"use client";
import { useState, useEffect } from "react";
import { Card, Button, Input } from "@/shared/components";
import { useTranslations } from "next-intl";
import { useNotificationStore } from "@/store/notificationStore";
export default function AutoDisableCard() {
const [data, setData] = useState({ enabled: false, threshold: 3 });
const [draft, setDraft] = useState({ enabled: false, threshold: 3 });
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editMode, setEditMode] = useState(false);
const t = useTranslations("settings");
const tc = useTranslations("common");
const notify = useNotificationStore();
useEffect(() => {
fetch("/api/settings/auto-disable-accounts")
.then((res) => res.json())
.then((json) => {
setData(json);
setDraft(json);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
const handleSave = async () => {
setSaving(true);
try {
const res = await fetch("/api/settings/auto-disable-accounts", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(draft),
});
if (!res.ok) throw new Error("Failed to save auto-disable config");
const savedData = await res.json();
setData(savedData);
setEditMode(false);
notify.success(t("savedSuccessfully") || "Saved successfully");
} catch (err) {
notify.error(err instanceof Error ? err.message : "Error saving");
} finally {
setSaving(false);
}
};
if (loading) return null;
return (
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
block
</span>
<h2 className="text-lg font-bold">{t("autoDisableBannedAccounts")}</h2>
</div>
{editMode ? (
<div className="flex gap-2">
<Button
size="sm"
variant="secondary"
onClick={() => {
setDraft(data);
setEditMode(false);
}}
>
{tc("cancel")}
</Button>
<Button
size="sm"
variant="primary"
icon="save"
onClick={handleSave}
disabled={saving}
>
{tc("save")}
</Button>
</div>
) : (
<Button size="sm" variant="secondary" icon="edit" onClick={() => setEditMode(true)}>
{tc("edit")}
</Button>
)}
</div>
<p className="text-sm text-text-muted mb-4">{t("autoDisableDescription")}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="rounded-lg bg-black/5 dark:bg-white/5 p-4 flex flex-col justify-center">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={editMode ? draft.enabled : data.enabled}
onChange={(e) => setDraft((prev) => ({ ...prev, enabled: e.target.checked }))}
disabled={!editMode}
className="w-4 h-4 text-primary bg-surface/50 border-white/20 rounded focus:ring-primary/50"
/>
<span className="text-sm font-medium">{t("autoDisableBannedAccounts")}</span>
</label>
</div>
<div className="rounded-lg bg-black/5 dark:bg-white/5 p-4">
<h3 className="text-xs font-bold uppercase tracking-wider mb-2 flex items-center gap-2">
{t("autoDisableThreshold")}
</h3>
{editMode ? (
<Input
type="number"
min="1"
max="10"
value={draft.threshold}
onChange={(e) =>
setDraft((prev) => ({ ...prev, threshold: parseInt(e.target.value) || 1 }))
}
disabled={!draft.enabled}
/>
) : (
<span className={`text-sm font-mono ${!data.enabled && "opacity-50"}`}>
{data.threshold} {t("failures", { count: data.threshold })}
</span>
)}
<p className="text-xs text-text-muted mt-2">{t("autoDisableThresholdDesc")}</p>
</div>
</div>
</div>
</Card>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Card, Button, ProxyConfigModal } from "@/shared/components";
import { Card, Button, ProxyConfigModal, Toggle } from "@/shared/components";
import { useTranslations } from "next-intl";
import ProxyRegistryManager from "./ProxyRegistryManager";
@@ -11,6 +11,8 @@ export default function ProxyTab() {
const mountedRef = useRef(true);
const t = useTranslations("settings");
const tc = useTranslations("common");
const [debugMode, setDebugMode] = useState(false);
const [loading, setLoading] = useState(true);
const loadGlobalProxy = async () => {
try {
@@ -22,6 +24,21 @@ export default function ProxyTab() {
} catch {}
};
const updateDebugMode = async (value: boolean) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ debugMode: value }),
});
if (res.ok) {
setDebugMode(value);
}
} catch (err) {
console.error("Failed to update debugMode:", err);
}
};
useEffect(() => {
mountedRef.current = true;
async function init() {
@@ -40,6 +57,19 @@ export default function ProxyTab() {
};
}, []);
useEffect(() => {
fetch("/api/settings")
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then((data) => {
setDebugMode(data.debugMode === true);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
return (
<>
<div className="flex flex-col gap-6">
@@ -78,6 +108,18 @@ export default function ProxyTab() {
</Card>
<ProxyRegistryManager />
<Card className="p-6 mt-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{t("debugToggle")}</p>
</div>
<Toggle
checked={debugMode}
onChange={() => updateDebugMode(!debugMode)}
disabled={loading}
/>
</div>
</Card>
</div>
<ProxyConfigModal
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
import { Card, Button } from "@/shared/components";
import { useNotificationStore } from "@/store/notificationStore";
import { useLocale, useTranslations } from "next-intl";
import AutoDisableCard from "./AutoDisableCard";
// ─── State colors and labels ──────────────────────────────────────────────
const STATE_STYLES = {
@@ -656,6 +657,8 @@ export default function ResilienceTab() {
onSave={handleSaveProfiles}
saving={saving}
/>
{/* 1.5 Auto Disable Banned Accounts */}
<AutoDisableCard />
{/* 2. Rate Limiting (editable defaults + active limiters) */}
<RateLimitCard
rateLimitStatus={data?.rateLimitStatus || []}
@@ -152,6 +152,40 @@ export default function RoutingTab() {
</p>
</Card>
{/* Adaptive Volume Routing */}
<Card>
<div className="flex items-start justify-between gap-4">
<div className="flex gap-3">
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500 h-fit">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
network_ping
</span>
</div>
<div>
<h3 className="text-lg font-semibold">
{t("adaptiveVolumeRouting") || "Adaptive Volume Routing"}
</h3>
<p className="text-sm text-text-muted mt-1">
{t("adaptiveVolumeRoutingDesc") ||
"Automatically adjusts traffic volume between providers based on real-time latency and error rates."}
</p>
</div>
</div>
<div className="pt-1">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={!!settings.adaptiveVolumeRouting}
onChange={(e) => updateSetting({ adaptiveVolumeRouting: e.target.checked })}
disabled={loading}
/>
<div className="w-11 h-6 bg-border peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
</div>
</Card>
{/* Wildcard Aliases */}
<Card>
<div className="flex items-center gap-3 mb-4">
@@ -7,6 +7,7 @@ import Card from "@/shared/components/Card";
import Badge from "@/shared/components/Badge";
import QuotaProgressBar from "./QuotaProgressBar";
import { calculatePercentage } from "./utils";
import ProviderIcon from "@/shared/components/ProviderIcon";
const planVariants = {
free: "default",
@@ -70,15 +71,7 @@ export default function ProviderLimitCard({
{provider?.slice(0, 2).toUpperCase() || "PR"}
</span>
) : (
<Image
src={`/providers/${provider}.png`}
alt={provider || t("providerLimits")}
width={40}
height={40}
className="object-contain rounded-lg"
sizes="40px"
onError={() => setImgError(true)}
/>
<ProviderIcon providerId={provider} size={40} />
)}
</div>
@@ -211,7 +211,12 @@ export default function ProviderLimits() {
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) &&
(conn.authType === "oauth" || conn.authType === "apikey")
);
await Promise.all(usageConnections.map((conn) => fetchQuota(conn.id, conn.provider)));
// Fix Issue #784: Fetch quotas in chunks of 5 to avoid spamming the backend/provider APIs and hanging the UI.
const chunkSize = 5;
for (let i = 0; i < usageConnections.length; i += chunkSize) {
const chunk = usageConnections.slice(i, i + chunkSize);
await Promise.all(chunk.map((conn) => fetchQuota(conn.id, conn.provider)));
}
setLastUpdated(new Date());
} catch (error) {
console.error("Error refreshing all:", error);
+38
View File
@@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import {
getAuditLog,
getAuditSummary,
AuditTarget,
AuditAction,
AuditSource,
} from "@/domain/configAudit";
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const summary = url.searchParams.get("summary");
if (summary === "true") {
return NextResponse.json(getAuditSummary());
}
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
const offset = parseInt(url.searchParams.get("offset") || "0", 10);
const target = url.searchParams.get("target") as AuditTarget | null;
const action = url.searchParams.get("action") as AuditAction | null;
const source = url.searchParams.get("source") as AuditSource | null;
const since = url.searchParams.get("since");
const options: any = { limit, offset };
if (target) options.target = target;
if (action) options.action = action;
if (source) options.source = source;
if (since) options.since = since;
const result = getAuditLog(options);
return NextResponse.json(result);
} catch (error) {
console.error("[API ERROR] /api/audit GET:", error);
return NextResponse.json({ error: "Failed to fetch audit log." }, { status: 500 });
}
}
+30
View File
@@ -0,0 +1,30 @@
import { NextResponse } from "next/server";
import {
getDegradationReport,
getDegradationSummary,
hasAnyDegradation,
} from "@/domain/degradation";
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const summaryStr = url.searchParams.get("summary");
if (summaryStr === "true") {
return NextResponse.json({
summary: getDegradationSummary(),
isDegraded: hasAnyDegradation(),
});
}
const report = getDegradationReport();
return NextResponse.json({
active: hasAnyDegradation(),
summary: getDegradationSummary(),
features: report,
});
} catch (error) {
console.error("[API ERROR] /api/health/degradation GET:", error);
return NextResponse.json({ error: "Failed to fetch degradation report." }, { status: 500 });
}
}
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import { getAllExpirations, getExpirationSummary } from "@/domain/providerExpiration";
export async function GET() {
try {
const list = getAllExpirations();
const summary = getExpirationSummary();
return NextResponse.json({
summary,
list,
});
} catch (error) {
console.error("[API ERROR] /api/providers/expiration GET:", error);
return NextResponse.json({ error: "Failed to fetch expiration metadata." }, { status: 500 });
}
}
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { PATCH } from "../route";
// Mock the localDb functions used in the route
vi.mock("../../../../lib/localDb", () => {
const original = vi.importActual("../../../../lib/localDb");
return {
...original,
getSettings: vi.fn(),
updateSettings: vi.fn(),
};
});
import { getSettings, updateSettings } from "../../../../lib/localDb";
// Helper to create a Request with JSON body
function createPatchRequest(body: unknown) {
return new Request("http://localhost/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
describe("PATCH /api/settings", () => {
beforeEach(() => {
vi.resetAllMocks();
// Default settings before each test
(getSettings as any).mockResolvedValue({
debugMode: false,
hiddenSidebarItems: [],
});
// Mock updateSettings to merge updates into the original
(updateSettings as any).mockImplementation(async (updates: Record<string, unknown>) => {
const current = await (getSettings as any)();
return { ...current, ...updates };
});
});
it("toggles debugMode via PATCH", async () => {
const req = createPatchRequest({ debugMode: true });
const res = await PATCH(req as any);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.debugMode).toBe(true);
// Ensure password is not leaked
expect(json).not.toHaveProperty("password");
// Verify DB update called with correct payload
expect(updateSettings).toHaveBeenCalledOnce();
const calledWith = (updateSettings as any).mock.calls[0][0];
expect(calledWith.debugMode).toBe(true);
});
it("updates hiddenSidebarItems via PATCH", async () => {
const req = createPatchRequest({ hiddenSidebarItems: [] });
const res = await PATCH(req as any);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.hiddenSidebarItems).toEqual([]);
expect(updateSettings).toHaveBeenCalledOnce();
const calledWith = (updateSettings as any).mock.calls[0][0];
expect(calledWith.hiddenSidebarItems).toEqual([]);
});
});
+18 -3
View File
@@ -95,6 +95,17 @@ export async function POST(req: NextRequest) {
}
const config = getAutoUpdateConfig();
const validation = await validateAutoUpdateRuntime(config);
if (!validation.supported) {
return NextResponse.json(
{
success: false,
error: validation.reason || "Auto-update is not supported in this environment.",
},
{ status: 400 }
);
}
// If we are in docker-compose mode, use the detached shell script background updates
if (config.mode === "docker-compose") {
@@ -132,9 +143,13 @@ export async function POST(req: NextRequest) {
try {
// Step 1: Install
send({ step: "install", status: "running", message: `Installing omniroute@${latest}...` });
await execFileAsync("npm", ["install", "-g", `omniroute@${latest}`, "--ignore-scripts"], {
timeout: 300000,
});
await execFileAsync(
"npm",
["install", "-g", `omniroute@${latest}`, "--ignore-scripts", "--legacy-peer-deps"],
{
timeout: 300000,
}
);
send({ step: "install", status: "done", message: `Installed omniroute@${latest}` });
// Step 2: Rebuild native modules (critical for better-sqlite3)
+285
View File
@@ -0,0 +1,285 @@
/**
* Configuration Audit Trail
*
* Records every change to provider connections, combos, and routing
* policies with before/after snapshots and diff detection.
* Enables rollback to previous configurations when changes cause issues.
*
* Each entry captures:
* - What changed (target type + ID)
* - Who/what triggered the change (source)
* - Before/after state snapshots
* - Computed diff summary
* - Optional human notes
*/
/** Types of configuration entities that can be audited */
export type AuditTarget = "provider" | "combo" | "policy" | "connection" | "settings";
/** How the change was triggered */
export type AuditSource = "dashboard" | "api" | "sync" | "auto-healing" | "cli" | "mcp";
/** Type of change */
export type AuditAction = "create" | "update" | "delete" | "enable" | "disable";
/** A single audit log entry */
export interface ConfigAuditEntry {
/** Unique entry ID */
id: string;
/** ISO timestamp of the change */
timestamp: string;
/** Type of change */
action: AuditAction;
/** What type of entity was changed */
target: AuditTarget;
/** ID of the changed entity */
targetId: string;
/** Human-readable name of the entity */
targetName: string;
/** State before the change (null for creates) */
before: Record<string, unknown> | null;
/** State after the change (null for deletes) */
after: Record<string, unknown> | null;
/** How the change was triggered */
source: AuditSource;
/** Computed diff summary */
diff: ConfigDiff;
/** Optional human note */
note: string | null;
}
/** Computed diff between two states */
export interface ConfigDiff {
/** Keys that were added */
added: string[];
/** Keys that were removed */
removed: string[];
/** Keys whose values changed */
changed: Array<{ key: string; from: unknown; to: unknown }>;
/** True if the states are identical */
isEmpty: boolean;
}
/** Configuration snapshot for export/import */
export interface ConfigSnapshot {
/** ISO timestamp when snapshot was taken */
timestamp: string;
/** Semantic version tag */
version: string;
/** Description of the snapshot */
description: string;
/** Full configuration data */
data: Record<string, unknown>;
}
// ── In-memory store ──────────────────────────────────────────────────────────
// In production, persist to SQLite alongside other domain state.
let auditLog: ConfigAuditEntry[] = [];
let idCounter = 0;
function generateId(): string {
idCounter++;
const ts = Date.now().toString(36);
const seq = idCounter.toString(36).padStart(4, "0");
return `audit-${ts}-${seq}`;
}
/**
* Compute a structured diff between two configuration states.
*/
export function computeDiff(
before: Record<string, unknown> | null,
after: Record<string, unknown> | null
): ConfigDiff {
const beforeKeys = new Set(before ? Object.keys(before) : []);
const afterKeys = new Set(after ? Object.keys(after) : []);
const added: string[] = [];
const removed: string[] = [];
const changed: Array<{ key: string; from: unknown; to: unknown }> = [];
// Keys in after but not in before → added
for (const key of afterKeys) {
if (!beforeKeys.has(key)) {
added.push(key);
}
}
// Keys in before but not in after → removed
for (const key of beforeKeys) {
if (!afterKeys.has(key)) {
removed.push(key);
}
}
// Keys in both → check for changes
for (const key of beforeKeys) {
if (afterKeys.has(key)) {
const beforeVal = before![key];
const afterVal = after![key];
if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
changed.push({ key, from: beforeVal, to: afterVal });
}
}
}
return {
added,
removed,
changed,
isEmpty: added.length === 0 && removed.length === 0 && changed.length === 0,
};
}
/**
* Record a configuration change in the audit log.
*/
export function recordChange(
action: AuditAction,
target: AuditTarget,
targetId: string,
targetName: string,
before: Record<string, unknown> | null,
after: Record<string, unknown> | null,
source: AuditSource,
note?: string | null
): ConfigAuditEntry {
const entry: ConfigAuditEntry = {
id: generateId(),
timestamp: new Date().toISOString(),
action,
target,
targetId,
targetName,
before,
after,
source,
diff: computeDiff(before, after),
note: note ?? null,
};
auditLog.push(entry);
// Keep log bounded (max 1000 entries in memory)
if (auditLog.length > 1000) {
auditLog = auditLog.slice(-1000);
}
return entry;
}
/**
* Get audit entries, optionally filtered.
*/
export function getAuditLog(options?: {
target?: AuditTarget;
targetId?: string;
action?: AuditAction;
source?: AuditSource;
since?: string; // ISO date
limit?: number;
offset?: number;
}): { entries: ConfigAuditEntry[]; total: number } {
let filtered = auditLog;
if (options?.target) {
filtered = filtered.filter((e) => e.target === options.target);
}
if (options?.targetId) {
filtered = filtered.filter((e) => e.targetId === options.targetId);
}
if (options?.action) {
filtered = filtered.filter((e) => e.action === options.action);
}
if (options?.source) {
filtered = filtered.filter((e) => e.source === options.source);
}
if (options?.since) {
filtered = filtered.filter((e) => e.timestamp >= options.since!);
}
const total = filtered.length;
// Sort newest first
filtered = [...filtered].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
// Paginate
const offset = options?.offset ?? 0;
const limit = options?.limit ?? 50;
filtered = filtered.slice(offset, offset + limit);
return { entries: filtered, total };
}
/**
* Get a specific audit entry by ID.
*/
export function getAuditEntry(id: string): ConfigAuditEntry | null {
return auditLog.find((e) => e.id === id) ?? null;
}
/**
* Get the state of an entity before a specific audit entry.
* Enables rollback by returning the `before` snapshot.
*/
export function getRollbackState(entryId: string): Record<string, unknown> | null {
const entry = getAuditEntry(entryId);
if (!entry) return null;
return entry.before;
}
/**
* Create a full configuration snapshot for export.
*/
export function createSnapshot(
version: string,
description: string,
configData: Record<string, unknown>
): ConfigSnapshot {
return {
timestamp: new Date().toISOString(),
version,
description,
data: JSON.parse(JSON.stringify(configData)), // deep clone
};
}
/**
* Get summary statistics of the audit log.
*/
export function getAuditSummary(): {
totalEntries: number;
byTarget: Record<string, number>;
byAction: Record<string, number>;
bySource: Record<string, number>;
oldestEntry: string | null;
newestEntry: string | null;
} {
const byTarget: Record<string, number> = {};
const byAction: Record<string, number> = {};
const bySource: Record<string, number> = {};
for (const entry of auditLog) {
byTarget[entry.target] = (byTarget[entry.target] || 0) + 1;
byAction[entry.action] = (byAction[entry.action] || 0) + 1;
bySource[entry.source] = (bySource[entry.source] || 0) + 1;
}
return {
totalEntries: auditLog.length,
byTarget,
byAction,
bySource,
oldestEntry: auditLog.length > 0 ? auditLog[0].timestamp : null,
newestEntry: auditLog.length > 0 ? auditLog[auditLog.length - 1].timestamp : null,
};
}
/**
* Reset the audit log. Useful for testing.
*/
export function resetAuditLog(): void {
auditLog = [];
idCounter = 0;
}
+253
View File
@@ -0,0 +1,253 @@
/**
* Graceful Degradation Framework
*
* Provides a standardized pattern for services that depend on external
* systems (Redis, vector databases, SSH, external APIs) to degrade
* capability instead of failing completely.
*
* Hierarchy: Full Reduced Minimal Safe Default
*
* Each service wraps its external calls with withDegradation(), which
* tries the primary path, falls back to a secondary, and ultimately
* returns a safe default if all backends are unavailable.
*/
/** Degradation levels from best to worst */
export type DegradationLevel = "full" | "reduced" | "minimal" | "default";
/** Status report for a degraded service */
export interface DegradationStatus {
/** Current operational level */
level: DegradationLevel;
/** Name of the feature/service */
feature: string;
/** Human-readable description of current capability */
capability: string;
/** Why the service is degraded (empty string if full) */
reason: string;
/** Timestamp of last status change */
since: string;
}
/** Result wrapper that includes degradation info */
export interface DegradedResult<T> {
/** The actual result (from primary, fallback, or default) */
result: T;
/** Degradation status */
status: DegradationStatus;
}
// ── Global degradation registry ─────────────────────────────────────────────
const registry = new Map<string, DegradationStatus>();
/**
* Execute an operation with graceful degradation.
*
* Tries the primary function first. If it fails, tries the fallback.
* If both fail, returns the safe default. All transitions are tracked
* in the global registry for dashboard visibility.
*
* @param feature - Name of the feature (e.g., "rate-limiting", "semantic-search")
* @param primary - Primary implementation (full capability)
* @param fallback - Fallback implementation (reduced capability)
* @param safeDefault - Safe default value (minimal/no capability)
* @param options - Optional configuration
* @returns Result with degradation status
*
* @example
* ```typescript
* const { result, status } = await withDegradation(
* 'rate-limiting',
* () => redisRateLimit(key, limit), // Full: distributed
* () => memoryRateLimit(key, limit), // Reduced: single-instance
* { allowed: true, remaining: Infinity }, // Default: permissive
* );
* ```
*/
export async function withDegradation<T>(
feature: string,
primary: () => T | Promise<T>,
fallback: () => T | Promise<T>,
safeDefault: T,
options?: {
/** Description of full capability */
fullCapability?: string;
/** Description of reduced capability */
reducedCapability?: string;
/** Description of default capability */
defaultCapability?: string;
/** Log function for degradation events */
onDegrade?: (status: DegradationStatus) => void;
}
): Promise<DegradedResult<T>> {
const now = new Date().toISOString();
// Try primary
try {
const result = await primary();
const status: DegradationStatus = {
level: "full",
feature,
capability: options?.fullCapability ?? "Full capability",
reason: "",
since: now,
};
updateRegistry(feature, status);
return { result, status };
} catch (primaryError) {
// Primary failed, try fallback
try {
const result = await fallback();
const status: DegradationStatus = {
level: "reduced",
feature,
capability: options?.reducedCapability ?? "Reduced capability (fallback active)",
reason: primaryError instanceof Error ? primaryError.message : String(primaryError),
since: now,
};
updateRegistry(feature, status);
options?.onDegrade?.(status);
return { result, status };
} catch (fallbackError) {
// Both failed, return safe default
const reason = [
primaryError instanceof Error ? primaryError.message : String(primaryError),
fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
].join(" → ");
const status: DegradationStatus = {
level: "default",
feature,
capability: options?.defaultCapability ?? "Safe default (all backends unavailable)",
reason,
since: now,
};
updateRegistry(feature, status);
options?.onDegrade?.(status);
return { result: safeDefault, status };
}
}
}
/**
* Synchronous version for non-async code paths.
*/
export function withDegradationSync<T>(
feature: string,
primary: () => T,
fallback: () => T,
safeDefault: T,
options?: {
fullCapability?: string;
reducedCapability?: string;
defaultCapability?: string;
onDegrade?: (status: DegradationStatus) => void;
}
): DegradedResult<T> {
const now = new Date().toISOString();
try {
const result = primary();
const status: DegradationStatus = {
level: "full",
feature,
capability: options?.fullCapability ?? "Full capability",
reason: "",
since: now,
};
updateRegistry(feature, status);
return { result, status };
} catch (primaryError) {
try {
const result = fallback();
const status: DegradationStatus = {
level: "reduced",
feature,
capability: options?.reducedCapability ?? "Reduced capability",
reason: primaryError instanceof Error ? primaryError.message : String(primaryError),
since: now,
};
updateRegistry(feature, status);
options?.onDegrade?.(status);
return { result, status };
} catch (fallbackError) {
const status: DegradationStatus = {
level: "default",
feature,
capability: options?.defaultCapability ?? "Safe default",
reason: `${primaryError}${fallbackError}`,
since: now,
};
updateRegistry(feature, status);
options?.onDegrade?.(status);
return { result: safeDefault, status };
}
}
}
// ── Registry management ─────────────────────────────────────────────────────
function updateRegistry(feature: string, status: DegradationStatus): void {
const existing = registry.get(feature);
// Only update 'since' if level actually changed
if (existing && existing.level === status.level) {
status.since = existing.since;
}
registry.set(feature, status);
}
/**
* Get degradation status for all tracked features.
*/
export function getDegradationReport(): DegradationStatus[] {
return Array.from(registry.values()).sort((a, b) => {
const order: Record<DegradationLevel, number> = {
default: 0,
minimal: 1,
reduced: 2,
full: 3,
};
return (order[a.level] ?? 4) - (order[b.level] ?? 4);
});
}
/**
* Get status for a specific feature.
*/
export function getFeatureStatus(feature: string): DegradationStatus | null {
return registry.get(feature) ?? null;
}
/**
* Check if any feature is degraded.
*/
export function hasAnyDegradation(): boolean {
for (const status of registry.values()) {
if (status.level !== "full") return true;
}
return false;
}
/**
* Get count of features at each degradation level.
*/
export function getDegradationSummary(): Record<DegradationLevel, number> {
const summary: Record<DegradationLevel, number> = {
full: 0,
reduced: 0,
minimal: 0,
default: 0,
};
for (const status of registry.values()) {
summary[status.level]++;
}
return summary;
}
/**
* Reset the registry. Useful for testing.
*/
export function resetDegradationRegistry(): void {
registry.clear();
}
+259
View File
@@ -0,0 +1,259 @@
/**
* Provider Expiration Tracking
*
* Tracks OAuth token, subscription, and API credit expiration dates
* per provider connection. Provides proactive alerts before expiration
* so operators can re-authenticate or renew before failures occur.
*
* Prevents the common failure mode where an OAuth token or subscription
* expires silently and requests start failing with 401/402 errors.
*/
/** Types of expiration events */
export type ExpiryType = "oauth_token" | "subscription" | "api_credits" | "free_tier_reset";
/** Status derived from expiration date */
export type ExpiryStatus = "active" | "expiring_soon" | "expired" | "unknown";
/** Tracked expiration for a provider connection */
export interface ProviderExpiration {
/** Connection ID (matches ProviderConnection.id) */
connectionId: string;
/** Provider name (e.g., "claude", "codex", "gemini-cli") */
provider: string;
/** Human-readable connection name */
connectionName: string;
/** ISO date when the credential expires (null = unknown) */
expiresAt: string | null;
/** Type of expiration */
expiryType: ExpiryType;
/** Days before expiry to trigger alert (default: 7) */
alertDays: number;
/** ISO date of last verification */
lastChecked: string;
/** Current status */
status: ExpiryStatus;
/** Optional note (e.g., "Membership expired, needs re-auth on dashboard") */
note: string | null;
}
/** Summary of provider expiration health */
export interface ExpirationSummary {
total: number;
active: number;
expiringSoon: number;
expired: number;
unknown: number;
nextExpiration: ProviderExpiration | null;
}
// ── In-memory store ──────────────────────────────────────────────────────────
// In production, this would be persisted to SQLite alongside other domain state.
// Using in-memory Map for the initial implementation.
const expirations = new Map<string, ProviderExpiration>();
/**
* Calculate expiry status from an expiration date.
*/
function calculateStatus(expiresAt: string | null, alertDays: number): ExpiryStatus {
if (!expiresAt) return "unknown";
const now = new Date();
const expiry = new Date(expiresAt);
if (isNaN(expiry.getTime())) return "unknown";
if (expiry <= now) return "expired";
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry <= alertDays) return "expiring_soon";
return "active";
}
/**
* Register or update an expiration tracking entry.
*/
export function setExpiration(
connectionId: string,
provider: string,
connectionName: string,
expiresAt: string | null,
expiryType: ExpiryType,
options?: { alertDays?: number; note?: string | null }
): ProviderExpiration {
const alertDays = options?.alertDays ?? 7;
const status = calculateStatus(expiresAt, alertDays);
const entry: ProviderExpiration = {
connectionId,
provider,
connectionName,
expiresAt,
expiryType,
alertDays,
lastChecked: new Date().toISOString(),
status,
note: options?.note ?? null,
};
expirations.set(connectionId, entry);
return entry;
}
/**
* Get expiration info for a specific connection.
*/
export function getExpiration(connectionId: string): ProviderExpiration | null {
const entry = expirations.get(connectionId);
if (!entry) return null;
// Recalculate status (may have changed since last check)
entry.status = calculateStatus(entry.expiresAt, entry.alertDays);
return entry;
}
/**
* Get all tracked expirations, with status recalculated.
*/
export function getAllExpirations(): ProviderExpiration[] {
const result: ProviderExpiration[] = [];
for (const entry of expirations.values()) {
entry.status = calculateStatus(entry.expiresAt, entry.alertDays);
result.push(entry);
}
return result.sort((a, b) => {
// Sort: expired first, then expiring_soon, then active, then unknown
const order: Record<ExpiryStatus, number> = {
expired: 0,
expiring_soon: 1,
active: 2,
unknown: 3,
};
return (order[a.status] ?? 4) - (order[b.status] ?? 4);
});
}
/**
* Get connections that are expired or expiring soon.
*/
export function getExpiringSoon(): ProviderExpiration[] {
return getAllExpirations().filter(
(e) => e.status === "expired" || e.status === "expiring_soon"
);
}
/**
* Get a summary of expiration health across all tracked connections.
*/
export function getExpirationSummary(): ExpirationSummary {
const all = getAllExpirations();
const summary: ExpirationSummary = {
total: all.length,
active: 0,
expiringSoon: 0,
expired: 0,
unknown: 0,
nextExpiration: null,
};
let nearestMs = Infinity;
for (const entry of all) {
switch (entry.status) {
case "active":
summary.active++;
break;
case "expiring_soon":
summary.expiringSoon++;
break;
case "expired":
summary.expired++;
break;
case "unknown":
summary.unknown++;
break;
}
// Track nearest expiration
if (entry.expiresAt) {
const ms = new Date(entry.expiresAt).getTime() - Date.now();
if (ms > 0 && ms < nearestMs) {
nearestMs = ms;
summary.nextExpiration = entry;
}
}
}
return summary;
}
/**
* Remove expiration tracking for a connection.
*/
export function removeExpiration(connectionId: string): boolean {
return expirations.delete(connectionId);
}
/**
* Try to detect expiration hints from HTTP response headers.
* Many providers include rate limit reset times that can indicate
* quota or token expiration.
*
* @param provider - Provider ID
* @param status - HTTP status code
* @param headers - Response headers (as plain object)
* @returns Detected expiration date or null
*/
export function detectExpirationFromResponse(
provider: string,
status: number,
headers: Record<string, string>
): { expiresAt: string; expiryType: ExpiryType } | null {
// 401 with specific patterns → token expired
if (status === 401) {
return {
expiresAt: new Date().toISOString(), // Already expired
expiryType: "oauth_token",
};
}
// 402 → payment/subscription expired
if (status === 402) {
return {
expiresAt: new Date().toISOString(),
expiryType: "subscription",
};
}
// Rate limit headers may indicate reset times
const resetHeader =
headers["x-ratelimit-reset"] ||
headers["x-ratelimit-reset-tokens"] ||
headers["retry-after"];
if (resetHeader && status === 429) {
const resetTime = parseInt(resetHeader, 10);
if (!isNaN(resetTime)) {
// Could be epoch seconds or seconds-from-now
const date =
resetTime > 1_000_000_000
? new Date(resetTime * 1000)
: new Date(Date.now() + resetTime * 1000);
return {
expiresAt: date.toISOString(),
expiryType: "free_tier_reset",
};
}
}
return null;
}
/**
* Reset all tracked expirations. Useful for testing.
*/
export function resetExpirations(): void {
expirations.clear();
}
+6
View File
@@ -1702,6 +1702,8 @@
"darkMode": "Dark Mode",
"lightMode": "Light Mode",
"systemTheme": "System Theme",
"debugToggle": "Enable Debug Mode",
"sidebarVisibilityToggle": "Show Sidebar Items",
"enableCache": "Enable Cache",
"cacheTTL": "Cache TTL",
"maxCacheSize": "Max Cache Size",
@@ -1717,6 +1719,10 @@
"timeoutMs": "Timeout (ms)",
"enableSystemPrompt": "Enable System Prompt",
"systemPromptText": "System Prompt Text",
"autoDisableBannedAccounts": "Auto-Disable Banned Accounts",
"autoDisableDescription": "Permanently mark provider connections as deactivated if they return specific terminal ban signals (e.g. HTTP 403 'verify your account'). This removes them from the combo rotation.",
"autoDisableThreshold": "Ban Threshold",
"autoDisableThresholdDesc": "Consecutive ban signals required before permanent deactivation.",
"enableThinking": "Enable Thinking",
"maxThinkingTokens": "Max Thinking Tokens",
"enableProxy": "Enable Proxy",
+57 -2
View File
@@ -24,6 +24,18 @@ type AssetSpec = {
downloadUrl: string;
};
type CloudflaredRuntimeDirs = {
runtimeRoot: string;
homeDir: string;
configDir: string;
cacheDir: string;
dataDir: string;
tempDir: string;
userProfileDir: string;
appDataDir: string;
localAppDataDir: string;
};
type BinaryResolution = {
binaryPath: string | null;
source: CloudflaredInstallSource | null;
@@ -125,6 +137,24 @@ function getLogFilePath() {
return path.join(getTunnelDir(), "quick-tunnel.log");
}
export function getCloudflaredRuntimeDirs(): CloudflaredRuntimeDirs {
const runtimeRoot = path.join(getTunnelDir(), "runtime");
const homeDir = path.join(runtimeRoot, "home");
const userProfileDir = path.join(runtimeRoot, "userprofile");
return {
runtimeRoot,
homeDir,
configDir: path.join(runtimeRoot, "config"),
cacheDir: path.join(runtimeRoot, "cache"),
dataDir: path.join(runtimeRoot, "data"),
tempDir: path.join(runtimeRoot, "tmp"),
userProfileDir,
appDataDir: path.join(userProfileDir, "AppData", "Roaming"),
localAppDataDir: path.join(userProfileDir, "AppData", "Local"),
};
}
function getLocalTargetUrl() {
const { apiPort } = getRuntimePorts();
return `http://127.0.0.1:${apiPort}`;
@@ -138,6 +168,13 @@ async function ensureTunnelDir() {
await fs.mkdir(path.join(getTunnelDir(), "bin"), { recursive: true });
}
async function ensureTunnelRuntimeDirs() {
const runtimeDirs = getCloudflaredRuntimeDirs();
await Promise.all(
Object.values(runtimeDirs).map((dirPath) => fs.mkdir(dirPath, { recursive: true }))
);
}
async function readStateFile(): Promise<PersistedTunnelState> {
try {
const content = await fs.readFile(getStateFilePath(), "utf8");
@@ -202,7 +239,8 @@ export function extractTryCloudflareUrl(text: string) {
}
export function buildCloudflaredChildEnv(
sourceEnv: NodeJS.ProcessEnv = process.env
sourceEnv: NodeJS.ProcessEnv = process.env,
runtimeDirs: CloudflaredRuntimeDirs = getCloudflaredRuntimeDirs()
): NodeJS.ProcessEnv {
const childEnv: NodeJS.ProcessEnv = {};
@@ -213,9 +251,25 @@ export function buildCloudflaredChildEnv(
}
}
childEnv.HOME = runtimeDirs.homeDir;
childEnv.XDG_CONFIG_HOME = runtimeDirs.configDir;
childEnv.XDG_CACHE_HOME = runtimeDirs.cacheDir;
childEnv.XDG_DATA_HOME = runtimeDirs.dataDir;
childEnv.USERPROFILE = runtimeDirs.userProfileDir;
childEnv.APPDATA = runtimeDirs.appDataDir;
childEnv.LOCALAPPDATA = runtimeDirs.localAppDataDir;
if (!childEnv.TMPDIR) childEnv.TMPDIR = runtimeDirs.tempDir;
if (!childEnv.TMP) childEnv.TMP = runtimeDirs.tempDir;
if (!childEnv.TEMP) childEnv.TEMP = runtimeDirs.tempDir;
return childEnv;
}
export function getCloudflaredStartArgs(targetUrl: string) {
return ["tunnel", "--url", targetUrl, "--no-autoupdate"];
}
export function getCloudflaredAssetSpec(
platform = process.platform,
arch = process.arch
@@ -493,6 +547,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>
await stopExistingTunnel();
await ensureTunnelDir();
await ensureTunnelRuntimeDirs();
await fs.writeFile(getLogFilePath(), "", "utf8");
await writeStateFile({
@@ -509,7 +564,7 @@ export async function startCloudflaredTunnel(): Promise<CloudflaredTunnelStatus>
const child = spawn(
binary.binaryPath as string,
["tunnel", "--url", targetUrl, "--no-autoupdate", "--protocol", "http2"],
getCloudflaredStartArgs(targetUrl),
{
stdio: ["ignore", "pipe", "pipe"],
env: buildCloudflaredChildEnv(),
+36 -10
View File
@@ -1,5 +1,5 @@
import { execFile, spawn } from "node:child_process";
import { closeSync, mkdirSync, openSync } from "node:fs";
import { closeSync, mkdirSync, openSync, existsSync } from "node:fs";
import { access } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
@@ -55,15 +55,31 @@ function shellQuote(value: string): string {
}
function parsePatchCommits(raw: string | undefined): string[] {
return (raw || "").split(/[\s,]+/).map((value) => value.trim()).filter(Boolean);
return (raw || "")
.split(/[\s,]+/)
.map((value) => value.trim())
.filter(Boolean);
}
export function getAutoUpdateConfig(env: NodeJS.ProcessEnv = process.env): AutoUpdateConfig {
const dataDir = env.DATA_DIR || "/tmp/omniroute";
const repoDir = env.AUTO_UPDATE_REPO_DIR || "/workspace/omniroute";
let mode = normalizeMode(env.AUTO_UPDATE_MODE);
if (mode === "npm") {
const isGitRepo = existsSync(path.join(process.cwd(), ".git"));
const currentDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
const isGlobalNodeModules = currentDir.includes("node_modules");
// If we are not in a global node_modules directory, we are likely a local source install/build.
// Even if .git is missing (downloaded zip), we should treat it as source.
if (isGitRepo || !isGlobalNodeModules) {
mode = "source" as any;
}
}
return {
mode: normalizeMode(env.AUTO_UPDATE_MODE),
mode,
repoDir,
composeFile: env.AUTO_UPDATE_COMPOSE_FILE || path.join(repoDir, "docker-compose.yml"),
composeProfile: env.AUTO_UPDATE_COMPOSE_PROFILE || "cli",
@@ -97,6 +113,15 @@ export async function validateAutoUpdateRuntime(
execFileImpl: ExecFileLike = execFileAsync,
existsImpl: (targetPath: string) => Promise<boolean> = pathExists
): Promise<AutoUpdateValidation> {
if (config.mode === ("source" as any)) {
return {
supported: false,
reason:
"Manual 'git pull && npm install && npm run build' is required for source installations.",
composeCommand: null,
};
}
if (config.mode !== "docker-compose") {
return { supported: true, reason: null, composeCommand: null };
}
@@ -139,7 +164,8 @@ export async function validateAutoUpdateRuntime(
if (!composeCommand) {
return {
supported: false,
reason: "Neither docker compose nor docker-compose is available inside the OmniRoute container.",
reason:
"Neither docker compose nor docker-compose is available inside the OmniRoute container.",
composeCommand: null,
};
}
@@ -150,7 +176,7 @@ export async function validateAutoUpdateRuntime(
export function buildNpmUpdateScript(latest: string): string {
return [
"set -eu",
`npm install -g omniroute@${latest} --ignore-scripts`,
`npm install -g omniroute@${latest} --ignore-scripts --legacy-peer-deps`,
"if command -v pm2 >/dev/null 2>&1; then",
" pm2 restart omniroute || true",
"fi",
@@ -173,7 +199,7 @@ export function buildDockerComposeUpdateScript({
? 'docker compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"'
: 'docker-compose -f "$COMPOSE_FILE" up -d --build "$SERVICE"';
const patchLines = config.patchCommits.length
? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(' ')}`]
? [`git cherry-pick --keep-redundant-commits ${config.patchCommits.map(shellQuote).join(" ")}`]
: [];
return [
@@ -189,13 +215,13 @@ export function buildDockerComposeUpdateScript({
'git config --global --add safe.directory "$REPO_DIR" >/dev/null 2>&1 || true',
'if [ -n "$(git status --porcelain)" ]; then',
' echo "[AutoUpdate] Refusing update: git worktree has local changes." >&2',
' exit 1',
'fi',
" exit 1",
"fi",
'git fetch --tags "$REMOTE"',
'if ! git rev-parse -q --verify "refs/tags/$TARGET_TAG" >/dev/null 2>&1; then',
' echo "[AutoUpdate] Tag $TARGET_TAG not found on remote $REMOTE." >&2',
' exit 1',
'fi',
" exit 1",
"fi",
'backup_branch="autoupdate/pre-${TARGET_TAG#v}-$(date +%Y%m%d-%H%M%S)"',
'git branch "$backup_branch" >/dev/null 2>&1 || true',
'git checkout -B "autoupdate/${TARGET_TAG#v}" "$TARGET_TAG"',

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