Compare commits

..

37 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza c4cdb52fe6 fix(ci): reference NPM_TOKEN environment for environment-scoped secrets (#18)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-02-14 04:31:26 -03:00
Diego Rodrigues de Sa e Souza 15ec100dcb fix(ci): write .npmrc explicitly for npm auth — support both secrets and vars (#17)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-02-14 04:26:05 -03:00
Diego Rodrigues de Sa e Souza d401ef40f1 fix(ci): support NPM_TOKEN from both secrets and vars, remove --provenance (#16)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-02-14 04:22:00 -03:00
Diego Rodrigues de Sa e Souza 92b4c59fec feat(npm): add npm package publishing with CLI entry point (#15)
- Add bin/omniroute.mjs CLI with banner, auto-open browser, graceful shutdown
- Add scripts/prepublish.mjs to build Next.js standalone into app/
- Add .github/workflows/npm-publish.yml for automated publish on release
- Update package.json: name=omniroute, bin, files, engines, keywords, prepublishOnly
- Add output: 'standalone' to next.config.mjs
- Add MIT LICENSE
- Update .npmignore and .gitignore for app/ build artifact

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-02-14 04:14:14 -03:00
Diego Rodrigues de Sa e Souza 4f0155eb4a Create SECURITY.md for security policy
Added a security policy document outlining supported versions and vulnerability reporting.
2026-02-13 23:21:18 -03:00
Diego Rodrigues de Sa e Souza e544d81624 Merge pull request #13 from diegosouzapw/dependabot/npm_and_yarn/qs-6.14.2
deps: bump qs from 6.14.1 to 6.14.2
2026-02-13 23:19:59 -03:00
Diego Rodrigues de Sa e Souza b8c50cc62d Merge pull request #14 from diegosouzapw/feature/new-endpoints-and-providers
feat: add new endpoints (rerank, audio, moderations) and providers (Hyperbolic, Deepgram, AssemblyAI, NanoBanana)
2026-02-13 22:58:15 -03:00
diegosouzapw c6000ecc8e feat(providers): add new endpoints (rerank, audio, moderations) and providers (Hyperbolic, Deepgram, AssemblyAI, NanoBanana)
- Add /v1/rerank endpoint with Cohere, Together, NVIDIA, Fireworks
- Add /v1/audio/transcriptions with OpenAI, Groq, Deepgram, AssemblyAI
- Add /v1/audio/speech with OpenAI, Hyperbolic, Deepgram
- Add /v1/moderations with OpenAI
- Add Hyperbolic as chat provider (8 models, OpenAI-compatible)
- Add Hyperbolic image generation (SDXL, SD2, FLUX)
- Add NanoBanana image generation (Flash + Pro via nanobananaapi.ai)
- Add Deepgram STT (Nova 3, Nova 2) with Token auth and binary format
- Add AssemblyAI STT (Universal 3 Pro) with async upload-poll workflow
- Add Deepgram TTS (Aura voices) and Hyperbolic TTS (Melo)
- Update EndpointPageClient to show 7 endpoint sections
- Update /v1/models to return type/subtype for all model categories
- Fix build: remove output:standalone from next.config.mjs
2026-02-13 22:57:32 -03:00
diegosouzapw 4e6ba7d9cc docs: Restructure changelog to Keep a Changelog format and add entries for model selector autocomplete and OpenAPI specification. 2026-02-13 21:35:06 -03:00
diegosouzapw c0c2816671 feat: Implement model selection and dynamic model fetching in ChatTesterMode and TestBenchMode. 2026-02-13 21:11:19 -03:00
diegosouzapw 80cc76d531 fix: Ensure server port is free before startup by killing existing processes. 2026-02-13 20:59:11 -03:00
dependabot[bot] af1ffab63d deps: bump qs from 6.14.1 to 6.14.2
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 23:58:54 +00:00
diegosouzapw 7ff1ba978a feat: add OpenAPI specification, generate package-lock, and remove various SVG assets from the public directory. 2026-02-13 20:57:44 -03:00
diegosouzapw d43bbf77fe feat: Enhance restart.sh to include clean build, robust server startup with health check, graceful shutdown, and real-time log tailing. 2026-02-13 20:47:12 -03:00
Diego Rodrigues de Sa e Souza 4e5dc6a80b Merge pull request #12 from diegosouzapw/fix/select-dark-theme
fix(ui): fix Select dropdown dark theme inconsistency
2026-02-13 19:22:36 -03:00
diegosouzapw 0be2852af2 fix(ui): fix Select dropdown dark theme inconsistency
Use bg-surface (theme-aware) instead of bg-white for select and
option elements. Prevents white dropdown panels in dark mode.
2026-02-13 19:22:09 -03:00
Diego Rodrigues de Sa e Souza b3c36ed2ca Merge pull request #11 from diegosouzapw/chore/rebrand-ui-omniroute
chore(ui): rebrand to OmniRoute
2026-02-13 19:14:19 -03:00
diegosouzapw 2b1b8e4539 chore(ui): rebrand to OmniRoute
- Sidebar: 'Endpoint Proxy' → 'OmniRoute'
- Page title: 'OmniRoute — AI Gateway for Multi-Provider LLMs'
- Description updated across layout and config
2026-02-13 19:13:57 -03:00
Diego Rodrigues de Sa e Souza 8b7b50f6fb Merge pull request #10 from diegosouzapw/fix/cloud-sync-404-spam
fix(sync): disable cloud sync and truncate error logs
2026-02-13 19:01:53 -03:00
diegosouzapw 9c0ba39ed6 fix(sync): disable cloud sync and truncate error logs
CLOUD_URL was pointing to omniroute.com which doesn't have a /sync/
endpoint, causing repeated 404 HTML dumps in console logs.

- Clear CLOUD_URL to disable cloud sync until server is ready
- Truncate sync error text to 200 chars to prevent HTML spam in logs
2026-02-13 19:01:31 -03:00
Diego Rodrigues de Sa e Souza 801a050995 Merge pull request #9 from diegosouzapw/feat/enable-socks5-proxy
feat(proxy): enable SOCKS5 proxy support by default
2026-02-13 18:30:08 -03:00
diegosouzapw e7532d7189 feat(proxy): enable SOCKS5 proxy support by default
SOCKS5 was already fully implemented in both backend
(proxyDispatcher.js) and frontend (ProxyConfigModal.js).
Enable the feature flags to show SOCKS5 option in the UI.
2026-02-13 18:29:49 -03:00
Diego Rodrigues de Sa e Souza 7349052b33 Merge pull request #8 from diegosouzapw/fix/test-token-corruption
fix(oauth): prevent connection test from corrupting valid tokens
2026-02-13 18:09:52 -03:00
diegosouzapw 0af22a558f fix(oauth): prevent connection test from corrupting valid tokens
Only attempt token refresh on 401/403 during connection tests when
the token is actually expired (isTokenExpired). Previously, any 401/403
triggered an aggressive refresh that could overwrite valid tokens when
the upstream returned transient errors (rate-limiting, etc.).

Fixes Cline, Qwen, and iFlow losing authentication after tests.
2026-02-13 18:09:28 -03:00
Diego Rodrigues de Sa e Souza 3206583299 Merge pull request #7 from diegosouzapw/fix/iflow-secret-env-sync
chore(env): sync .env.example with current .env structure
2026-02-13 17:47:58 -03:00
diegosouzapw 575bda7375 chore(env): sync .env.example with current .env structure
Add missing Storage (SQLite) section, INSTANCE_NAME, and
align all sections with the actual .env file.
2026-02-13 17:47:38 -03:00
Diego Rodrigues de Sa e Souza b3cde151c3 Merge pull request #6 from diegosouzapw/fix/oauth-upsert-broadened
fix(oauth): broaden upsert to match any existing connection
2026-02-13 17:30:16 -03:00
diegosouzapw b7e757f3ba fix(oauth): broaden upsert to match any existing connection
Remove test_status restriction from the no-email upsert fallback.
Now matches any existing OAuth connection for the same provider
(not just failed ones), preventing duplicates regardless of status.
2026-02-13 17:29:51 -03:00
Diego Rodrigues de Sa e Souza 81a78e8733 Merge pull request #5 from diegosouzapw/fix/oauth-reauth-upsert
fix(oauth): prevent duplicate connections on re-authentication
2026-02-13 17:23:19 -03:00
diegosouzapw fbac38b2b5 fix(oauth): prevent duplicate connections on re-authentication
For providers that don't return email (e.g. Codex), re-authenticating
always created a new 'Account N' entry because the upsert check in
createProviderConnection only matched by email.

Added fallback: when no email is available, find the most recent
auth_failed/refresh_failed connection for the same provider and update
it instead of creating a duplicate.
2026-02-13 17:22:50 -03:00
Diego Rodrigues de Sa e Souza f5898d3898 Merge pull request #3 from diegosouzapw/dependabot/npm_and_yarn/development-55a9f41e4a
deps: bump eslint from 9.39.2 to 10.0.0 in the development group
2026-02-13 16:51:58 -03:00
Diego Rodrigues de Sa e Souza 75e9648d06 Merge pull request #2 from diegosouzapw/dependabot/github_actions/actions/github-script-8
build(deps): bump actions/github-script from 7 to 8
2026-02-13 16:51:25 -03:00
Diego Rodrigues de Sa e Souza 878277edc9 Merge pull request #4 from diegosouzapw/feature/versioning-and-model-import
feat(versioning): set initial version to 0.0.1 and restore dynamic model import
2026-02-13 16:50:59 -03:00
diegosouzapw 50fc931086 feat(versioning): set initial version to 0.0.1 and restore dynamic model import
- Set package.json version to 0.0.1 (initial OmniRoute release)
- Set open-sse/package.json version to 0.0.1
- Restore 'Import from /models' button for standard providers (openai, gemini, deepseek, etc.)
- Add handleImportModels function to ProviderDetailPage
2026-02-13 16:49:06 -03:00
dependabot[bot] 172461863e deps: bump eslint from 9.39.2 to 10.0.0 in the development group
Bumps the development group with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 9.39.2 to 10.0.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 19:31:36 +00:00
dependabot[bot] 79e10c9a41 build(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 19:30:25 +00:00
diegosouzapw 699329944e feat: initial OmniRoute release (rebranded from 9router)
This project is inspired by and originally forked from 9router by decolua
(https://github.com/decolua/9router).

Full rebrand: 9router → OmniRoute across all source code, configuration,
Docker, documentation, and assets.
2026-02-13 16:29:27 -03:00
629 changed files with 8317 additions and 50117 deletions
+54
View File
@@ -0,0 +1,54 @@
---
description: Git workflow — NEVER commit directly to main. Always use feature branches.
---
# Git Workflow
## ⚠️ CRITICAL RULE: NEVER commit directly to `main`
## Steps
1. **Before starting any work**, create a feature branch from `main`:
```bash
git checkout main && git pull origin main
git checkout -b feature/<feature-name>
```
2. **During development**, commit to the feature branch:
```bash
git add -A && git commit -m "<type>(<scope>): <description>"
```
3. **Before pushing**, verify the build passes:
```bash
npm run build
```
4. **When the feature is complete and verified**, push the branch and STOP:
```bash
git push origin feature/<feature-name>
```
5. **DO NOT** create a PR, merge, or push to `main`. Let the user handle that.
## Branch naming convention
- `feature/<name>` — new features
- `fix/<name>` — bugfixes
- `refactor/<name>` — refactoring
- `docker/<name>` — Docker / infrastructure changes
- `style/<name>` — UI / CSS changes
## Commit types
- `feat` — new feature
- `fix` — bugfix
- `refactor` — code refactoring
- `style` — UI / CSS changes
- `docker` — Docker / infrastructure
- `docs` — documentation
- `chore` — maintenance
+11 -63
View File
@@ -1,22 +1,14 @@
# OmniRoute environment contract
# This file reflects actual runtime usage in the current codebase.
# ═══════════════════════════════════════════════════
# REQUIRED SECRETS — Generate strong values!
# ═══════════════════════════════════════════════════
# Generate with: openssl rand -base64 48
JWT_SECRET=
# Generate with: openssl rand -hex 32
API_KEY_SECRET=
# Initial admin password — CHANGE THIS before first use!
INITIAL_PASSWORD=CHANGEME
# Required
JWT_SECRET=change-me-to-a-long-random-secret
INITIAL_PASSWORD=123456
DATA_DIR=/var/lib/omniroute
# Storage (SQLite)
STORAGE_DRIVER=sqlite
# Generate with: openssl rand -hex 32
STORAGE_ENCRYPTION_KEY=
STORAGE_ENCRYPTION_KEY=change-me-storage-encryption-key
STORAGE_ENCRYPTION_KEY_VERSION=v1
LOG_RETENTION_DAYS=90
SQLITE_MAX_SIZE_MB=2048
@@ -28,25 +20,18 @@ NODE_ENV=production
INSTANCE_NAME=omniroute
# Recommended security and ops variables
API_KEY_SECRET=endpoint-proxy-api-key-secret
MACHINE_ID_SALT=endpoint-proxy-salt
ENABLE_REQUEST_LOGS=false
AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false
# Input Sanitizer (FASE-01 — prompt injection & PII protection)
# INPUT_SANITIZER_ENABLED=true
# INPUT_SANITIZER_MODE=warn # warn | block | redact
# PII_REDACTION_ENABLED=false
# Cloud sync variables
# Must point to this running instance so internal sync jobs can call /api/sync/cloud.
# Server-side preferred variables:
BASE_URL=http://localhost:20128
CLOUD_URL=
# Backward-compatible/public variables:
# NEXT_PUBLIC_BASE_URL is also used as the OAuth redirect_uri origin when running behind a
# reverse proxy (e.g., nginx). Set this to your public-facing URL so OAuth callbacks work.
# Example: NEXT_PUBLIC_BASE_URL=https://omniroute.example.com
NEXT_PUBLIC_BASE_URL=http://localhost:20128
NEXT_PUBLIC_CLOUD_URL=
@@ -60,11 +45,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# ALL_PROXY=socks5://127.0.0.1:7890
# NO_PROXY=localhost,127.0.0.1
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google)
# Requires wreq-js to be installed (included in dependencies)
# ENABLE_TLS_FINGERPRINT=true
# Optional CLI runtime overrides (Docker/host integration)
# CLI_MODE=auto
# CLI_EXTRA_PATHS=/host-cli/bin
@@ -82,45 +62,17 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Provider OAuth Credentials (optional — override hardcoded defaults)
# These can also be set via data/provider-credentials.json
# CLAUDE_OAUTH_CLIENT_ID=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) — IMPORTANT FOR REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────
# The built-in Google OAuth credentials ONLY work when OmniRoute runs on
# localhost (127.0.0.1 / local network). They are registered with
# redirect_uri = http://localhost:PORT/callback and Google will reject any
# other redirect URI with: redirect_uri_mismatch.
#
# If you are hosting OmniRoute on a remote server (VPS, Docker, cloud), you
# MUST register your own Google Cloud OAuth 2.0 credentials:
#
# 1. Go to https://console.cloud.google.com/apis/credentials
# 2. Create an OAuth 2.0 Client ID (type: "Web application")
# 3. Add your server URL as Authorized redirect URI:
# https://your-server.com/callback
# 4. Copy the Client ID and Client Secret below.
#
# See the full tutorial in README.md → "OAuth em Servidor Remoto" section.
#
# Antigravity (Google Gemini Code Assist):
# ANTIGRAVITY_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
ANTIGRAVITY_OAUTH_CLIENT_SECRET=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
# Gemini CLI (Google AI):
# GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
# GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
# GEMINI_OAUTH_CLIENT_ID=
# GEMINI_OAUTH_CLIENT_SECRET=
# GEMINI_CLI_OAUTH_CLIENT_ID=
GEMINI_OAUTH_CLIENT_SECRET=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
GEMINI_CLI_OAUTH_CLIENT_SECRET=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
# ─────────────────────────────────────────────────────────────────────────────
# CLAUDE_OAUTH_CLIENT_ID=
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
# CODEX_OAUTH_CLIENT_ID=
# CODEX_OAUTH_CLIENT_SECRET=
# QWEN_OAUTH_CLIENT_ID=
# IFLOW_OAUTH_CLIENT_ID=
IFLOW_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
# IFLOW_OAUTH_CLIENT_SECRET=
# ANTIGRAVITY_OAUTH_CLIENT_ID=
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
# API Key Providers (Phase 1 + Phase 4)
# Add via Dashboard → Providers → Add API Key, or set here
@@ -149,7 +101,3 @@ IFLOW_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
# Logging
# LOG_LEVEL=info
# LOG_FORMAT=text
LOG_TO_FILE=true
# LOG_FILE_PATH=logs/application/app.log
# LOG_MAX_FILE_SIZE=50M
# LOG_RETENTION_DAYS=7
-4
View File
@@ -20,10 +20,6 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "next"
update-types: ["version-update:semver-major"]
- dependency-name: "eslint"
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
- package-ecosystem: "github-actions"
directory: "/"
-147
View File
@@ -1,147 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- name: Dependency audit
run: npm audit --audit-level=high --omit=dev
- name: Check for known vulnerabilities
run: npx is-my-node-vulnerable
continue-on-error: true
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run build
test-unit:
name: Unit Tests
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
node-version: [20, 22]
env:
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
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run test:unit
test-coverage:
name: Coverage
runs-on: ubuntu-latest
needs: build
env:
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
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:coverage
- name: Check coverage threshold
run: |
echo "Coverage report generated. Check output for threshold compliance."
test-e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: build
env:
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
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm run test:e2e
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: build
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
INITIAL_PASSWORD: ci-test-password-for-integration
DATA_DIR: /tmp/omniroute-ci
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:integration
continue-on-error: true
test-security:
name: Security Tests
runs-on: ubuntu-latest
needs: build
env:
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
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run test:security
continue-on-error: true
-55
View File
@@ -1,55 +0,0 @@
name: Publish to Docker Hub
on:
release:
types: [published]
permissions:
contents: read
jobs:
docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Extract version from release tag
id: version
run: |
VERSION="${GITHUB_REF_NAME}"
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing Docker image version: $VERSION"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
target: runner-base
push: true
tags: |
diegosouzapw/omniroute:${{ steps.version.outputs.version }}
diegosouzapw/omniroute:latest
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: diegosouzapw/omniroute
short-description: "OmniRoute — Unified AI proxy. Route any LLM through one endpoint."
readme-filepath: ./README.md
+2 -2
View File
@@ -14,10 +14,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
-23
View File
@@ -47,33 +47,20 @@ next-env.d.ts
data/
logs/*
# analysis directories (generated, not tracked)
.analysis/
antigravity-manager-analysis/
# docs (allow specific tracked files)
docs/*
!docs/ARCHITECTURE.md
!docs/CODEBASE_DOCUMENTATION.md
!docs/CONTRIBUTING.md
!docs/USER_GUIDE.md
!docs/API_REFERENCE.md
!docs/TROUBLESHOOTING.md
!docs/EXECUTION_CONTEXT_PROVIDER_SYNC.md
!docs/TASK_NEBIUS_BACKEND_ENABLEMENT.md
!docs/frontend-backend-provider-gap-report.md
!docs/openapi.yaml
!docs/PLANO-IMPLANTACAO.md
!docs/TASKS.md
!docs/FASE-*.md
!docs/adr/
!docs/cli-tools/
!docs/planning/
!docs/improvement-plans/
!docs/api/
!docs/VM_DEPLOYMENT_GUIDE.md
!docs/FEATURES.md
!docs/screenshots/
# open-sse tests
open-sse/test/*
@@ -85,13 +72,3 @@ open-sse/test/*
test-results/
playwright-report/
blob-report/
cloud/
omnirouteCloud/
omnirouteSite/
# Security Analysis (standalone project with own git)
security-analysis/
# Deploy workflow (contains sensitive VPS credentials)
.agent/workflows/deploy.md
clipr/
+17 -18
View File
@@ -8,7 +8,6 @@ Unified AI proxy/router — route any LLM through one endpoint. Multi-provider s
## Stack
- **Runtime**: Next.js 16 (App Router), Node.js, ES Modules
- **Language**: TypeScript 5.9 (`src/`) + JavaScript (`open-sse/`)
- **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/`
- **Streaming**: SSE via `open-sse` internal package
- **Styling**: Tailwind CSS v4
@@ -22,15 +21,15 @@ All persistence uses SQLite through domain-specific modules:
| Module | Responsibility |
| -------------- | ------------------------------------------ |
| `core.ts` | SQLite engine, migrations, WAL, encryption |
| `providers.ts` | Provider connections & nodes |
| `models.ts` | Model aliases, MITM aliases, custom models |
| `combos.ts` | Combo configurations |
| `apiKeys.ts` | API key management & validation |
| `settings.ts` | Settings, pricing, proxy config |
| `backup.ts` | Backup / restore operations |
| `core.js` | SQLite engine, migrations, WAL, encryption |
| `providers.js` | Provider connections & nodes |
| `models.js` | Model aliases, MITM aliases, custom models |
| `combos.js` | Combo configurations |
| `apiKeys.js` | API key management & validation |
| `settings.js` | Settings, pricing, proxy config |
| `backup.js` | Backup / restore operations |
`src/lib/localDb.ts` is a **re-export layer only** — all 27+ consumers import from it,
`src/lib/localDb.js` is a **re-export layer only** — all 27+ consumers import from it,
but the real logic lives in `src/lib/db/`.
### Request Pipeline (`open-sse/`)
@@ -50,25 +49,25 @@ Translation between provider formats: `open-sse/translator/`
### OAuth & Tokens (`src/lib/oauth/`)
18 modules handling OAuth flows, token refresh, and provider credentials.
Default credentials are hardcoded in `src/lib/oauth/constants/oauth.ts`,
Default credentials are hardcoded in `src/lib/oauth/constants/oauth.js`,
overridable via env vars or `data/provider-credentials.json`.
### Supporting Systems
| System | Location |
| -------------------------- | ------------------------------------------------- |
| Usage tracking & analytics | `src/lib/usageDb.ts`, `src/lib/usageAnalytics.ts` |
| Token health checks | `src/lib/tokenHealthCheck.ts` |
| Cloud sync | `src/lib/cloudSync.ts` |
| Proxy logging | `src/lib/proxyLogger.ts` |
| Data paths resolution | `src/lib/dataPaths.ts` |
| Usage tracking & analytics | `src/lib/usageDb.js`, `src/lib/usageAnalytics.js` |
| Token health checks | `src/lib/tokenHealthCheck.js` |
| Cloud sync | `src/lib/cloudSync.js` |
| Proxy logging | `src/lib/proxyLogger.js` |
| Data paths resolution | `src/lib/dataPaths.js` |
### Adding a New Provider
1. Register in `src/shared/constants/providers.ts`
1. Register in `src/shared/constants/providers.js`
2. Add executor in `open-sse/executors/`
3. Add translator rules in `open-sse/translator/` (if non-OpenAI format)
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` (if OAuth-based)
4. Add OAuth config in `src/lib/oauth/constants/oauth.js` (if OAuth-based)
## Review Focus
@@ -84,7 +83,7 @@ overridable via env vars or `data/provider-credentials.json`.
- DB operations go through `src/lib/db/` modules, never raw SQL in routes
- Provider requests flow through `open-sse/handlers/`
- Translations use `open-sse/translator/` modules
- `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module
- `localDb.js` is re-exports only — add new functions to the proper `db/*.js` module
### Code Quality
+103 -361
View File
@@ -2,409 +2,151 @@
All notable changes to OmniRoute are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
## [1.0.8] — 2026-02-21
## [Unreleased]
> ### 🔒 API Security & Windows Support
>
> Adds API Endpoint Protection for `/models`, Windows server startup fixes, and UI improvements.
### Added
### ✨ New Features
- Model selector with autocomplete in Chat Tester and Test Bench modes — prevents "model does not exist" errors by letting users choose available models (`c25230c`)
- OpenAPI specification at `docs/openapi.yaml` covering all 89 API endpoints (`7abda4d`)
- Enhanced `restart.sh` with clean build, health check, graceful shutdown (Ctrl+C), and real-time log tailing (`0db8f3d`)
- **API Endpoint Protection (`/models`)** — New Security Tab settings to optionally require an API key for the `/v1/models` endpoint (returns 404 when unauthorized) and to selectively block specific providers from appearing in the models list ([#100](https://github.com/diegosouzapw/OmniRoute/issues/100), [#96](https://github.com/diegosouzapw/OmniRoute/issues/96))
- **Interactive Provider UI** — Blocked Providers setting features an interactive chip selector with visual badges for all available AI providers
### Fixed
### 🐛 Bug Fixes
- **Windows Server Startup** — Fixed `ERR_INVALID_FILE_URL_PATH` crash on Windows by safely wrapping `import.meta.url` resolution with a fallback to `process.cwd()` for globally installed npm packages ([#98](https://github.com/diegosouzapw/OmniRoute/issues/98))
- **Combo buttons visibility** — Fixed layout overlap and tight spacing for the Quick Action buttons (Clone / Delete / Test) on the Combos page on narrower screens ([#95](https://github.com/diegosouzapw/OmniRoute/issues/95))
- Server port collision (EADDRINUSE) during restart — now kills port before `next start` (`e4c5c0c`)
---
## [1.0.7] — 2026-02-20
## [0.0.1] — 2026-02-13
> ### 🐛 Bugfix Release — OpenAI Compatibility, Custom Models & OAuth UX
>
> Fixes three community-reported issues: stream default now follows OpenAI spec, custom OpenAI-compatible providers appear in `/v1/models`, and Google OAuth shows a clear error + tutorial for remote deployments.
Initial public release of OmniRoute (rebranded from 9router).
### 🐛 Bug Fixes
### Added
- **`stream` defaults to `false`** — Aligns with the OpenAI specification which explicitly states `stream` defaults to `false`. Previously OmniRoute defaulted to `true`, causing SSE data to be returned instead of a JSON object, breaking clients like Spacebot, OpenCode, and standard Python/Rust/Go OpenAI SDKs that don't explicitly set `stream: true` ([#89](https://github.com/diegosouzapw/OmniRoute/issues/89))
- **Custom AI providers now appear in `/v1/models`** — OpenAI-compatible custom providers (e.g. FriendLI) whose provider ID wasn't in the built-in alias map were silently excluded from the models list even when active. Fixed by also checking the raw provider ID from the database against active connections ([#90](https://github.com/diegosouzapw/OmniRoute/issues/90))
- **OAuth `redirect_uri_mismatch` — improved UX for remote deployments** — Google OAuth providers (Antigravity, Gemini CLI) now always use `localhost` as redirect URI matching the registered credentials. Remote-access users see a targeted amber warning with a link to the new setup guide. The token exchange error message explains the root cause and guides users to configure their own credentials ([#91](https://github.com/diegosouzapw/OmniRoute/issues/91))
- **28 AI Providers** — OpenAI, Anthropic, Google Gemini, DeepSeek, Groq, xAI, Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius, GitHub Copilot, Cursor, Kiro, Kimi, MiniMax, iFlow, and more
- **OpenAI-compatible proxy** at `/api/v1/chat/completions` with automatic format translation, load balancing, and failover
- **Anthropic Messages API** at `/api/v1/messages` for Claude-native clients
- **OpenAI Responses API** at `/api/v1/responses` for modern OpenAI workflows
- **Embeddings API** at `/api/v1/embeddings` with 6 providers and 9 models
- **Image Generation API** at `/api/v1/images/generations` with 4 providers and 9 models
- **Format Translator** — automatic request/response conversion between OpenAI, Anthropic, Gemini, and OpenAI Responses formats
- **Translator Playground** with 4 modes: Playground, Chat Tester, Test Bench, Live Monitor
- **Combo Routing** — named route configurations with priority, weighted, and round-robin strategies
- **API Key Management** — create/revoke keys with usage attribution
- **Usage Dashboard** — analytics, call logs, request logger with API key filtering and cost tracking
- **Provider Health Diagnostics** — structured status (runtime errors, auth failures, token refresh) with per-connection retest
- **CLI Tools Integration** — runtime detection for Cline, Kiro, Droid, OpenClaw with backup/restore
- **OAuth Flows** — for Cursor, Kiro, Kimi, and GitHub Copilot
- **Docker Support** — multi-stage Dockerfile, docker-compose with 3 profiles (base, cli, host), production compose
- **SOCKS5 Proxy** — outbound proxy support enabled by default (`ab8d752`)
- **Unified Storage** — `DATA_DIR` / `XDG_CONFIG_HOME` resolution with auto-migration from `~/.omniroute`
- **In-app Documentation** at `/docs` with quick start, endpoint reference, and client compatibility notes
- **Dark Theme UI** — modern dashboard with glassmorphism, responsive layout
- `<think>` tag parser for reasoning models (DeepSeek, Qwen)
- Non-stream response translation for all formats
- Secure cookie handling for LAN/reverse-proxy deployments
### 📖 Documentation
### Fixed
- **OAuth em Servidor Remoto tutorial** — New README section with step-by-step guide to configure custom Google Cloud OAuth 2.0 credentials for remote/VPS/Docker deployments
- **`.env.example` Google OAuth block** — Added prominent warning block explaining remote credential requirements with direct links to Google Cloud Console
- OAuth re-authentication no longer creates duplicate connections (`773f117`, `510aedd`)
- Connection test no longer corrupts valid OAuth tokens (`a2ba189`)
- Cloud sync disabled to prevent 404 log spam (`71d132e`)
- `.env.example` synced with current environment structure (`6bdc74b`)
- Select dropdown dark theme inconsistency (`1bd734d`)
### 📁 Files Modified
### Dependencies
| File | Change |
| -------------------------------------- | ------------------------------------------------------------------------------------------- |
| `open-sse/handlers/chatCore.ts` | `stream` defaults to `false` (was `true`) per OpenAI spec |
| `src/app/api/v1/models/route.ts` | Added raw `providerId` check for custom models active-provider filter |
| `src/shared/components/OAuthModal.tsx` | Force `localhost` redirect for Google OAuth; improved `redirect_uri_mismatch` error message |
| `.env.example` | Added ⚠️ Google OAuth remote credentials block with step-by-step instructions |
| `README.md` | New "🔐 OAuth em Servidor Remoto" tutorial section |
- `actions/github-script` bumped from 7 to 8 (`f6a994a`)
- `eslint` bumped from 9.39.2 to 10.0.0 (`ecd4aea`)
---
## [1.0.6] — 2026-02-20
## Pre-Release History (9router)
> ### ✨ Provider & Combo Toggles — Strict Model Filtering
>
> `/v1/models` now shows only models from providers with active connections. Combos and providers can be toggled on/off directly from the dashboard.
> The following entries document the legacy 9router project before it was
> rebranded to OmniRoute. All changes below were included in the initial
> `0.0.1` release.
### ✨ New Features
### 0.2.75 — 2026-02-11
- **Provider toggle on Providers page** — Enable/disable all connections for a provider directly from the main Providers list. Toggle is always visible, no hover needed
- **Combo enable/disable toggle** — Each combo on the Combos page now has a toggle. Disabled combos are excluded from `/v1/models`
- **OAuth private IP support** — Expanded localhost detection to include private/LAN IPs (`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`) for correct OAuth redirect URIs
- API key attribution in usage/call logs with per-key analytics aggregates
- Usage dashboard API key observability (distribution donut, filterable table)
- In-app docs page (`/docs`) with quick start, endpoint reference, and client compatibility notes
- Unified storage path policy (`DATA_DIR``XDG_CONFIG_HOME``~/.omniroute`)
- Build-phase guard for `usageDb` (in-memory during `next build`)
- LAN/reverse-proxy cookie security detection
- Hardened Gemini 3 Flash normalization and non-stream SSE fallback parsing
- CLI tool runtime and OAuth refresh reliability improvements
- Provider health diagnostics with structured error types
### 🐛 Bug Fixes
### 0.2.74 — 2026-02-11
- **`/v1/models` strict filtering** — Models are now shown only from providers with active, enabled connections. Previously, if no connections existed or all were disabled, all 378+ models were shown as a fallback
- **Disabled provider models hidden** — Toggling off a provider immediately removes its models from `/v1/models`
- Model resolution fallback fix for unprefixed models
- GitHub Copilot dynamic endpoint selection (Codex → `/responses`)
- Non-stream translation path for OpenAI Responses
- Updated GitHub model catalog with compatibility aliases
---
### 0.2.73 — 2026-02-09
## [1.0.5] — 2026-02-20
- Expanded provider registry from 18 → 28 providers (DeepSeek, Groq, xAI, Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM)
- `/v1/embeddings` endpoint with 6 providers and 9 models
- `/v1/images/generations` endpoint with 4 providers and 9 models
- `<think>` tag parser for reasoning models
- Available Endpoints card on Endpoint page (127 chat, 9 embedding, 9 image models)
> ### 🐛 Hotfix — Model Filtering & Docker DATA_DIR
>
> Filters all model types in `/v1/models` by active providers and fixes Docker data directory mismatch.
### 0.2.72 — 2026-02-08
### 🐛 Bug Fixes
- Split Kimi into dual providers: `kimi` (OpenAI-compatible) and `kimi-coding` (Moonshot API)
- Hybrid CLI runtime support with Docker profiles (`runner-base`, `runner-cli`)
- Hardened cloud sync/auth flow with SSE fallback
- **`/v1/models` full filtering** — Embedding, image, rerank, audio, and moderation models are now filtered by active provider connections, matching chat model behavior. Providers like Together AI no longer appear without a configured API key (#88)
- **Docker `DATA_DIR`** — Added `ENV DATA_DIR=/app/data` to Dockerfile and `docker-compose.yml` ensuring the volume mount always matches the app data directory — prevents empty database on container recreation
### 0.2.66 — 2026-02-06
---
- Cursor provider end-to-end support with OAuth import flow
- `requireLogin` control and `hasPassword` state handling
- Usage/quota UX improvements
- Model support for custom providers
- Codex updates (GPT-5.3, thinking levels), Claude Opus 4.6, MiniMax Coding
- Auto-validation for provider API keys
## [1.0.4] — 2026-02-19
### 0.2.56 — 2026-02-04
> ### 🔧 Provider Filtering, OAuth Proxy Fix & Documentation
>
> Dashboard model filtering by active providers, provider enable/disable visual indicators, OAuth login fix for nginx reverse proxy, and LLM onboarding documentation.
- Anthropic-compatible provider support
- Provider icons across dashboard
- Enhanced usage tracking pipeline
### ✨ Features
### 0.2.52 — 2026-02-02
- **API Models filtering** — `GET /api/models` now returns only models from active providers; use `?all=true` for all models (#85)
- **Provider disabled indicator** — Provider cards show ⏸ "Disabled" badge with reduced opacity when all connections are inactive (#85)
- **`llm.txt`** — Comprehensive LLM onboarding file with project overview, architecture, flows, and conventions (#84)
- **WhatsApp Community** — Added WhatsApp group link to README badges and Support section
- Codex Cursor compatibility and Next.js 16 proxy migration
- OpenAI-compatible provider nodes (CRUD/validation/test)
- Token expiration and key-validity checks
- Non-streaming response translation for multiple formats
- Kiro OAuth wiring and token refresh support
### 🐛 Bug Fixes
### 0.2.43 — 2026-01-27
- **OAuth behind nginx** — Fixed OAuth login failing when behind a reverse proxy by using `window.location.origin` for redirect URI instead of hardcoded `localhost` (#86)
- **`NEXT_PUBLIC_BASE_URL` for OAuth** — Documented env var usage as redirect URI override for proxy deployments (#86)
- Fixed CLI tools model selection
- Fixed Kiro translator request handling
### 📁 Files Added
### 0.2.36 — 2026-01-19
| File | Purpose |
| --------- | -------------------------------------------------- |
| `llm.txt` | LLM and contributor onboarding (llms.txt standard) |
- Usage dashboard page
- Outbound proxy support in Open SSE fetch pipeline
- Fixed combo fallback behavior
### 📁 Files Modified
### 0.2.31 — 2026-01-18
| File | Change |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| `src/app/api/models/route.ts` | Filter by active providers, `?all=true` param, `available` field |
| `src/app/(dashboard)/dashboard/providers/page.tsx` | `allDisabled` detection + ⏸ badge + opacity-50 on provider cards |
| `src/shared/components/OAuthModal.tsx` | Proxy-aware redirect URI using `window.location.origin` |
| `.env.example` | Documented `NEXT_PUBLIC_BASE_URL` for OAuth behind proxy |
- Fixed Kiro token refresh and executor behavior
- Fixed Kiro request translation handling
---
### 0.2.27 — 2026-01-15
## [1.0.3] — 2026-02-19
- Added Kiro provider support with OAuth flow
- Fixed Codex provider behavior
> ### 📊 Logs Dashboard & Real-Time Console Viewer
>
> Unified logs interface with real-time console log viewer, file-based logging via console interception, and server initialization improvements.
### 0.2.21 — 2026-01-12
### ✨ Features
- **Logs Dashboard** — Consolidated 4-tab page at `/dashboard/logs` with Request Logs, Proxy Logs, Audit Logs, and Console tabs
- **Console Log Viewer** — Terminal-style real-time log viewer with color-coded log levels, auto-scroll, search/filtering, level filter, and 5-second polling
- **Console Interceptor** — Monkey-patches `console.log/info/warn/error/debug` at server start to capture all application output as JSON lines to `logs/application/app.log`
- **Log Rotation** — Size-based rotation and retention-based cleanup for log files
### 🔧 Improvements
- **Instrumentation consolidation** — Moved `initAuditLog()`, `cleanupExpiredLogs()`, and console interceptor initialization to Next.js `instrumentation.ts` (runs on both dev and prod server start)
- **Structured Logger file output** — `structuredLogger.ts` now also appends JSON log entries to the log file
- **Pino Logger fix** — Fixed broken mix of pino `transport` targets + manual `createWriteStream`; now uses `pino/file` transport targets exclusively with absolute paths
### 🗂️ Files Added
| File | Purpose |
| ---------------------------------------------------- | ----------------------------------------------------------------- |
| `src/app/(dashboard)/dashboard/logs/page.tsx` | Tabbed Logs Dashboard page |
| `src/app/(dashboard)/dashboard/logs/AuditLogTab.tsx` | Audit log tab component extracted from standalone page |
| `src/shared/components/ConsoleLogViewer.tsx` | Terminal-style real-time log viewer |
| `src/app/api/logs/console/route.ts` | API endpoint to read log file (filters last 1h, level, component) |
| `src/lib/consoleInterceptor.ts` | Console method monkey-patching for file capture |
| `src/lib/logRotation.ts` | Log rotation by size and cleanup by retention days |
### 🗂️ Files Modified
| File | Change |
| --------------------------------------- | ------------------------------------------------------------------------------- |
| `src/shared/components/Sidebar.tsx` | Nav: "Request Logs" → "Logs" with `description` icon |
| `src/shared/components/Breadcrumbs.tsx` | Added breadcrumb labels for `logs`, `audit-log`, `console` |
| `src/instrumentation.ts` | Added console interceptor + audit log init + expired log cleanup |
| `src/server-init.ts` | Added console interceptor import (backup init) |
| `src/shared/utils/logger.ts` | Fixed pino file transport using `pino/file` targets |
| `src/shared/utils/structuredLogger.ts` | Added `appendFileSync` file writing + log file config |
| `.env.example` | Added `LOG_TO_FILE`, `LOG_FILE_PATH`, `LOG_MAX_FILE_SIZE`, `LOG_RETENTION_DAYS` |
### ⚙️ Configuration
New environment variables:
| Variable | Default | Description |
| -------------------- | -------------------------- | ----------------------------- |
| `LOG_TO_FILE` | `true` | Enable/disable file logging |
| `LOG_FILE_PATH` | `logs/application/app.log` | Log file path |
| `LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation |
| `LOG_RETENTION_DAYS` | `7` | Days to retain old log files |
---
## [1.0.2] — 2026-02-18
> ### 🔒 Security Hardening, Architecture Improvements & UX Polish
>
> Comprehensive audit-driven improvements across security, architecture, testing, and user experience.
### 🛡️ Security (Phase 0)
- **Auth guard** — API route protection via `withAuth` middleware for all dashboard routes
- **CSRF protection** — Token-based CSRF guard for all state-changing API routes
- **Request payload validation** — Zod schemas for provider, combo, key, and settings endpoints
- **Prompt injection guard** — Input sanitization against malicious prompt patterns
- **Body size guard** — Route-specific body size limits with dedicated audio upload threshold
- **Rate limiter** — Per-IP rate limiting with configurable windows and thresholds
### 🏗️ Architecture (Phase 12)
- **DI container** — Simple dependency injection container for service registration
- **Policy engine** — Consolidated `PolicyEngine` for routing, security, and rate limiting
- **SQLite migration** — Database migration system with versioned migration runner
- **Graceful shutdown** — Clean server shutdown with connection draining
- **TypeScript fixes** — Resolved all `tsc` errors; removed redundant `@ts-check` directives
- **Pipeline decomposition** — `handleSingleModelChat` decomposed into composable pipeline stages
- **Prompt template versioning** — Version-tracked prompt templates with rollback support
- **Eval scheduling** — Automated evaluation suite scheduling with cron-based runner
- **Plugin architecture** — Extensible plugin system for custom middleware and handlers
### 🧪 Testing & CI (Phase 2)
- **Coverage thresholds** — Jest coverage thresholds enforced in CI (368 tests passing)
- **Proxy pipeline integration tests** — End-to-end tests for the proxy request pipeline
- **CI audit workflow** — npm audit and security scanning in GitHub Actions
- **k6 load tests** — Performance testing with ramping VUs and custom metrics
### ✨ UX & Polish (Phase 34)
- **Session management** — Session info card with login time, age, user agent, and logout
- **Focus indicators** — Global `:focus-visible` styles and `--focus-ring` CSS utility
- **Audit log viewer** — Security event audit log with structured data display
- **Dashboard cleanup** — Removed unused files, fixed Quick Start links to Endpoint page
- **Documentation** — Troubleshooting guide, deployment improvements
---
## [1.1.0] — 2026-02-18
> ### 🔧 API Compatibility & SDK Hardening
>
> Response sanitization, role normalization, and structured output improvements for strict OpenAI SDK compatibility and cross-provider robustness.
### 🛡️ Response Sanitization (NEW)
- **Response sanitizer module** — New `responseSanitizer.ts` strips non-standard fields (`x_groq`, `usage_breakdown`, `service_tier`, etc.) from all OpenAI-format provider responses, fixing OpenAI Python SDK v1.83+ Pydantic validation failures that returned raw strings instead of parsed `ChatCompletion` objects
- **Streaming chunk sanitization** — Passthrough streaming mode now sanitizes each SSE chunk in real-time via `sanitizeStreamingChunk()`, ensuring strict `chat.completion.chunk` schema compliance
- **ID/Object/Usage normalization** — Ensures `id`, `object`, `created`, `model`, `choices`, and `usage` fields always exist with correct types
- **Usage field cleanup** — Strips non-standard usage sub-fields, keeps only `prompt_tokens`, `completion_tokens`, `total_tokens`, and OpenAI detail fields
### 🧠 Think Tag Extraction (NEW)
- **`<think>` tag extraction** — Automatically extracts `<think>...</think>` blocks from thinking model responses (DeepSeek R1, Kimi K2 Thinking, etc.) into OpenAI's standard `reasoning_content` field
- **Streaming think-tag stripping** — Real-time `<think>` extraction in passthrough SSE stream, preventing JSON parsing errors in downstream tools
- **Preserves native reasoning** — Providers that already send `reasoning_content` natively (e.g., OpenAI o1) are not overwritten
### 🔄 Role Normalization (NEW)
- **`developer``system` conversion** — OpenAI's new `developer` role is automatically converted to `system` for all non-OpenAI providers (Claude, Gemini, Kiro, etc.)
- **`system``user` merging** — For models that reject the `system` role (GLM, ERNIE), system messages are intelligently merged into the first user message with clear delimiters
- **Model-aware normalization** — Uses model name prefix matching (`glm-*`, `ernie-*`) for compatibility decisions, avoiding hardcoded provider-level flags
### 📐 Structured Output for Gemini (NEW)
- **`response_format` → Gemini conversion** — OpenAI's `json_schema` structured output is now translated to Gemini's `responseMimeType` + `responseSchema` in the translator pipeline
- **`json_object` support** — `response_format: { type: "json_object" }` maps to Gemini's `application/json` MIME type
- **Schema cleanup** — Automatically removes unsupported JSON Schema keywords (`$schema`, `additionalProperties`) for Gemini compatibility
### 📁 Files Added
| File | Purpose |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| `open-sse/handlers/responseSanitizer.ts` | Response field stripping, think-tag extraction, ID/usage normalization |
| `open-sse/services/roleNormalizer.ts` | Developer→system, system→user role conversion pipeline |
### 📁 Files Modified
| File | Change |
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
| `open-sse/handlers/chatCore.ts` | Integrated response sanitizer for non-streaming OpenAI responses |
| `open-sse/utils/stream.ts` | Integrated streaming chunk sanitizer + think-tag extraction in passthrough mode |
| `open-sse/translator/index.ts` | Integrated role normalizer into the request translation pipeline |
| `open-sse/translator/request/openai-to-gemini.ts` | Added `response_format``responseMimeType`/`responseSchema` conversion |
---
## [1.0.0] — 2026-02-18
> ### 🎉 First Major Release — OmniRoute 1.0
>
> OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single OpenAI-compatible endpoint. This release represents the culmination of the entire development effort — from initial prototype to production-ready platform.
### 🧠 Core Routing & Intelligence
- **Smart 4-tier fallback** — Auto-routing: Subscription → Cheap → Free → Emergency
- **6 routing strategies** — Fill First, Round Robin, Power-of-Two-Choices, Random, Least Used, Cost Optimized
- **Semantic caching** — Auto-cache responses for deduplication with configurable TTL
- **Request idempotency** — Prevent duplicate processing of identical requests
- **Thinking budget validation** — Control reasoning token allocation per request
- **System prompt injection** — Configurable global system prompts for all requests
### 🔌 Providers & Models
- **20+ AI providers** — OpenAI, Claude (Anthropic), Gemini, GitHub Copilot, DeepSeek, Groq, xAI, Mistral, Qwen, iFlow, Kiro, OpenRouter, GLM, MiniMax, Kimi, NVIDIA NIM, and more
- **Multi-account support** — Multiple accounts per provider with automatic rotation
- **OAuth 2.0 (PKCE)** — Automatic token management and refresh for Claude Code, Codex, Gemini CLI, Copilot, Kiro
- **Auto token refresh** — Background refresh with expiry detection and unrecoverable error handling
- **Model import** — Import models from API-compatible passthrough providers
- **OpenAI-compatible validation** — Fallback validation via chat completions for providers without `/models` endpoint
- **TLS fingerprint spoofing** — Browser-like TLS fingerprinting via `wreq-js` to bypass bot detection
### 🔄 Format Translation
- **Multi-format translation** — Seamless OpenAI ↔ Claude ↔ Gemini ↔ OpenAI Responses API conversion
- **Translator Playground** — 4 interactive modes:
- **Playground** — Test format translations between any provider formats
- **Chat Tester** — Send real requests through the proxy with visual response rendering
- **Test Bench** — Automated batch testing across multiple providers
- **Live Monitor** — Real-time stream of active proxy requests and translations
### 🎯 Combos & Fallback Chains
- **Custom combos** — Create model combinations with multi-provider fallback chains
- **6 combo balancing strategies** — Fill First, Round Robin, Random, Least Used, P2C, Cost Optimized
- **Combo circuit breaker** — Auto-disable failing providers within a combo chain
### 🛡️ Resilience & Security
- **Circuit breakers** — Auto-recovery with configurable thresholds and cooldown periods
- **Exponential backoff** — Progressive retry delays for failed requests
- **Anti-thundering herd** — Mutex-based protection against concurrent retry storms
- **Rate limit detection** — Per-provider RPM, min gap, and max concurrent request tracking
- **Editable rate limits** — Configurable defaults via Settings → Resilience with persistence
- **Prompt injection guard** — Input sanitization for malicious prompt patterns
- **PII redaction** — Automatic detection and masking of personally identifiable information
- **AES-256-GCM encryption** — Credential encryption at rest
- **IP access control** — Whitelist/blacklist IP filtering
- **SOCKS5 proxy support** — Outbound proxy for upstream provider calls
### 📊 Observability & Analytics
- **Analytics dashboard** — Recharts-based SVG charts: stat cards, model usage bar chart, provider breakdown table with success rates and latency
- **Real-time health monitoring** — Provider health, rate limits, latency telemetry
- **Request logs** — Dedicated page with SQLite-persisted proxy request/response logs
- **Limits & Quotas** — Separate dashboard for quota monitoring with reset countdowns
- **Cost analytics** — Token cost tracking and budget management per provider
- **Request telemetry** — Correlation IDs, structured logging, request timing
### 💾 Database & Backup
- **Dual database** — LowDB (JSON) for config + SQLite for domain state and proxy logs
- **Export database** — `GET /api/db-backups/export` — Download SQLite database file
- **Export all** — `GET /api/db-backups/exportAll` — Full backup as `.tar.gz` archive (DB + settings + combos + providers + masked API keys)
- **Import database** — `POST /api/db-backups/import` — Upload and restore with validation, integrity check, and pre-import backup
- **Automatic backups** — Configurable backup schedule with retention
- **Storage health** — Dashboard widget with database size, path, and backup status
### 🖥️ Dashboard & UI
- **Full dashboard** — Provider management, analytics, health monitoring, settings, CLI tools
- **9 dashboard sections** — Providers, Combos, Analytics, Health, Translator, Settings, CLI Tools, Usage, Endpoint
- **Settings restructure** — 6 tabs: Security, Routing, Resilience, AI, System/Storage, Advanced
- **Shared UI component library** — Reusable components (Avatar, Badge, Button, Card, DataTable, Modal, etc.)
- **Dark/Light/System theme** — Persistent theme selection with system preference detection
- **Agent showcase grid** — Visual grid of 10 AI coding agents in README header
- **Provider logos** — Logo assets for all supported agents and providers
- **Red shield badges** — Styled badge icons across all documentation
### ☁️ Deployment & Infrastructure
- **Docker support** — Multi-stage Dockerfile with `base` and `cli` profiles
- **Docker Hub** — `diegosouzapw/omniroute` with `latest` and versioned tags
- **Docker CI/CD** — GitHub Actions auto-build and push on release
- **npm CLI package** — `npx omniroute` with auto-launch
- **npm CI/CD** — GitHub Actions auto-publish to npm on release
- **Akamai VM deployment** — Production deployment on Nanode 1GB with nginx reverse proxy
- **Cloud sync** — Sync configuration across devices via Cloudflare Worker
- **Edge compatibility** — Native `crypto.randomUUID()` for Cloudflare Workers
### 🧪 Testing & Quality
- **100% TypeScript** — Full migration of `src/` (200+ files) and `open-sse/` (94 files) — zero `@ts-ignore`, zero TypeScript errors
- **CI/CD pipeline** — GitHub Actions for lint, build, test, npm publish, Docker publish
- **Unit tests** — 20+ test suites covering domain logic, security, caching, routing
- **E2E tests** — Playwright specs for API, navigation, and responsive behavior
- **LLM evaluations** — Golden set testing framework with 4 match strategies (`exact`, `contains`, `regex`, `custom`)
- **Security tests** — CLI runtime, Docker hardening, cloud sync, and OpenAI compatibility
### 📖 Documentation
- **8 language READMEs** — English, Portuguese (pt-BR), Spanish, Russian, Chinese (zh-CN), German, French, Italian
- **VM Deployment Guide** — Complete guide (VM + Docker + nginx + Cloudflare + security)
- **Features Gallery** — 9 dashboard screenshots with descriptions
- **API Reference** — Full endpoint documentation including backup/export/import
- **User Guide** — Step-by-step setup, configuration, and usage instructions
- **Architecture docs** — System design, component decomposition, ADRs
- **OpenAPI specification** — Machine-readable API documentation
- **Troubleshooting guide** — Common issues and solutions
- **Security policy** — `SECURITY.md` with vulnerability reporting via GitHub Security Advisories
- **Roadmap** — 150+ planned features across 6 categories
### 🔌 API Endpoints
- `/v1/chat/completions` — OpenAI-compatible chat endpoint with format translation
- `/v1/embeddings` — Embedding generation
- `/v1/images/generations` — Image generation
- `/v1/models` — Model listing with provider filtering
- `/v1/rerank` — Re-ranking endpoint
- `/v1/audio/*` — Audio transcription and translation
- `/v1/moderations` — Content moderation
- `/api/db-backups/export` — Database export
- `/api/db-backups/exportAll` — Full archive export
- `/api/db-backups/import` — Database import with validation
- 30+ dashboard API routes for providers, combos, settings, analytics, health, CLI tools
---
[1.0.7]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.7
[1.0.6]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.6
[1.0.5]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.5
[1.0.4]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.4
[1.1.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.1.0
[1.0.3]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.3
[1.0.2]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.2
[1.0.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.0
- Initial README and project setup
-273
View File
@@ -1,273 +0,0 @@
# Contributing to OmniRoute
Thank you for your interest in contributing! This guide covers everything you need to get started.
---
## Development Setup
### Prerequisites
- **Node.js** 20+ (recommended: 22 LTS)
- **npm** 10+
- **Git**
### Clone & Install
```bash
git clone https://github.com/diegosouzapw/OmniRoute.git
cd OmniRoute
npm install
```
### Environment Variables
```bash
# Create your .env from the template
cp .env.example .env
# Generate required secrets
echo "JWT_SECRET=$(openssl rand -base64 48)" >> .env
echo "API_KEY_SECRET=$(openssl rand -hex 32)" >> .env
```
Key variables for development:
| Variable | Development Default | Description |
| ---------------------- | ----------------------- | ------------------------- |
| `PORT` | `3000` | Server port |
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Base URL for frontend |
| `JWT_SECRET` | (generate above) | JWT signing secret |
| `INITIAL_PASSWORD` | `123456` | First login password |
| `ENABLE_REQUEST_LOGS` | `false` | Enable debug request logs |
### Running Locally
```bash
# Development mode (hot reload)
npm run dev
# Production build
npm run build
npm run start
# Common port configuration
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
Default URLs:
- **Dashboard**: `http://localhost:3000/dashboard`
- **API**: `http://localhost:3000/v1`
---
## Git Workflow
> ⚠️ **NEVER commit directly to `main`.** Always use feature branches.
```bash
git checkout -b feat/your-feature-name
# ... make changes ...
git commit -m "feat: describe your change"
git push -u origin feat/your-feature-name
# Open a Pull Request on GitHub
```
### Branch Naming
| Prefix | Purpose |
| ----------- | ------------------------- |
| `feat/` | New features |
| `fix/` | Bug fixes |
| `refactor/` | Code restructuring |
| `docs/` | Documentation changes |
| `test/` | Test additions/fixes |
| `chore/` | Tooling, CI, dependencies |
### Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```
feat: add circuit breaker for provider calls
fix: resolve JWT secret validation edge case
docs: update SECURITY.md with PII protection
test: add observability unit tests
refactor(db): consolidate rate limit tables
```
Scopes: `db`, `sse`, `oauth`, `dashboard`, `api`, `cli`, `docker`, `ci`.
---
## Running Tests
```bash
# All unit tests
npm test
npm run test:unit
# Specific test suites
npm run test:security # Security tests
npm run test:fixes # Fix verification tests
# With coverage
npm run test:coverage
# E2E tests (requires Playwright)
npm run test:e2e
# Lint + format check
npm run lint
npm run check
```
Current test status: **368+ unit tests** covering:
- Provider translators and format conversion
- Rate limiting, circuit breaker, and resilience
- Semantic cache, idempotency, progress tracking
- Database operations and schema
- OAuth flows and authentication
- API endpoint validation
---
## Code Style
- **ESLint** — Run `npm run lint` before committing
- **Prettier** — Auto-formatted via `lint-staged` on commit
- **TypeScript** — All `src/` code uses `.ts`/`.tsx`; document with TSDoc (`@param`, `@returns`, `@throws`)
- **No `eval()`** — ESLint enforces `no-eval`, `no-implied-eval`, `no-new-func`
- **Zod validation** — Use Zod schemas for API input validation
---
## Project Structure
```
src/ # TypeScript (.ts / .tsx)
├── app/ # Next.js App Router
│ ├── (dashboard)/ # Dashboard pages (.tsx)
│ ├── api/ # API routes (.ts)
│ └── login/ # Auth pages (.tsx)
├── domain/ # Domain types and response helpers (.ts)
├── lib/ # Core business logic (.ts)
│ ├── db/ # SQLite database layer
│ ├── oauth/ # OAuth services per provider
│ ├── cacheLayer.ts # LRU cache
│ ├── semanticCache.ts # Semantic response cache
│ ├── idempotencyLayer.ts # Request deduplication
│ └── localDb.ts # Settings facade (LowDB for config, SQLite for domain data)
├── shared/
│ ├── components/ # React components (.tsx)
│ ├── middleware/ # Correlation IDs, etc.
│ ├── utils/ # Circuit breaker, sanitizer, etc.
│ └── validation/ # Zod schemas
└── sse/ # SSE chat handlers (.ts)
open-sse/ # @omniroute/open-sse workspace (JavaScript)
├── handlers/ # chatCore.js — main request handler
├── services/ # Rate limit, fallback
├── translators/ # Format converters (OpenAI ↔ Claude ↔ Gemini)
└── utils/ # Progress tracker, stream helpers
tests/
├── unit/ # Node.js test runner (.test.mjs)
└── e2e/ # Playwright tests
docs/ # Documentation
├── USER_GUIDE.md # Provider setup, CLI integration
├── API_REFERENCE.md # All endpoints
├── TROUBLESHOOTING.md # Common issues
├── ARCHITECTURE.md # System architecture
└── adr/ # Architecture Decision Records
```
---
## Adding a New Provider
### Step 1: OAuth Service (if using OAuth)
Create `src/lib/oauth/services/your-provider.ts` extending `OAuthService`:
```typescript
import { OAuthService } from "../OAuthService";
export class YourProviderService extends OAuthService {
constructor() {
super({
name: "your-provider",
authUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
clientId: "...",
scopes: ["..."],
});
}
}
```
### Step 2: Register Provider
Add to `src/lib/oauth/providers.ts`:
```typescript
import { YourProviderService } from "./services/your-provider";
// Add to the providers map
```
### Step 3: Add Constants
Add provider constants in `src/lib/providerConstants.ts`:
- Provider prefix (e.g., `yp/`)
- Default models
- Pricing info
### Step 4: Add Translator (if non-OpenAI format)
Create translator in `open-sse/translators/` if the provider uses a custom API format.
### Step 5: Add Timeout
Add request timeout configuration in `src/shared/utils/requestTimeout.ts`.
### Step 6: Add Tests
Write unit tests in `tests/unit/` covering at minimum:
- Provider registration
- Request/response translation
- Error handling
---
## Pull Request Checklist
- [ ] Tests pass (`npm test`)
- [ ] Linting passes (`npm run lint`)
- [ ] Build succeeds (`npm run build`)
- [ ] TypeScript types added for new public functions and interfaces
- [ ] No hardcoded secrets or fallback values
- [ ] CHANGELOG updated (if user-facing change)
- [ ] Documentation updated (if applicable)
---
## Releasing
When a new GitHub Release is created (e.g. `v0.4.0`), the package is **automatically published to npm** via GitHub Actions:
```bash
gh release create v0.4.0 --title "v0.4.0" --generate-notes
```
---
## Getting Help
- **Architecture**: See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **ADRs**: See `docs/adr/` for architectural decision records
+1 -3
View File
@@ -12,7 +12,6 @@ WORKDIR /app
LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint" \
org.opencontainers.image.url="https://omniroute.online" \
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
org.opencontainers.image.licenses="MIT"
@@ -20,8 +19,7 @@ ENV NODE_ENV=production
ENV PORT=20128
ENV HOSTNAME=0.0.0.0
# Data directory inside Docker — must match the volume mount in docker-compose.yml
ENV DATA_DIR=/app/data
# Runtime writable location for localDb when DATA_DIR is configured to /app/data
RUN mkdir -p /app/data
COPY --from=builder /app/public ./public
-1000
View File
File diff suppressed because it is too large Load Diff
-999
View File
@@ -1,999 +0,0 @@
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
# 🚀 OmniRoute — El Gateway de IA Gratuito
### Nunca dejes de programar. Enrutamiento inteligente hacia **modelos de IA GRATUITOS y económicos** con fallback automático.
_Tu proxy de API universal — un endpoint, 36+ proveedores, cero tiempo de inactividad._
**Chat Completions • Embeddings • Generación de Imágenes • Audio • Reranking • 100% TypeScript**
---
### 🤖 Proveedor de IA Gratuito para tus agentes de programación favoritos
_Conecta cualquier IDE o herramienta CLI con IA a través de OmniRoute — gateway de API gratuito para programación ilimitada._
<table>
<tr>
<td align="center" width="110">
<a href="https://github.com/cline/cline">
<img src="./public/providers/openclaw.png" alt="OpenClaw" width="48"/><br/>
<b>OpenClaw</b>
</a><br/>
<sub>⭐ 205K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/HKUDS/nanobot">
<img src="./public/providers/nanobot.png" alt="NanoBot" width="48"/><br/>
<b>NanoBot</b>
</a><br/>
<sub>⭐ 20.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/sipeed/picoclaw">
<img src="./public/providers/picoclaw.jpg" alt="PicoClaw" width="48"/><br/>
<b>PicoClaw</b>
</a><br/>
<sub>⭐ 14.6K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/zeroclaw-labs/zeroclaw">
<img src="./public/providers/zeroclaw.png" alt="ZeroClaw" width="48"/><br/>
<b>ZeroClaw</b>
</a><br/>
<sub>⭐ 9.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/nearai/ironclaw">
<img src="./public/providers/ironclaw.png" alt="IronClaw" width="48"/><br/>
<b>IronClaw</b>
</a><br/>
<sub>⭐ 2.1K</sub>
</td>
</tr>
<tr>
<td align="center" width="110">
<a href="https://github.com/anomalyco/opencode">
<img src="./public/providers/opencode.svg" alt="OpenCode" width="48"/><br/>
<b>OpenCode</b>
</a><br/>
<sub>⭐ 106K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/openai/codex">
<img src="./public/providers/codex.png" alt="Codex CLI" width="48"/><br/>
<b>Codex CLI</b>
</a><br/>
<sub>⭐ 60.8K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/anthropics/claude-code">
<img src="./public/providers/claude.png" alt="Claude Code" width="48"/><br/>
<b>Claude Code</b>
</a><br/>
<sub>⭐ 67.3K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/google-gemini/gemini-cli">
<img src="./public/providers/gemini-cli.png" alt="Gemini CLI" width="48"/><br/>
<b>Gemini CLI</b>
</a><br/>
<sub>⭐ 94.7K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/Kilo-Org/kilocode">
<img src="./public/providers/kilocode.png" alt="Kilo Code" width="48"/><br/>
<b>Kilo Code</b>
</a><br/>
<sub>⭐ 15.5K</sub>
</td>
</tr>
</table>
<sub>📡 Todos los agentes se conectan vía <code>http://localhost:20128/v1</code> o <code>http://cloud.omniroute.online/v1</code> — una configuración, modelos y cuota ilimitados</sub>
---
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License](https://img.shields.io/github/license/diegosouzapw/OmniRoute)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[🌐 Website](https://omniroute.online) • [🚀 Inicio Rápido](#-inicio-rápido) • [💡 Características](#-características-principales) • [📖 Docs](#-documentación) • [💰 Precios](#-precios-resumidos)
🌐 **Disponible en:** [English](README.md) | [Português](README.pt-BR.md) | [Español](README.es.md) | [Русский](README.ru.md) | [中文](README.zh-CN.md) | [Deutsch](README.de.md) | [Français](README.fr.md) | [Italiano](README.it.md)
</div>
---
## 🤔 ¿Por qué OmniRoute?
**Deja de desperdiciar dinero y chocar con límites:**
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> La cuota de suscripción expira sin usar cada mes
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Los límites de tasa te detienen en medio de la programación
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> APIs caras ($20-50/mes por proveedor)
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Cambiar manualmente entre proveedores
**OmniRoute resuelve esto:**
-**Maximiza suscripciones** - Rastrea cuotas, usa cada bit antes del reset
-**Fallback automático** - Suscripción → API Key → Barato → Gratuito, cero tiempo de inactividad
-**Multi-cuenta** - Round-robin entre cuentas por proveedor
-**Universal** - Funciona con Claude Code, Codex, Gemini CLI, Cursor, Cline, OpenClaw, cualquier herramienta CLI
---
## 🔄 Cómo Funciona
```
┌─────────────┐
│ Tu CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...)
│ Tool │
└──────┬──────┘
│ http://localhost:20128/v1
┌─────────────────────────────────────────┐
│ OmniRoute (Enrutador Inteligente) │
│ • Traducción de formato (OpenAI ↔ Claude) │
│ • Rastreo de cuota + Embeddings + Imágenes │
│ • Renovación automática de tokens │
└──────┬──────────────────────────────────┘
├─→ [Tier 1: SUSCRIPCIÓN] Claude Code, Codex, Gemini CLI
│ ↓ cuota agotada
├─→ [Tier 2: API KEY] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM, etc.
│ ↓ límite de presupuesto
├─→ [Tier 3: BARATO] GLM ($0.6/1M), MiniMax ($0.2/1M)
│ ↓ límite de presupuesto
└─→ [Tier 4: GRATUITO] iFlow, Qwen, Kiro (ilimitado)
Resultado: Nunca dejes de programar, costo mínimo
```
---
## ⚡ Inicio Rápido
**1. Instala globalmente:**
```bash
npm install -g omniroute
omniroute
```
🎉 El Dashboard se abre en `http://localhost:20128`
| Comando | Descripción |
| ----------------------- | ---------------------------------------------- |
| `omniroute` | Iniciar servidor (puerto predeterminado 20128) |
| `omniroute --port 3000` | Usar puerto personalizado |
| `omniroute --no-open` | No abrir navegador automáticamente |
| `omniroute --help` | Mostrar ayuda |
**2. Conecta un proveedor GRATUITO:**
Dashboard → Proveedores → Conectar **Claude Code** o **Antigravity** → Login OAuth → ¡Listo!
**3. Usa en tu herramienta CLI:**
```
Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Configuración:
Endpoint: http://localhost:20128/v1
API Key: [copiar del dashboard]
Model: if/kimi-k2-thinking
```
**¡Eso es todo!** Comienza a programar con modelos de IA GRATUITOS.
**Alternativa — ejecutar desde código fuente:**
```bash
cp .env.example .env
npm install
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
---
## 🐳 Docker
OmniRoute está disponible como imagen Docker pública en [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute).
**Ejecución rápida:**
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**Con archivo de entorno:**
```bash
# Copia y edita el .env primero
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**Usando Docker Compose:**
```bash
# Perfil base (sin herramientas CLI)
docker compose --profile base up -d
# Perfil CLI (Claude Code, Codex, OpenClaw integrados)
docker compose --profile cli up -d
```
| Imagen | Tag | Tamaño | Descripción |
| ------------------------ | -------- | ------ | ---------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Última versión estable |
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Versión actual |
---
## 💰 Precios Resumidos
| Tier | Proveedor | Costo | Reset de Cuota | Mejor Para |
| ------------------ | ----------------- | ---------------------------- | ----------------- | ----------------------- |
| **💳 SUSCRIPCIÓN** | Claude Code (Pro) | $20/mes | 5h + semanal | Ya suscrito |
| | Codex (Plus/Pro) | $20-200/mes | 5h + semanal | Usuarios OpenAI |
| | Gemini CLI | **GRATUITO** | 180K/mes + 1K/día | ¡Todos! |
| | GitHub Copilot | $10-19/mes | Mensual | Usuarios GitHub |
| **🔑 API KEY** | NVIDIA NIM | **GRATUITO** (1000 créditos) | Único | Pruebas gratuitas |
| | DeepSeek | Por uso | Ninguno | Mejor precio/calidad |
| | Groq | Tier gratuito + pago | Limitado | Inferencia ultra-rápida |
| | xAI (Grok) | Por uso | Ninguno | Modelos Grok |
| | Mistral | Tier gratuito + pago | Limitado | IA Europea |
| | OpenRouter | Por uso | Ninguno | 100+ modelos |
| **💰 BARATO** | GLM-4.7 | $0.6/1M | Diario 10h | Respaldo económico |
| | MiniMax M2.1 | $0.2/1M | Rotativo 5h | Opción más barata |
| | Kimi K2 | $9/mes fijo | 10M tokens/mes | Costo predecible |
| **🆓 GRATUITO** | iFlow | $0 | Ilimitado | 8 modelos gratuitos |
| | Qwen | $0 | Ilimitado | 3 modelos gratuitos |
| | Kiro | $0 | Ilimitado | Claude gratuito |
**💡 Consejo Pro:** ¡Comienza con Gemini CLI (180K gratis/mes) + iFlow (ilimitado gratis) = $0 de costo!
---
## 🎯 Casos de Uso
### Caso 1: "Tengo suscripción Claude Pro"
**Problema:** La cuota expira sin usar, límites de tasa durante programación intensa
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (usar suscripción al máximo)
2. glm/glm-4.7 (respaldo barato cuando la cuota se agota)
3. if/kimi-k2-thinking (fallback de emergencia gratuito)
Costo mensual: $20 (suscripción) + ~$5 (respaldo) = $25 total
vs. $20 + chocar con límites = frustración
```
### Caso 2: "Quiero costo cero"
**Problema:** No puede pagar suscripciones, necesita IA confiable para programar
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K gratis/mes)
2. if/kimi-k2-thinking (ilimitado gratis)
3. qw/qwen3-coder-plus (ilimitado gratis)
Costo mensual: $0
Calidad: Modelos listos para producción
```
### Caso 3: "Necesito programar 24/7, sin interrupciones"
**Problema:** Plazos ajustados, no puede permitirse tiempo de inactividad
```
Combo: "always-on"
1. cc/claude-opus-4-6 (mejor calidad)
2. cx/gpt-5.2-codex (segunda suscripción)
3. glm/glm-4.7 (barato, reset diario)
4. minimax/MiniMax-M2.1 (más barato, reset 5h)
5. if/kimi-k2-thinking (gratuito ilimitado)
Resultado: 5 capas de fallback = cero tiempo de inactividad
```
### Caso 4: "Quiero IA GRATUITA en OpenClaw"
**Problema:** Necesita asistente de IA en apps de mensajería, completamente gratuito
```
Combo: "openclaw-free"
1. if/glm-4.7 (ilimitado gratis)
2. if/minimax-m2.1 (ilimitado gratis)
3. if/kimi-k2-thinking (ilimitado gratis)
Costo mensual: $0
Acceso vía: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Características Principales
### 🧠 Enrutamiento e Inteligencia
| Característica | Qué Hace |
| -------------------------------------- | ------------------------------------------------------------------------------- |
| 🎯 **Fallback Inteligente 4 Tiers** | Auto-enrutamiento: Suscripción → API Key → Barato → Gratuito |
| 📊 **Rastreo de Cuota en Tiempo Real** | Conteo de tokens en vivo + countdown de reset por proveedor |
| 🔄 **Traducción de Formato** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro transparente |
| 👥 **Soporte Multi-Cuenta** | Múltiples cuentas por proveedor con selección inteligente |
| 🔄 **Renovación Automática de Token** | Tokens OAuth se renuevan automáticamente con reintentos |
| 🎨 **Combos Personalizados** | 6 estrategias: fill-first, round-robin, p2c, random, least-used, cost-optimized |
| 🧩 **Modelos Personalizados** | Agrega cualquier ID de modelo a cualquier proveedor |
| 🌐 **Enrutador Wildcard** | Enruta patrones `provider/*` a cualquier proveedor dinámicamente |
| 🧠 **Presupuesto de Razonamiento** | Modos passthrough, auto, custom y adaptativo para modelos de razonamiento |
| 💬 **Inyección de System Prompt** | System prompt global aplicado en todas las solicitudes |
| 📄 **API Responses** | Soporte completo de la API Responses de OpenAI (`/v1/responses`) para Codex |
### 🎵 APIs Multi-Modal
| Característica | Qué Hace |
| ----------------------------- | ------------------------------------------------------ |
| 🖼️ **Generación de Imágenes** | `/v1/images/generations` — 4 proveedores, 9+ modelos |
| 📐 **Embeddings** | `/v1/embeddings` — 6 proveedores, 9+ modelos |
| 🎤 **Transcripción de Audio** | `/v1/audio/transcriptions` — Compatible con Whisper |
| 🔊 **Texto a Voz** | `/v1/audio/speech` — Síntesis de audio multi-proveedor |
| 🛡️ **Moderaciones** | `/v1/moderations` — Verificaciones de seguridad |
| 🔀 **Reranking** | `/v1/rerank` — Reranking de relevancia de documentos |
### 🛡️ Resiliencia y Seguridad
| Característica | Qué Hace |
| ---------------------------------- | ---------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Auto-apertura/cierre por proveedor con umbrales configurables |
| 🛡️ **Anti-Thundering Herd** | Mutex + semáforo rate-limit para proveedores con API key |
| 🧠 **Caché Semántico** | Caché de dos niveles (firma + semántico) reduce costo y latencia |
| ⚡ **Idempotencia de Solicitud** | Ventana de dedup de 5s para solicitudes duplicadas |
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detección de bot vía TLS con wreq-js |
| 🌐 **Filtrado de IP** | Allowlist/blocklist para control de acceso a la API |
| 📊 **Rate Limits Editables** | RPM, gap mínimo y concurrencia máxima configurables |
### 📊 Observabilidad y Analytics
| Característica | Qué Hace |
| ------------------------------ | --------------------------------------------------------------------- |
| 📝 **Logs de Solicitud** | Modo debug con logs completos de request/response |
| 💾 **Logs SQLite** | Logs de proxy persistentes sobreviven a reinicios |
| 📊 **Dashboard de Analytics** | Recharts: cards de estadísticas, gráfico de uso, tabla de proveedores |
| 📈 **Rastreo de Progreso** | Eventos de progreso SSE opt-in para streaming |
| 🧪 **Evaluaciones de LLM** | Pruebas con conjunto golden y 4 estrategias de match |
| 🔍 **Telemetría de Solicitud** | Agregación de latencia p50/p95/p99 + rastreo X-Request-Id |
| 📋 **Logs + Cuotas** | Páginas dedicadas para navegación de logs y rastreo de cuotas |
| 🏥 **Dashboard de Salud** | Uptime, estados de circuit breaker, lockouts, stats de caché |
| 💰 **Rastreo de Costos** | Gestión de presupuesto + configuración de precios por modelo |
### ☁️ Deploy y Sincronización
| Característica | Qué Hace |
| --------------------------------- | ------------------------------------------------------------------------------- |
| 💾 **Cloud Sync** | Sincroniza configuraciones entre dispositivos vía Cloudflare Workers |
| 🌐 **Deploy en Cualquier Lugar** | Localhost, VPS, Docker, Cloudflare Workers |
| 🔑 **Gestión de API Keys** | Genera, rota y define alcance de API keys por proveedor |
| 🧙 **Asistente de Configuración** | Setup guiado en 4 pasos para nuevos usuarios |
| 🔧 **Dashboard CLI Tools** | Configuración en un clic para Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
| 🔄 **Backups de DB** | Backup y restauración automáticos de todas las configuraciones |
<details>
<summary><b>📖 Detalles de Características</b></summary>
### 🎯 Fallback Inteligente 4 Tiers
Crea combos con fallback automático:
```
Combo: "my-coding-stack"
1. cc/claude-opus-4-6 (tu suscripción)
2. nvidia/llama-3.3-70b (API NVIDIA gratuita)
3. glm/glm-4.7 (respaldo barato, $0.6/1M)
4. if/kimi-k2-thinking (fallback gratuito)
→ Cambia automáticamente cuando la cuota se agota o ocurren errores
```
### 📊 Rastreo de Cuota en Tiempo Real
- Consumo de tokens por proveedor
- Countdown de reset (5 horas, diario, semanal)
- Estimación de costo para tiers pagos
- Reportes de gastos mensuales
### 🔄 Traducción de Formato
Traducción transparente entre formatos:
- **OpenAI** ↔ **Claude****Gemini****OpenAI Responses**
- Tu herramienta CLI envía formato OpenAI → OmniRoute traduce → El proveedor recibe formato nativo
- Funciona con cualquier herramienta que soporte endpoints OpenAI personalizados
### 👥 Soporte Multi-Cuenta
- Agrega múltiples cuentas por proveedor
- Round-robin automático o enrutamiento por prioridad
- Fallback a la siguiente cuenta cuando una alcanza la cuota
### 🔄 Renovación Automática de Token
- Los tokens OAuth se renuevan automáticamente antes de expirar
- Sin necesidad de re-autenticación manual
- Experiencia transparente en todos los proveedores
### 🎨 Combos Personalizados
- Crea combinaciones ilimitadas de modelos
- 6 estrategias: fill-first, round-robin, power-of-two-choices, random, least-used, cost-optimized
- Comparte combos entre dispositivos con Cloud Sync
### 🏥 Dashboard de Salud
- Estado del sistema (uptime, versión, uso de memoria)
- Estados de circuit breaker por proveedor (Closed/Open/Half-Open)
- Estado de rate limit y lockouts activos
- Estadísticas de caché de firma
- Telemetría de latencia (p50/p95/p99) + caché de prompt
- Reset de salud con un clic
### 🔧 Playground del Traductor
- Debug, prueba y visualiza traducciones de formato de API
- Envía solicitudes y ve cómo OmniRoute traduce entre formatos de proveedores
- Invaluable para troubleshooting de problemas de integración
### 💾 Cloud Sync
- Sincroniza proveedores, combos y configuraciones entre dispositivos
- Sincronización automática en segundo plano
- Almacenamiento cifrado seguro
</details>
---
## 📖 Guía de Configuración
<details>
<summary><b>💳 Proveedores por Suscripción</b></summary>
### Claude Code (Pro/Max)
```bash
Dashboard → Proveedores → Conectar Claude Code
→ Login OAuth → Renovación automática de token
→ Rastreo de cuota 5h + semanal
Modelos:
cc/claude-opus-4-6
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
```
**Consejo Pro:** Usa Opus para tareas complejas, Sonnet para velocidad. ¡OmniRoute rastrea cuota por modelo!
### OpenAI Codex (Plus/Pro)
```bash
Dashboard → Proveedores → Conectar Codex
→ Login OAuth (puerto 1455)
→ Reset 5h + semanal
Modelos:
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
```
### Gemini CLI (¡GRATUITO 180K/mes!)
```bash
Dashboard → Proveedores → Conectar Gemini CLI
→ Google OAuth
→ 180K completions/mes + 1K/día
Modelos:
gc/gemini-3-flash-preview
gc/gemini-2.5-pro
```
**Mejor Valor:** ¡Tier gratuito enorme! Úsalo antes de los tiers pagos.
### GitHub Copilot
```bash
Dashboard → Proveedores → Conectar GitHub
→ OAuth vía GitHub
→ Reset mensual (1ro del mes)
Modelos:
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3-pro
```
</details>
<details>
<summary><b>🔑 Proveedores por API Key</b></summary>
### NVIDIA NIM (¡GRATUITO 1000 créditos!)
1. Regístrate: [build.nvidia.com](https://build.nvidia.com)
2. Obtén API key gratuita (1000 créditos de inferencia incluidos)
3. Dashboard → Agregar Proveedor → NVIDIA NIM:
- API Key: `nvapi-your-key`
**Modelos:** `nvidia/llama-3.3-70b-instruct`, `nvidia/mistral-7b-instruct`, y 50+ más
**Consejo Pro:** ¡API compatible con OpenAI — funciona perfectamente con la traducción de formato de OmniRoute!
### DeepSeek
1. Regístrate: [platform.deepseek.com](https://platform.deepseek.com)
2. Obtén API key
3. Dashboard → Agregar Proveedor → DeepSeek
**Modelos:** `deepseek/deepseek-chat`, `deepseek/deepseek-coder`
### Groq (¡Tier Gratuito Disponible!)
1. Regístrate: [console.groq.com](https://console.groq.com)
2. Obtén API key (tier gratuito incluido)
3. Dashboard → Agregar Proveedor → Groq
**Modelos:** `groq/llama-3.3-70b`, `groq/mixtral-8x7b`
**Consejo Pro:** ¡Inferencia ultra-rápida — mejor para programación en tiempo real!
### OpenRouter (100+ Modelos)
1. Regístrate: [openrouter.ai](https://openrouter.ai)
2. Obtén API key
3. Dashboard → Agregar Proveedor → OpenRouter
**Modelos:** Accede a 100+ modelos de todos los principales proveedores a través de una única API key.
</details>
<details>
<summary><b>💰 Proveedores Baratos (Respaldo)</b></summary>
### GLM-4.7 (Reset diario, $0.6/1M)
1. Regístrate: [Zhipu AI](https://open.bigmodel.cn/)
2. Obtén API key del Plan Coding
3. Dashboard → Agregar API Key:
- Proveedor: `glm`
- API Key: `your-key`
**Usa:** `glm/glm-4.7`
**Consejo Pro:** ¡El Plan Coding ofrece 3× cuota a 1/7 del costo! Reset diario 10:00 AM.
### MiniMax M2.1 (Reset 5h, $0.20/1M)
1. Regístrate: [MiniMax](https://www.minimax.io/)
2. Obtén API key
3. Dashboard → Agregar API Key
**Usa:** `minimax/MiniMax-M2.1`
**Consejo Pro:** ¡Opción más barata para contexto largo (1M tokens)!
### Kimi K2 ($9/mes fijo)
1. Suscríbete: [Moonshot AI](https://platform.moonshot.ai/)
2. Obtén API key
3. Dashboard → Agregar API Key
**Usa:** `kimi/kimi-latest`
**Consejo Pro:** ¡$9/mes fijo por 10M tokens = $0.90/1M de costo efectivo!
</details>
<details>
<summary><b>🆓 Proveedores GRATUITOS (Respaldo de Emergencia)</b></summary>
### iFlow (8 modelos GRATUITOS)
```bash
Dashboard → Conectar iFlow
→ Login OAuth iFlow
→ Uso ilimitado
Modelos:
if/kimi-k2-thinking
if/qwen3-coder-plus
if/glm-4.7
if/minimax-m2
if/deepseek-r1
```
### Qwen (3 modelos GRATUITOS)
```bash
Dashboard → Conectar Qwen
→ Autorización por código de dispositivo
→ Uso ilimitado
Modelos:
qw/qwen3-coder-plus
qw/qwen3-coder-flash
```
### Kiro (Claude GRATUITO)
```bash
Dashboard → Conectar Kiro
→ AWS Builder ID o Google/GitHub
→ Uso ilimitado
Modelos:
kr/claude-sonnet-4.5
kr/claude-haiku-4.5
```
</details>
<details>
<summary><b>🎨 Crear Combos</b></summary>
### Ejemplo 1: Maximizar Suscripción → Respaldo Barato
```
Dashboard → Combos → Crear Nuevo
Nombre: premium-coding
Modelos:
1. cc/claude-opus-4-6 (Suscripción primaria)
2. glm/glm-4.7 (Respaldo barato, $0.6/1M)
3. minimax/MiniMax-M2.1 (Fallback más barato, $0.20/1M)
Usa en CLI: premium-coding
```
### Ejemplo 2: Solo Gratuito (Costo Cero)
```
Nombre: free-combo
Modelos:
1. gc/gemini-3-flash-preview (180K gratis/mes)
2. if/kimi-k2-thinking (ilimitado)
3. qw/qwen3-coder-plus (ilimitado)
Costo: ¡$0 para siempre!
```
</details>
<details>
<summary><b>🔧 Integración CLI</b></summary>
### Cursor IDE
```
Configuración → Modelos → Avanzado:
OpenAI API Base URL: http://localhost:20128/v1
OpenAI API Key: [del dashboard OmniRoute]
Model: cc/claude-opus-4-6
```
### Claude Code
Usa la página **CLI Tools** en el dashboard para configuración en un clic, o edita `~/.claude/settings.json` manualmente.
### Codex CLI
```bash
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
```
### OpenClaw
**Opción 1 — Dashboard (recomendado):**
```
Dashboard → CLI Tools → OpenClaw → Seleccionar Modelo → Aplicar
```
**Opción 2 — Manual:** Edita `~/.openclaw/openclaw.json`:
```json
{
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://127.0.0.1:20128/v1",
"apiKey": "sk_omniroute",
"api": "openai-completions"
}
}
}
}
```
> **Nota:** OpenClaw solo funciona con OmniRoute local. Usa `127.0.0.1` en lugar de `localhost` para evitar problemas de resolución IPv6.
### Cline / Continue / RooCode
```
Configuración → Configuración de API:
Proveedor: OpenAI Compatible
Base URL: http://localhost:20128/v1
API Key: [del dashboard OmniRoute]
Model: if/kimi-k2-thinking
```
</details>
---
## 📊 Modelos Disponibles
<details>
<summary><b>Ver todos los modelos disponibles</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUITO:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Créditos GRATUITOS:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ más modelos en [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUITO:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUITO:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUITO:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modelos:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Cualquier modelo de [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Evaluaciones (Evals)
OmniRoute incluye un framework de evaluación integrado para probar la calidad de respuestas de LLM contra un conjunto golden. Accede vía **Analytics → Evals** en el dashboard.
### Conjunto Golden Integrado
El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
- Saludos, matemáticas, geografía, generación de código
- Conformidad de formato JSON, traducción, markdown
- Rechazo de seguridad (contenido dañino), conteo, lógica booleana
### Estrategias de Evaluación
| Estrategia | Descripción | Ejemplo |
| ---------- | ---------------------------------------------------- | -------------------------------- |
| `exact` | La salida debe coincidir exactamente | `"4"` |
| `contains` | La salida debe contener subcadena (case-insensitive) | `"Paris"` |
| `regex` | La salida debe coincidir con el patrón regex | `"1.*2.*3"` |
| `custom` | Función JS personalizada retorna true/false | `(output) => output.length > 10` |
---
## 🐛 Solución de Problemas
<details>
<summary><b>Haz clic para expandir la guía de solución de problemas</b></summary>
**"Language model did not provide messages"**
- Cuota del proveedor agotada → Verifica el rastreador de cuota en el dashboard
- Solución: Usa combo con fallback o cambia a tier más barato
**Rate limiting**
- Cuota de suscripción agotada → Fallback a GLM/MiniMax
- Agrega combo: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**Token OAuth expirado**
- Renovado automáticamente por OmniRoute
- Si persiste: Dashboard → Proveedor → Reconectar
**Costos altos**
- Verifica estadísticas de uso en Dashboard → Costos
- Cambia modelo primario a GLM/MiniMax
- Usa tier gratuito (Gemini CLI, iFlow) para tareas no críticas
**Dashboard se abre en el puerto equivocado**
- Establece `PORT=20128` y `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
**Errores de cloud sync**
- Verifica que `BASE_URL` apunte a tu instancia en ejecución
- Verifica que `CLOUD_URL` apunte a tu endpoint cloud esperado
- Mantén los valores `NEXT_PUBLIC_*` alineados con los valores del servidor
**Primer login no funciona**
- Verifica `INITIAL_PASSWORD` en `.env`
- Si no está definido, la contraseña predeterminada es `123456`
**Sin logs de solicitud**
- Establece `ENABLE_REQUEST_LOGS=true` en `.env`
**Prueba de conexión muestra "Invalid" para proveedores compatibles con OpenAI**
- Muchos proveedores no exponen el endpoint `/models`
- OmniRoute v1.0.6+ incluye validación vía chat completions como fallback
- Asegúrate de que la URL base incluya el sufijo `/v1`
</details>
---
## 🛠️ Stack Tecnológico
- **Runtime**: Node.js 20+
- **Lenguaje**: TypeScript 5.9 — **100% TypeScript** en `src/` y `open-sse/` (v1.0.6)
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
- **Base de Datos**: LowDB (JSON) + SQLite (estado del dominio + logs de proxy)
- **Streaming**: Server-Sent Events (SSE)
- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys
- **Testing**: Node.js test runner (368+ tests unitarios)
- **CI/CD**: GitHub Actions (publicación automática npm + Docker Hub en release)
- **Website**: [omniroute.online](https://omniroute.online)
- **Paquete**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
- **Resiliencia**: Circuit breaker, backoff exponencial, anti-thundering herd, spoofing TLS
---
## 📖 Documentación
| Documento | Descripción |
| ------------------------------------------------ | -------------------------------------------------- |
| [Guía del Usuario](docs/USER_GUIDE.md) | Proveedores, combos, integración CLI, deploy |
| [Referencia de API](docs/API_REFERENCE.md) | Todos los endpoints con ejemplos |
| [Solución de Problemas](docs/TROUBLESHOOTING.md) | Problemas comunes y soluciones |
| [Arquitectura](docs/ARCHITECTURE.md) | Arquitectura del sistema e internos |
| [Contribuir](CONTRIBUTING.md) | Setup de desarrollo y directrices |
| [Spec OpenAPI](docs/openapi.yaml) | Especificación OpenAPI 3.0 |
| [Política de Seguridad](SECURITY.md) | Reportar vulnerabilidades y prácticas de seguridad |
---
## 📧 Soporte
> 💬 **¡Únete a la comunidad!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtén ayuda, comparte consejos y mantente al día.
- **Website**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Grupo de la Comunidad](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Proyecto Original**: [9router por decolua](https://github.com/decolua/9router)
---
## 👥 Contribuidores
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
### Cómo Contribuir
1. Haz fork del repositorio
2. Crea tu rama de funcionalidad (`git checkout -b feature/amazing-feature`)
3. Haz commit de tus cambios (`git commit -m 'Add amazing feature'`)
4. Haz push a la rama (`git push origin feature/amazing-feature`)
5. Abre un Pull Request
Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para directrices detalladas.
### Lanzar una Nueva Versión
```bash
# Crea un release — la publicación en npm ocurre automáticamente
gh release create v1.0.6 --title "v1.0.6" --generate-notes
```
---
## 📊 Historial de Stars
<a href="https://star-history.com/#diegosouzapw/OmniRoute&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
</picture>
</a>
---
## 🙏 Agradecimientos
Agradecimiento especial a **[9router](https://github.com/decolua/9router)** por **[decolua](https://github.com/decolua)** — el proyecto original que inspiró este fork. OmniRoute se construye sobre esa increíble base con características adicionales, APIs multi-modal y una reescritura completa en TypeScript.
Agradecimiento especial a **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — la implementación original en Go que inspiró esta adaptación a JavaScript.
---
## 📄 Licencia
Licencia MIT - consulta [LICENSE](LICENSE) para detalles.
---
<div align="center">
<sub>Hecho con ❤️ para desarrolladores que programan 24/7</sub>
<br/>
<sub><a href="https://omniroute.online">omniroute.online</a></sub>
</div>
-999
View File
@@ -1,999 +0,0 @@
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
# 🚀 OmniRoute — La Passerelle IA Gratuite
### N'arrêtez jamais de coder. Routage intelligent vers des **modèles IA GRATUITS et économiques** avec fallback automatique.
_Votre proxy API universel — un endpoint, 36+ fournisseurs, zéro temps d'arrêt._
**Chat Completions • Embeddings • Génération d'images • Audio • Reranking • 100% TypeScript**
---
### 🤖 Fournisseur IA gratuit pour vos agents de programmation préférés
_Connectez n'importe quel IDE ou outil CLI alimenté par l'IA via OmniRoute — passerelle API gratuite pour un codage illimité._
<table>
<tr>
<td align="center" width="110">
<a href="https://github.com/cline/cline">
<img src="./public/providers/openclaw.png" alt="OpenClaw" width="48"/><br/>
<b>OpenClaw</b>
</a><br/>
<sub>⭐ 205K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/HKUDS/nanobot">
<img src="./public/providers/nanobot.png" alt="NanoBot" width="48"/><br/>
<b>NanoBot</b>
</a><br/>
<sub>⭐ 20.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/sipeed/picoclaw">
<img src="./public/providers/picoclaw.jpg" alt="PicoClaw" width="48"/><br/>
<b>PicoClaw</b>
</a><br/>
<sub>⭐ 14.6K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/zeroclaw-labs/zeroclaw">
<img src="./public/providers/zeroclaw.png" alt="ZeroClaw" width="48"/><br/>
<b>ZeroClaw</b>
</a><br/>
<sub>⭐ 9.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/nearai/ironclaw">
<img src="./public/providers/ironclaw.png" alt="IronClaw" width="48"/><br/>
<b>IronClaw</b>
</a><br/>
<sub>⭐ 2.1K</sub>
</td>
</tr>
<tr>
<td align="center" width="110">
<a href="https://github.com/anomalyco/opencode">
<img src="./public/providers/opencode.svg" alt="OpenCode" width="48"/><br/>
<b>OpenCode</b>
</a><br/>
<sub>⭐ 106K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/openai/codex">
<img src="./public/providers/codex.png" alt="Codex CLI" width="48"/><br/>
<b>Codex CLI</b>
</a><br/>
<sub>⭐ 60.8K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/anthropics/claude-code">
<img src="./public/providers/claude.png" alt="Claude Code" width="48"/><br/>
<b>Claude Code</b>
</a><br/>
<sub>⭐ 67.3K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/google-gemini/gemini-cli">
<img src="./public/providers/gemini-cli.png" alt="Gemini CLI" width="48"/><br/>
<b>Gemini CLI</b>
</a><br/>
<sub>⭐ 94.7K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/Kilo-Org/kilocode">
<img src="./public/providers/kilocode.png" alt="Kilo Code" width="48"/><br/>
<b>Kilo Code</b>
</a><br/>
<sub>⭐ 15.5K</sub>
</td>
</tr>
</table>
<sub>📡 Tous les agents se connectent via <code>http://localhost:20128/v1</code> ou <code>http://cloud.omniroute.online/v1</code> — une configuration, modèles et quota illimités</sub>
---
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License](https://img.shields.io/github/license/diegosouzapw/OmniRoute)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[🌐 Site web](https://omniroute.online) • [🚀 Démarrage rapide](#-démarrage-rapide) • [💡 Fonctionnalités](#-fonctionnalités-principales) • [📖 Docs](#-documentation) • [💰 Tarifs](#-aperçu-des-tarifs)
🌐 **Disponible en :** [English](README.md) | [Português](README.pt-BR.md) | [Español](README.es.md) | [Русский](README.ru.md) | [中文](README.zh-CN.md) | [Deutsch](README.de.md) | [Français](README.fr.md) | [Italiano](README.it.md)
</div>
---
## 🤔 Pourquoi OmniRoute ?
**Arrêtez de gaspiller de l'argent et de vous heurter aux limites :**
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Le quota d'abonnement expire inutilisé chaque mois
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Les limites de débit vous arrêtent en plein codage
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> APIs coûteuses (20-50 $/mois par fournisseur)
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Changement manuel entre fournisseurs
**OmniRoute résout ces problèmes :**
-**Maximisez les abonnements** — Suivez les quotas, utilisez chaque bit avant la réinitialisation
-**Fallback automatique** — Abonnement → Clé API → Économique → Gratuit, zéro temps d'arrêt
-**Multi-comptes** — Round-robin entre les comptes par fournisseur
-**Universel** — Fonctionne avec Claude Code, Codex, Gemini CLI, Cursor, Cline, OpenClaw, tout outil CLI
---
## 🔄 Comment ça fonctionne
```
┌─────────────┐
│ Votre CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...)
│ Tool │
└──────┬──────┘
│ http://localhost:20128/v1
┌─────────────────────────────────────────┐
│ OmniRoute (Routeur intelligent) │
│ • Traduction de format (OpenAI ↔ Claude) │
│ • Suivi des quotas + Embeddings + Images │
│ • Renouvellement automatique des tokens │
└──────┬──────────────────────────────────┘
├─→ [Tier 1: ABONNEMENT] Claude Code, Codex, Gemini CLI
│ ↓ quota épuisé
├─→ [Tier 2: CLÉ API] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM, etc.
│ ↓ limite de budget
├─→ [Tier 3: ÉCONOMIQUE] GLM ($0.6/1M), MiniMax ($0.2/1M)
│ ↓ limite de budget
└─→ [Tier 4: GRATUIT] iFlow, Qwen, Kiro (illimité)
Résultat : Ne jamais arrêter de coder, coût minimal
```
---
## ⚡ Démarrage rapide
**1. Installer globalement :**
```bash
npm install -g omniroute
omniroute
```
🎉 Le tableau de bord s'ouvre sur `http://localhost:20128`
| Commande | Description |
| ----------------------- | ------------------------------------------- |
| `omniroute` | Démarrer le serveur (port par défaut 20128) |
| `omniroute --port 3000` | Utiliser un port personnalisé |
| `omniroute --no-open` | Ne pas ouvrir le navigateur automatiquement |
| `omniroute --help` | Afficher l'aide |
**2. Connecter un fournisseur GRATUIT :**
Tableau de bord → Fournisseurs → Connecter **Claude Code** ou **Antigravity** → Connexion OAuth → Terminé !
**3. Utiliser dans votre outil CLI :**
```
Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Paramètres :
Endpoint : http://localhost:20128/v1
API Key : [copier depuis le tableau de bord]
Model : if/kimi-k2-thinking
```
**C'est tout !** Commencez à coder avec des modèles IA GRATUITS.
**Alternative — exécuter depuis le code source :**
```bash
cp .env.example .env
npm install
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
---
## 🐳 Docker
OmniRoute est disponible en tant qu'image Docker publique sur [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute).
**Démarrage rapide :**
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**Avec fichier d'environnement :**
```bash
# Copier et modifier le .env d'abord
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**Avec Docker Compose :**
```bash
# Profil de base (sans outils CLI)
docker compose --profile base up -d
# Profil CLI (Claude Code, Codex, OpenClaw intégrés)
docker compose --profile cli up -d
```
| Image | Tag | Taille | Description |
| ------------------------ | -------- | ------ | ----------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Dernière version stable |
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Version actuelle |
---
## 💰 Aperçu des tarifs
| Tier | Fournisseur | Coût | Réinitialisation | Idéal pour |
| ----------------- | ----------------- | -------------------------- | ------------------- | ----------------------------- |
| **💳 ABONNEMENT** | Claude Code (Pro) | 20 $/mois | 5h + hebdomadaire | Déjà abonné |
| | Codex (Plus/Pro) | 20-200 $/mois | 5h + hebdomadaire | Utilisateurs OpenAI |
| | Gemini CLI | **GRATUIT** | 180K/mois + 1K/jour | Tout le monde ! |
| | GitHub Copilot | 10-19 $/mois | Mensuel | Utilisateurs GitHub |
| **🔑 CLÉ API** | NVIDIA NIM | **GRATUIT** (1000 crédits) | Unique | Tests gratuits |
| | DeepSeek | À l'usage | Aucune | Meilleur rapport qualité-prix |
| | Groq | Niveau gratuit + payant | Limité | Inférence ultra-rapide |
| | xAI (Grok) | À l'usage | Aucune | Modèles Grok |
| | Mistral | Niveau gratuit + payant | Limité | IA européenne |
| | OpenRouter | À l'usage | Aucune | 100+ modèles |
| **💰 ÉCONOMIQUE** | GLM-4.7 | 0,6 $/1M | Quotidien 10h | Backup économique |
| | MiniMax M2.1 | 0,2 $/1M | Rotatif 5h | Option la moins chère |
| | Kimi K2 | 9 $/mois fixe | 10M tokens/mois | Coût prévisible |
| **🆓 GRATUIT** | iFlow | 0 $ | Illimité | 8 modèles gratuits |
| | Qwen | 0 $ | Illimité | 3 modèles gratuits |
| | Kiro | 0 $ | Illimité | Claude gratuit |
**💡 Conseil Pro :** Commencez avec Gemini CLI (180K gratuits/mois) + iFlow (illimité gratuit) = 0 $ de coût !
---
## 🎯 Cas d'utilisation
### Cas 1 : « J'ai un abonnement Claude Pro »
**Problème :** Le quota expire inutilisé, limites de débit pendant le codage intensif
```
Combo : "maximize-claude"
1. cc/claude-opus-4-6 (utiliser l'abonnement au maximum)
2. glm/glm-4.7 (backup économique quand le quota est épuisé)
3. if/kimi-k2-thinking (fallback d'urgence gratuit)
Coût mensuel : 20 $ (abonnement) + ~5 $ (backup) = 25 $ au total
vs. 20 $ + atteindre les limites = frustration
```
### Cas 2 : « Je veux zéro coût »
**Problème :** Impossible de payer des abonnements, besoin d'IA fiable pour coder
```
Combo : "free-forever"
1. gc/gemini-3-flash (180K gratuits/mois)
2. if/kimi-k2-thinking (illimité gratuit)
3. qw/qwen3-coder-plus (illimité gratuit)
Coût mensuel : 0 $
Qualité : Modèles prêts pour la production
```
### Cas 3 : « Je dois coder 24/7, sans interruption »
**Problème :** Délais serrés, ne peut pas se permettre de temps d'arrêt
```
Combo : "always-on"
1. cc/claude-opus-4-6 (meilleure qualité)
2. cx/gpt-5.2-codex (deuxième abonnement)
3. glm/glm-4.7 (économique, reset quotidien)
4. minimax/MiniMax-M2.1 (le moins cher, reset 5h)
5. if/kimi-k2-thinking (gratuit illimité)
Résultat : 5 niveaux de fallback = zéro temps d'arrêt
```
### Cas 4 : « Je veux l'IA GRATUITE dans OpenClaw »
**Problème :** Besoin d'assistant IA dans les apps de messagerie, entièrement gratuit
```
Combo : "openclaw-free"
1. if/glm-4.7 (illimité gratuit)
2. if/minimax-m2.1 (illimité gratuit)
3. if/kimi-k2-thinking (illimité gratuit)
Coût mensuel : 0 $
Accès via : WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Fonctionnalités principales
### 🧠 Routage & Intelligence
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------------- | ------------------------------------------------------------------------------- |
| 🎯 **Fallback intelligent 4 niveaux** | Auto-routage : Abonnement → Clé API → Économique → Gratuit |
| 📊 **Suivi des quotas en temps réel** | Comptage de tokens en direct + compte à rebours de réinitialisation |
| 🔄 **Traduction de format** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro transparent |
| 👥 **Support multi-comptes** | Plusieurs comptes par fournisseur avec sélection intelligente |
| 🔄 **Renouvellement auto des tokens** | Les tokens OAuth se renouvellent automatiquement avec retry |
| 🎨 **Combos personnalisés** | 6 stratégies : fill-first, round-robin, p2c, random, least-used, cost-optimized |
| 🧩 **Modèles personnalisés** | Ajoutez n'importe quel ID de modèle à n'importe quel fournisseur |
| 🌐 **Routeur wildcard** | Routez les patterns `provider/*` vers n'importe quel fournisseur dynamiquement |
| 🧠 **Budget de raisonnement** | Modes passthrough, auto, custom et adaptive pour les modèles de raisonnement |
| 💬 **Injection System Prompt** | System prompt global appliqué à toutes les requêtes |
| 📄 **API Responses** | Support complet de l'API Responses d'OpenAI (`/v1/responses`) pour Codex |
### 🎵 APIs multi-modales
| Fonctionnalité | Ce qu'elle fait |
| -------------------------- | ------------------------------------------------------- |
| 🖼️ **Génération d'images** | `/v1/images/generations` — 4 fournisseurs, 9+ modèles |
| 📐 **Embeddings** | `/v1/embeddings` — 6 fournisseurs, 9+ modèles |
| 🎤 **Transcription audio** | `/v1/audio/transcriptions` — compatible Whisper |
| 🔊 **Texte vers parole** | `/v1/audio/speech` — synthèse audio multi-fournisseur |
| 🛡️ **Modérations** | `/v1/moderations` — vérifications de sécurité |
| 🔀 **Reranking** | `/v1/rerank` — reclassement de pertinence des documents |
### 🛡️ Résilience & Sécurité
| Fonctionnalité | Ce qu'elle fait |
| ------------------------------- | -------------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Ouverture/fermeture auto par fournisseur avec seuils configurables |
| 🛡️ **Anti-Thundering Herd** | Mutex + sémaphore de rate-limit pour les fournisseurs avec clé API |
| 🧠 **Cache sémantique** | Cache à deux niveaux (signature + sémantique) réduit coût et latence |
| ⚡ **Idempotence des requêtes** | Fenêtre de dédup 5s pour les requêtes dupliquées |
| 🔒 **Spoofing TLS Fingerprint** | Contournement de détection de bot via wreq-js |
| 🌐 **Filtrage IP** | Allowlist/blocklist pour le contrôle d'accès API |
| 📊 **Rate limits éditables** | RPM configurable, intervalle minimum, concurrence max |
### 📊 Observabilité & Analytique
| Fonctionnalité | Ce qu'elle fait |
| --------------------------------- | ------------------------------------------------------------------------- |
| 📝 **Logs de requêtes** | Mode debug avec logs complets requête/réponse |
| 💾 **Logs SQLite** | Logs proxy persistants survivant aux redémarrages |
| 📊 **Tableau de bord analytique** | Recharts : cartes de stats, graphique d'utilisation, tableau fournisseurs |
| 📈 **Suivi de progression** | Événements SSE de progression opt-in pour le streaming |
| 🧪 **Évaluations LLM** | Tests avec golden set et 4 stratégies de correspondance |
| 🔍 **Télémétrie des requêtes** | Agrégation de latence p50/p95/p99 + traçage X-Request-Id |
| 📋 **Logs + Quotas** | Pages dédiées pour navigation des logs et suivi des quotas |
| 🏥 **Tableau de bord santé** | Uptime, états circuit breaker, lockouts, stats cache |
| 💰 **Suivi des coûts** | Gestion de budget + configuration des prix par modèle |
### ☁️ Déploiement & Synchronisation
| Fonctionnalité | Ce qu'elle fait |
| --------------------------------- | ------------------------------------------------------------------------------- |
| 💾 **Cloud Sync** | Synchroniser les paramètres entre appareils via Cloudflare Workers |
| 🌐 **Déployer partout** | Localhost, VPS, Docker, Cloudflare Workers |
| 🔑 **Gestion des clés API** | Générer, faire tourner et limiter les clés API par fournisseur |
| 🧙 **Assistant de configuration** | Setup guidé en 4 étapes pour les nouveaux utilisateurs |
| 🔧 **Tableau de bord CLI Tools** | Configuration en un clic pour Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
| 🔄 **Sauvegardes DB** | Sauvegarde et restauration automatiques de tous les paramètres |
<details>
<summary><b>📖 Détails des fonctionnalités</b></summary>
### 🎯 Fallback intelligent 4 niveaux
Créez des combos avec fallback automatique :
```
Combo : "my-coding-stack"
1. cc/claude-opus-4-6 (votre abonnement)
2. nvidia/llama-3.3-70b (API NVIDIA gratuite)
3. glm/glm-4.7 (backup économique, $0.6/1M)
4. if/kimi-k2-thinking (fallback gratuit)
→ Bascule automatiquement lorsque le quota est épuisé ou en cas d'erreurs
```
### 📊 Suivi des quotas en temps réel
- Consommation de tokens par fournisseur
- Compte à rebours de réinitialisation (5 heures, quotidien, hebdomadaire)
- Estimation des coûts pour les niveaux payants
- Rapports de dépenses mensuels
### 🔄 Traduction de format
Traduction transparente entre les formats :
- **OpenAI** ↔ **Claude****Gemini****OpenAI Responses**
- Votre CLI envoie le format OpenAI → OmniRoute traduit → Le fournisseur reçoit le format natif
- Fonctionne avec tout outil supportant les endpoints OpenAI personnalisés
### 👥 Support multi-comptes
- Ajouter plusieurs comptes par fournisseur
- Round-robin automatique ou routage par priorité
- Basculement vers le compte suivant lorsqu'un quota est atteint
### 🔄 Renouvellement automatique des tokens
- Les tokens OAuth se renouvellent automatiquement avant expiration
- Pas de réauthentification manuelle nécessaire
- Expérience transparente sur tous les fournisseurs
### 🎨 Combos personnalisés
- Créer des combinaisons de modèles illimitées
- 6 stratégies : fill-first, round-robin, power-of-two-choices, random, least-used, cost-optimized
- Partager les combos entre appareils avec Cloud Sync
### 🏥 Tableau de bord santé
- Statut du système (uptime, version, utilisation mémoire)
- États des circuit breakers par fournisseur (Closed/Open/Half-Open)
- Statut des rate limits et lockouts actifs
- Statistiques du cache de signatures
- Télémétrie de latence (p50/p95/p99) + cache de prompt
- Réinitialisation de la santé en un clic
### 🔧 Playground du traducteur
- Déboguer, tester et visualiser les traductions de format d'API
- Envoyer des requêtes et voir comment OmniRoute traduit entre les formats des fournisseurs
- Inestimable pour résoudre les problèmes d'intégration
### 💾 Cloud Sync
- Synchroniser fournisseurs, combos et paramètres entre appareils
- Synchronisation en arrière-plan automatique
- Stockage chiffré sécurisé
</details>
---
## 📖 Guide de configuration
<details>
<summary><b>💳 Fournisseurs par abonnement</b></summary>
### Claude Code (Pro/Max)
```bash
Tableau de bord → Fournisseurs → Connecter Claude Code
→ Connexion OAuth → Renouvellement auto des tokens
→ Suivi de quota 5h + hebdomadaire
Modèles :
cc/claude-opus-4-6
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
```
**Conseil Pro :** Utilisez Opus pour les tâches complexes, Sonnet pour la vitesse. OmniRoute suit les quotas par modèle !
### OpenAI Codex (Plus/Pro)
```bash
Tableau de bord → Fournisseurs → Connecter Codex
→ Connexion OAuth (port 1455)
→ Reset 5h + hebdomadaire
Modèles :
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
```
### Gemini CLI (GRATUIT 180K/mois !)
```bash
Tableau de bord → Fournisseurs → Connecter Gemini CLI
→ Google OAuth
→ 180K completions/mois + 1K/jour
Modèles :
gc/gemini-3-flash-preview
gc/gemini-2.5-pro
```
**Meilleure valeur :** Niveau gratuit énorme ! Utilisez avant les niveaux payants.
### GitHub Copilot
```bash
Tableau de bord → Fournisseurs → Connecter GitHub
→ OAuth via GitHub
→ Reset mensuel (1er du mois)
Modèles :
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3-pro
```
</details>
<details>
<summary><b>🔑 Fournisseurs par clé API</b></summary>
### NVIDIA NIM (GRATUIT 1000 crédits !)
1. S'inscrire : [build.nvidia.com](https://build.nvidia.com)
2. Obtenir une clé API gratuite (1000 crédits d'inférence inclus)
3. Tableau de bord → Ajouter fournisseur → NVIDIA NIM :
- API Key : `nvapi-your-key`
**Modèles :** `nvidia/llama-3.3-70b-instruct`, `nvidia/mistral-7b-instruct` et 50+ autres
**Conseil Pro :** API compatible OpenAI — fonctionne parfaitement avec la traduction de format d'OmniRoute !
### DeepSeek
1. S'inscrire : [platform.deepseek.com](https://platform.deepseek.com)
2. Obtenir une clé API
3. Tableau de bord → Ajouter fournisseur → DeepSeek
**Modèles :** `deepseek/deepseek-chat`, `deepseek/deepseek-coder`
### Groq (Niveau gratuit disponible !)
1. S'inscrire : [console.groq.com](https://console.groq.com)
2. Obtenir une clé API (niveau gratuit inclus)
3. Tableau de bord → Ajouter fournisseur → Groq
**Modèles :** `groq/llama-3.3-70b`, `groq/mixtral-8x7b`
**Conseil Pro :** Inférence ultra-rapide — idéal pour le codage en temps réel !
### OpenRouter (100+ modèles)
1. S'inscrire : [openrouter.ai](https://openrouter.ai)
2. Obtenir une clé API
3. Tableau de bord → Ajouter fournisseur → OpenRouter
**Modèles :** Accès à 100+ modèles de tous les grands fournisseurs via une seule clé API.
</details>
<details>
<summary><b>💰 Fournisseurs économiques (Backup)</b></summary>
### GLM-4.7 (Reset quotidien, $0.6/1M)
1. S'inscrire : [Zhipu AI](https://open.bigmodel.cn/)
2. Obtenir une clé API du Coding Plan
3. Tableau de bord → Ajouter clé API :
- Fournisseur : `glm`
- API Key : `your-key`
**Utilisez :** `glm/glm-4.7`
**Conseil Pro :** Le Coding Plan offre 3× le quota à 1/7 du coût ! Reset quotidien à 10h.
### MiniMax M2.1 (Reset 5h, $0.20/1M)
1. S'inscrire : [MiniMax](https://www.minimax.io/)
2. Obtenir une clé API
3. Tableau de bord → Ajouter clé API
**Utilisez :** `minimax/MiniMax-M2.1`
**Conseil Pro :** L'option la moins chère pour le contexte long (1M tokens) !
### Kimi K2 (9 $/mois fixe)
1. S'abonner : [Moonshot AI](https://platform.moonshot.ai/)
2. Obtenir une clé API
3. Tableau de bord → Ajouter clé API
**Utilisez :** `kimi/kimi-latest`
**Conseil Pro :** 9 $/mois fixe pour 10M tokens = 0,90 $/1M de coût effectif !
</details>
<details>
<summary><b>🆓 Fournisseurs GRATUITS (Backup d'urgence)</b></summary>
### iFlow (8 modèles GRATUITS)
```bash
Tableau de bord → Connecter iFlow
→ Connexion OAuth iFlow
→ Utilisation illimitée
Modèles :
if/kimi-k2-thinking
if/qwen3-coder-plus
if/glm-4.7
if/minimax-m2
if/deepseek-r1
```
### Qwen (3 modèles GRATUITS)
```bash
Tableau de bord → Connecter Qwen
→ Autorisation par code d'appareil
→ Utilisation illimitée
Modèles :
qw/qwen3-coder-plus
qw/qwen3-coder-flash
```
### Kiro (Claude GRATUIT)
```bash
Tableau de bord → Connecter Kiro
→ AWS Builder ID ou Google/GitHub
→ Utilisation illimitée
Modèles :
kr/claude-sonnet-4.5
kr/claude-haiku-4.5
```
</details>
<details>
<summary><b>🎨 Créer des combos</b></summary>
### Exemple 1 : Maximiser l'abonnement → Backup économique
```
Tableau de bord → Combos → Créer nouveau
Nom : premium-coding
Modèles :
1. cc/claude-opus-4-6 (Abonnement principal)
2. glm/glm-4.7 (Backup économique, $0.6/1M)
3. minimax/MiniMax-M2.1 (Fallback le moins cher, $0.20/1M)
Utilisez en CLI : premium-coding
```
### Exemple 2 : Gratuit uniquement (Zéro coût)
```
Nom : free-combo
Modèles :
1. gc/gemini-3-flash-preview (180K gratuits/mois)
2. if/kimi-k2-thinking (illimité)
3. qw/qwen3-coder-plus (illimité)
Coût : 0 $ pour toujours !
```
</details>
<details>
<summary><b>🔧 Intégration CLI</b></summary>
### Cursor IDE
```
Paramètres → Modèles → Avancé :
OpenAI API Base URL : http://localhost:20128/v1
OpenAI API Key : [du tableau de bord OmniRoute]
Model : cc/claude-opus-4-6
```
### Claude Code
Utilisez la page **CLI Tools** dans le tableau de bord pour la configuration en un clic, ou modifiez `~/.claude/settings.json` manuellement.
### Codex CLI
```bash
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
```
### OpenClaw
**Option 1 — Tableau de bord (recommandé) :**
```
Tableau de bord → CLI Tools → OpenClaw → Sélectionner modèle → Appliquer
```
**Option 2 — Manuel :** Modifier `~/.openclaw/openclaw.json` :
```json
{
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://127.0.0.1:20128/v1",
"apiKey": "sk_omniroute",
"api": "openai-completions"
}
}
}
}
```
> **Note :** OpenClaw fonctionne uniquement avec OmniRoute local. Utilisez `127.0.0.1` au lieu de `localhost` pour éviter les problèmes de résolution IPv6.
### Cline / Continue / RooCode
```
Paramètres → Configuration API :
Fournisseur : OpenAI Compatible
Base URL : http://localhost:20128/v1
API Key : [du tableau de bord OmniRoute]
Model : if/kimi-k2-thinking
```
</details>
---
## 📊 Modèles disponibles
<details>
<summary><b>Voir tous les modèles disponibles</b></summary>
**Claude Code (`cc/`)** - Pro/Max :
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro :
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - GRATUIT :
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)** :
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - Crédits GRATUITS :
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ modèles sur [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M :
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M :
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - GRATUIT :
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - GRATUIT :
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - GRATUIT :
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ modèles :
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Tout modèle de [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Évaluations (Evals)
OmniRoute inclut un framework d'évaluation intégré pour tester la qualité des réponses LLM contre un golden set. Accès via **Analytics → Evals** dans le tableau de bord.
### Golden Set intégré
Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
- Salutations, mathématiques, géographie, génération de code
- Conformité format JSON, traduction, markdown
- Rejet de sécurité (contenu nocif), comptage, logique booléenne
### Stratégies d'évaluation
| Stratégie | Description | Exemple |
| ---------- | -------------------------------------------------------------- | -------------------------------- |
| `exact` | La sortie doit correspondre exactement | `"4"` |
| `contains` | La sortie doit contenir la sous-chaîne (insensible à la casse) | `"Paris"` |
| `regex` | La sortie doit correspondre au motif regex | `"1.*2.*3"` |
| `custom` | Fonction JS personnalisée retourne true/false | `(output) => output.length > 10` |
---
## 🐛 Dépannage
<details>
<summary><b>Cliquez pour développer le guide de dépannage</b></summary>
**« Language model did not provide messages »**
- Quota du fournisseur épuisé → Vérifiez le suivi de quota dans le tableau de bord
- Solution : Utilisez un combo avec fallback ou passez à un niveau moins cher
**Rate limiting**
- Quota d'abonnement épuisé → Fallback vers GLM/MiniMax
- Ajoutez un combo : `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**Token OAuth expiré**
- Renouvelé automatiquement par OmniRoute
- Si le problème persiste : Tableau de bord → Fournisseur → Reconnecter
**Coûts élevés**
- Vérifiez les statistiques d'utilisation dans Tableau de bord → Coûts
- Changez le modèle principal pour GLM/MiniMax
- Utilisez le niveau gratuit (Gemini CLI, iFlow) pour les tâches non critiques
**Le tableau de bord s'ouvre sur le mauvais port**
- Définissez `PORT=20128` et `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
**Erreurs de cloud sync**
- Vérifiez que `BASE_URL` pointe vers votre instance en cours d'exécution
- Vérifiez que `CLOUD_URL` pointe vers le point de terminaison cloud attendu
- Gardez les valeurs `NEXT_PUBLIC_*` alignées avec les valeurs du serveur
**Le premier login ne fonctionne pas**
- Vérifiez `INITIAL_PASSWORD` dans `.env`
- Si non défini, le mot de passe par défaut est `123456`
**Pas de logs de requêtes**
- Définissez `ENABLE_REQUEST_LOGS=true` dans `.env`
**Le test de connexion affiche « Invalid » pour les fournisseurs compatibles OpenAI**
- Beaucoup de fournisseurs n'exposent pas le point de terminaison `/models`
- OmniRoute v1.0.6+ inclut une validation de secours via chat completions
- Assurez-vous que l'URL de base inclut le suffixe `/v1`
</details>
---
## 🛠️ Stack technologique
- **Runtime** : Node.js 20+
- **Langage** : TypeScript 5.9 — **100% TypeScript** dans `src/` et `open-sse/` (v1.0.6)
- **Framework** : Next.js 16 + React 19 + Tailwind CSS 4
- **Base de données** : LowDB (JSON) + SQLite (état du domaine + logs proxy)
- **Streaming** : Server-Sent Events (SSE)
- **Auth** : OAuth 2.0 (PKCE) + JWT + API Keys
- **Tests** : Node.js test runner (368+ tests unitaires)
- **CI/CD** : GitHub Actions (publication automatique npm + Docker Hub lors du release)
- **Site web** : [omniroute.online](https://omniroute.online)
- **Package** : [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
- **Docker** : [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
- **Résilience** : Circuit breaker, backoff exponentiel, anti-thundering herd, spoofing TLS
---
## 📖 Documentation
| Document | Description |
| ------------------------------------------ | --------------------------------------------------- |
| [Guide utilisateur](docs/USER_GUIDE.md) | Fournisseurs, combos, intégration CLI, déploiement |
| [Référence API](docs/API_REFERENCE.md) | Tous les endpoints avec exemples |
| [Dépannage](docs/TROUBLESHOOTING.md) | Problèmes courants et solutions |
| [Architecture](docs/ARCHITECTURE.md) | Architecture système et détails internes |
| [Contribuer](CONTRIBUTING.md) | Configuration de développement et directives |
| [Spécification OpenAPI](docs/openapi.yaml) | Spécification OpenAPI 3.0 |
| [Politique de sécurité](SECURITY.md) | Signalement de vulnérabilités et pratiques sécurité |
---
## 📧 Support
> 💬 **Rejoignez notre communauté !** [Groupe WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtenez de l'aide, partagez des astuces et restez informé.
- **Site web** : [omniroute.online](https://omniroute.online)
- **GitHub** : [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues** : [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp** : [Groupe communautaire](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Projet original** : [9router par decolua](https://github.com/decolua/9router)
---
## 👥 Contributeurs
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
### Comment contribuer
1. Forkez le dépôt
2. Créez votre branche de fonctionnalité (`git checkout -b feature/amazing-feature`)
3. Committez vos changements (`git commit -m 'Add amazing feature'`)
4. Poussez vers la branche (`git push origin feature/amazing-feature`)
5. Ouvrez une Pull Request
Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour les directives détaillées.
### Publier une nouvelle version
```bash
# Créer un release — la publication npm est automatique
gh release create v1.0.6 --title "v1.0.6" --generate-notes
```
---
## 📊 Historique des Stars
<a href="https://star-history.com/#diegosouzapw/OmniRoute&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
</picture>
</a>
---
## 🙏 Remerciements
Remerciements spéciaux à **[9router](https://github.com/decolua/9router)** par **[decolua](https://github.com/decolua)** — le projet original qui a inspiré ce fork. OmniRoute construit sur cette base incroyable avec des fonctionnalités supplémentaires, des APIs multi-modales et une réécriture complète en TypeScript.
Remerciements spéciaux à **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — l'implémentation originale en Go qui a inspiré ce portage en JavaScript.
---
## 📄 Licence
Licence MIT — voir [LICENSE](LICENSE) pour les détails.
---
<div align="center">
<sub>Fait avec ❤️ pour les développeurs qui codent 24/7</sub>
<br/>
<sub><a href="https://omniroute.online">omniroute.online</a></sub>
</div>
-1000
View File
File diff suppressed because it is too large Load Diff
+740 -610
View File
File diff suppressed because it is too large Load Diff
-1053
View File
File diff suppressed because it is too large Load Diff
-999
View File
@@ -1,999 +0,0 @@
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
# 🚀 OmniRoute — Бесплатный AI Gateway
### Никогда не прекращайте программировать. Умная маршрутизация к **БЕСПЛАТНЫМ и дешёвым AI-моделям** с автоматическим fallback.
_Ваш универсальный API-прокси — одна точка доступа, 36+ провайдеров, нулевой простой._
**Chat Completions • Embeddings • Генерация изображений • Аудио • Reranking • 100% TypeScript**
---
### 🤖 Бесплатный AI-провайдер для ваших любимых агентов программирования
_Подключайте любую IDE или CLI-инструмент с AI через OmniRoute — бесплатный API gateway для неограниченного программирования._
<table>
<tr>
<td align="center" width="110">
<a href="https://github.com/cline/cline">
<img src="./public/providers/openclaw.png" alt="OpenClaw" width="48"/><br/>
<b>OpenClaw</b>
</a><br/>
<sub>⭐ 205K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/HKUDS/nanobot">
<img src="./public/providers/nanobot.png" alt="NanoBot" width="48"/><br/>
<b>NanoBot</b>
</a><br/>
<sub>⭐ 20.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/sipeed/picoclaw">
<img src="./public/providers/picoclaw.jpg" alt="PicoClaw" width="48"/><br/>
<b>PicoClaw</b>
</a><br/>
<sub>⭐ 14.6K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/zeroclaw-labs/zeroclaw">
<img src="./public/providers/zeroclaw.png" alt="ZeroClaw" width="48"/><br/>
<b>ZeroClaw</b>
</a><br/>
<sub>⭐ 9.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/nearai/ironclaw">
<img src="./public/providers/ironclaw.png" alt="IronClaw" width="48"/><br/>
<b>IronClaw</b>
</a><br/>
<sub>⭐ 2.1K</sub>
</td>
</tr>
<tr>
<td align="center" width="110">
<a href="https://github.com/anomalyco/opencode">
<img src="./public/providers/opencode.svg" alt="OpenCode" width="48"/><br/>
<b>OpenCode</b>
</a><br/>
<sub>⭐ 106K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/openai/codex">
<img src="./public/providers/codex.png" alt="Codex CLI" width="48"/><br/>
<b>Codex CLI</b>
</a><br/>
<sub>⭐ 60.8K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/anthropics/claude-code">
<img src="./public/providers/claude.png" alt="Claude Code" width="48"/><br/>
<b>Claude Code</b>
</a><br/>
<sub>⭐ 67.3K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/google-gemini/gemini-cli">
<img src="./public/providers/gemini-cli.png" alt="Gemini CLI" width="48"/><br/>
<b>Gemini CLI</b>
</a><br/>
<sub>⭐ 94.7K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/Kilo-Org/kilocode">
<img src="./public/providers/kilocode.png" alt="Kilo Code" width="48"/><br/>
<b>Kilo Code</b>
</a><br/>
<sub>⭐ 15.5K</sub>
</td>
</tr>
</table>
<sub>📡 Все агенты подключаются через <code>http://localhost:20128/v1</code> или <code>http://cloud.omniroute.online/v1</code> — одна конфигурация, неограниченные модели и квота</sub>
---
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License](https://img.shields.io/github/license/diegosouzapw/OmniRoute)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[🌐 Сайт](https://omniroute.online) • [🚀 Быстрый старт](#-быстрый-старт) • [💡 Функции](#-основные-функции) • [📖 Документация](#-документация) • [💰 Цены](#-обзор-цен)
🌐 **Доступно на:** [English](README.md) | [Português](README.pt-BR.md) | [Español](README.es.md) | [Русский](README.ru.md) | [中文](README.zh-CN.md) | [Deutsch](README.de.md) | [Français](README.fr.md) | [Italiano](README.it.md)
</div>
---
## 🤔 Почему OmniRoute?
**Перестаньте тратить деньги и упираться в лимиты:**
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Квота подписки истекает неиспользованной каждый месяц
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Лимиты скорости останавливают вас посреди программирования
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Дорогие API ($20-50/месяц за провайдера)
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> Ручное переключение между провайдерами
**OmniRoute решает это:**
- ✅ **Максимизируйте подписки** — Отслеживайте квоты, используйте всё до сброса
- ✅ **Автоматический fallback** — Подписка → API Key → Дешёвый → Бесплатный, нулевой простой
- ✅ **Мульти-аккаунт** — Round-robin между аккаунтами каждого провайдера
- ✅ **Универсальный** — Работает с Claude Code, Codex, Gemini CLI, Cursor, Cline, OpenClaw, любым CLI-инструментом
---
## 🔄 Как это работает
```
┌─────────────┐
│ Ваш CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...)
│ Tool │
└──────┬──────┘
│ http://localhost:20128/v1
┌─────────────────────────────────────────┐
│ OmniRoute (Умный маршрутизатор) │
│ • Трансляция формата (OpenAI ↔ Claude) │
│ • Отслеживание квот + Embeddings + Изображения │
│ • Автообновление токенов │
└──────┬──────────────────────────────────┘
├─→ [Tier 1: ПОДПИСКА] Claude Code, Codex, Gemini CLI
│ ↓ квота исчерпана
├─→ [Tier 2: API KEY] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM и др.
│ ↓ лимит бюджета
├─→ [Tier 3: ДЕШЁВЫЙ] GLM ($0.6/1M), MiniMax ($0.2/1M)
│ ↓ лимит бюджета
└─→ [Tier 4: БЕСПЛАТНЫЙ] iFlow, Qwen, Kiro (неограниченно)
Результат: Никогда не прекращайте программировать, минимальные затраты
```
---
## ⚡ Быстрый старт
**1. Установите глобально:**
```bash
npm install -g omniroute
omniroute
```
🎉 Dashboard открывается на `http://localhost:20128`
| Команда | Описание |
| ----------------------- | ------------------------------------------ |
| `omniroute` | Запустить сервер (порт по умолчанию 20128) |
| `omniroute --port 3000` | Использовать другой порт |
| `omniroute --no-open` | Не открывать браузер автоматически |
| `omniroute --help` | Показать справку |
**2. Подключите БЕСПЛАТНОГО провайдера:**
Dashboard → Провайдеры → Подключить **Claude Code** или **Antigravity** → OAuth вход → Готово!
**3. Используйте в CLI-инструменте:**
```
Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Настройки:
Endpoint: http://localhost:20128/v1
API Key: [скопируйте из dashboard]
Model: if/kimi-k2-thinking
```
**Готово!** Начните программировать с БЕСПЛАТНЫМИ AI-моделями.
**Альтернатива — запуск из исходного кода:**
```bash
cp .env.example .env
npm install
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
---
## 🐳 Docker
OmniRoute доступен как публичный Docker-образ на [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute).
**Быстрый запуск:**
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**С файлом окружения:**
```bash
# Скопируйте и отредактируйте .env
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**Используя Docker Compose:**
```bash
# Базовый профиль (без CLI-инструментов)
docker compose --profile base up -d
# CLI-профиль (Claude Code, Codex, OpenClaw встроены)
docker compose --profile cli up -d
```
| Образ | Тег | Размер | Описание |
| ------------------------ | -------- | ------ | -------------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Последний стабильный релиз |
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Текущая версия |
---
## 💰 Обзор цен
| Tier | Провайдер | Стоимость | Сброс квоты | Лучше всего для |
| ----------------- | ----------------- | ----------------------------- | ------------------ | -------------------------------- |
| **💳 ПОДПИСКА** | Claude Code (Pro) | $20/мес | 5ч + еженедельно | Уже подписан |
| | Codex (Plus/Pro) | $20-200/мес | 5ч + еженедельно | Пользователи OpenAI |
| | Gemini CLI | **БЕСПЛАТНО** | 180K/мес + 1K/день | Все! |
| | GitHub Copilot | $10-19/мес | Ежемесячно | Пользователи GitHub |
| **🔑 API KEY** | NVIDIA NIM | **БЕСПЛАТНО** (1000 кредитов) | Одноразово | Бесплатное тестирование |
| | DeepSeek | По использованию | Нет | Лучшее соотношение цена/качество |
| | Groq | Беспл. уровень + платный | Ограничено | Сверхбыстрый вывод |
| | xAI (Grok) | По использованию | Нет | Модели Grok |
| | Mistral | Беспл. уровень + платный | Ограничено | Европейский AI |
| | OpenRouter | По использованию | Нет | 100+ моделей |
| **💰 ДЕШЁВЫЙ** | GLM-4.7 | $0.6/1M | Ежедневно 10ч | Бюджетный бэкап |
| | MiniMax M2.1 | $0.2/1M | 5ч ротация | Самый дешёвый вариант |
| | Kimi K2 | $9/мес фикс | 10M токенов/мес | Предсказуемая цена |
| **🆓 БЕСПЛАТНЫЙ** | iFlow | $0 | Неограниченно | 8 бесплатных моделей |
| | Qwen | $0 | Неограниченно | 3 бесплатные модели |
| | Kiro | $0 | Неограниченно | Claude бесплатно |
**💡 Совет:** Начните с Gemini CLI (180K бесплатно/мес) + iFlow (неограниченно бесплатно) = $0!
---
## 🎯 Сценарии использования
### Сценарий 1: «У меня подписка Claude Pro»
**Проблема:** Квота истекает неиспользованной, лимиты скорости во время интенсивного программирования
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (используйте подписку полностью)
2. glm/glm-4.7 (дешёвый бэкап при исчерпании квоты)
3. if/kimi-k2-thinking (бесплатный аварийный fallback)
Месячная стоимость: $20 (подписка) + ~$5 (бэкап) = $25 итого
vs. $20 + упирание в лимиты = разочарование
```
### Сценарий 2: «Хочу нулевую стоимость»
**Проблема:** Не может позволить подписки, нужен надёжный AI для программирования
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K бесплатно/мес)
2. if/kimi-k2-thinking (неограниченно бесплатно)
3. qw/qwen3-coder-plus (неограниченно бесплатно)
Месячная стоимость: $0
Качество: Модели готовые к продакшену
```
### Сценарий 3: «Мне нужно программировать 24/7, без перерывов»
**Проблема:** Дедлайны, не может позволить простой
```
Combo: "always-on"
1. cc/claude-opus-4-6 (лучшее качество)
2. cx/gpt-5.2-codex (вторая подписка)
3. glm/glm-4.7 (дешёвый, ежедневный сброс)
4. minimax/MiniMax-M2.1 (самый дешёвый, сброс 5ч)
5. if/kimi-k2-thinking (бесплатно неограниченно)
Результат: 5 уровней fallback = нулевой простой
```
### Сценарий 4: «Хочу БЕСПЛАТНЫЙ AI в OpenClaw»
**Проблема:** Нужен AI-ассистент в мессенджерах, полностью бесплатно
```
Combo: "openclaw-free"
1. if/glm-4.7 (неограниченно бесплатно)
2. if/minimax-m2.1 (неограниченно бесплатно)
3. if/kimi-k2-thinking (неограниченно бесплатно)
Месячная стоимость: $0
Доступ через: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 💡 Основные функции
### 🧠 Маршрутизация и интеллект
| Функция | Что делает |
| ------------------------------------------- | ----------------------------------------------------------------------------- |
| 🎯 **Умный 4-уровневый Fallback** | Авто-маршрутизация: Подписка → API Key → Дешёвый → Бесплатный |
| 📊 **Отслеживание квот в реальном времени** | Счётчик токенов в реальном времени + обратный отсчёт до сброса |
| 🔄 **Трансляция формата** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro бесшовно |
| 👥 **Мульти-аккаунт** | Несколько аккаунтов на провайдера с интеллектуальным выбором |
| 🔄 **Автообновление токенов** | OAuth-токены обновляются автоматически с повторами |
| 🎨 **Пользовательские комбо** | 6 стратегий: fill-first, round-robin, p2c, random, least-used, cost-optimized |
| 🧩 **Пользовательские модели** | Добавьте любой ID модели к любому провайдеру |
| 🌐 **Wildcard-маршрутизатор** | Маршрутизируйте паттерны `provider/*` к любому провайдеру динамически |
| 🧠 **Бюджет рассуждений** | Режимы passthrough, auto, custom и adaptive для моделей рассуждений |
| 💬 **Инъекция System Prompt** | Глобальный system prompt для всех запросов |
| 📄 **API Responses** | Полная поддержка OpenAI Responses API (`/v1/responses`) для Codex |
### 🎵 Мультимодальные API
| Функция | Что делает |
| ---------------------------- | --------------------------------------------------- |
| 🖼️ **Генерация изображений** | `/v1/images/generations` — 4 провайдера, 9+ моделей |
| 📐 **Embeddings** | `/v1/embeddings` — 6 провайдеров, 9+ моделей |
| 🎤 **Транскрипция аудио** | `/v1/audio/transcriptions` — Совместимо с Whisper |
| 🔊 **Текст в речь** | `/v1/audio/speech` — Мульти-провайдерный синтез |
| 🛡️ **Модерация** | `/v1/moderations` — Проверки безопасности контента |
| 🔀 **Reranking** | `/v1/rerank` — Переранжирование релевантности |
### 🛡️ Устойчивость и безопасность
| Функция | Что делает |
| -------------------------------- | -------------------------------------------------------------- |
| 🔌 **Circuit Breaker** | Авто-открытие/закрытие по провайдеру с настраиваемыми порогами |
| 🛡️ **Anti-Thundering Herd** | Mutex + семафор для API key провайдеров |
| 🧠 **Семантический кеш** | Двухуровневый кеш (сигнатура + семантика) снижает стоимость |
| ⚡ **Идемпотентность запросов** | 5с окно дедупликации для дублирующихся запросов |
| 🔒 **Спуфинг TLS Fingerprint** | Обход обнаружения ботов через wreq-js |
| 🌐 **Фильтрация IP** | Allowlist/blocklist для контроля доступа к API |
| 📊 **Настраиваемые Rate Limits** | Настраиваемые RPM, минимальный интервал, макс. конкуррентность |
### 📊 Наблюдаемость и аналитика
| Функция | Что делает |
| ----------------------------- | ------------------------------------------------------------------------ |
| 📝 **Логи запросов** | Режим debug с полными логами запросов/ответов |
| 💾 **Логи SQLite** | Постоянные proxy-логи переживают перезапуски |
| 📊 **Dashboard аналитики** | Recharts: карточки статистики, график использования, таблица провайдеров |
| 📈 **Отслеживание прогресса** | Opt-in SSE-события прогресса для стриминга |
| 🧪 **Оценки LLM** | Тестирование с golden set и 4 стратегиями сравнения |
| 🔍 **Телеметрия запросов** | Агрегация латентности p50/p95/p99 + трекинг X-Request-Id |
| 📋 **Логи + Квоты** | Отдельные страницы для просмотра логов и отслеживания квот |
| 🏥 **Dashboard здоровья** | Uptime, состояния circuit breaker, блокировки, статистика кеша |
| 💰 **Отслеживание стоимости** | Управление бюджетом + настройка цен по моделям |
### ☁️ Деплой и синхронизация
| Функция | Что делает |
| -------------------------- | --------------------------------------------------------------------------- |
| 💾 **Cloud Sync** | Синхронизация настроек между устройствами через Cloudflare Workers |
| 🌐 **Деплой куда угодно** | Localhost, VPS, Docker, Cloudflare Workers |
| 🔑 **Управление API Keys** | Генерация, ротация и настройка scope API keys по провайдерам |
| 🧙 **Мастер настройки** | 4-шаговая настройка для новых пользователей |
| 🔧 **Dashboard CLI Tools** | Настройка в один клик для Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
| 🔄 **Бэкапы БД** | Автоматическое резервное копирование и восстановление всех настроек |
<details>
<summary><b>📖 Подробности функций</b></summary>
### 🎯 Умный 4-уровневый Fallback
Создавайте комбо с автоматическим fallback:
```
Combo: "my-coding-stack"
1. cc/claude-opus-4-6 (ваша подписка)
2. nvidia/llama-3.3-70b (бесплатный NVIDIA API)
3. glm/glm-4.7 (дешёвый бэкап, $0.6/1M)
4. if/kimi-k2-thinking (бесплатный fallback)
→ Автоматически переключается при исчерпании квоты или ошибках
```
### 📊 Отслеживание квот в реальном времени
- Потребление токенов по провайдерам
- Обратный отсчёт до сброса (5 часов, ежедневно, еженедельно)
- Оценка стоимости для платных уровней
- Ежемесячные отчёты о расходах
### 🔄 Трансляция формата
Бесшовная трансляция между форматами:
- **OpenAI****Claude****Gemini** ↔ **OpenAI Responses**
- Ваш CLI отправляет формат OpenAI → OmniRoute транслирует → Провайдер получает нативный формат
- Работает с любым инструментом, поддерживающим пользовательские OpenAI endpoints
### 👥 Мульти-аккаунт
- Добавляйте несколько аккаунтов на провайдера
- Автоматический round-robin или маршрутизация по приоритету
- Fallback на следующий аккаунт при исчерпании квоты
### 🔄 Автообновление токенов
- OAuth-токены обновляются автоматически до истечения
- Без необходимости ручной повторной аутентификации
- Бесшовный опыт по всем провайдерам
### 🎨 Пользовательские комбо
- Создавайте неограниченные комбинации моделей
- 6 стратегий: fill-first, round-robin, power-of-two-choices, random, least-used, cost-optimized
- Делитесь комбо между устройствами с Cloud Sync
### 🏥 Dashboard здоровья
- Статус системы (uptime, версия, использование памяти)
- Состояния circuit breaker по провайдерам (Closed/Open/Half-Open)
- Статус rate limit и активные блокировки
- Статистика кеша сигнатур
- Телеметрия латентности (p50/p95/p99) + кеш промптов
- Сброс состояния здоровья одним кликом
### 🔧 Playground транслятора
- Отладка, тестирование и визуализация трансляции форматов API
- Отправляйте запросы и смотрите, как OmniRoute транслирует между форматами провайдеров
- Бесценно для устранения проблем интеграции
### 💾 Cloud Sync
- Синхронизация провайдеров, комбо и настроек между устройствами
- Автоматическая фоновая синхронизация
- Безопасное шифрованное хранилище
</details>
---
## 📖 Руководство по настройке
<details>
<summary><b>💳 Провайдеры по подписке</b></summary>
### Claude Code (Pro/Max)
```bash
Dashboard → Провайдеры → Подключить Claude Code
→ OAuth вход → Автообновление токенов
→ Отслеживание квоты 5ч + еженедельно
Модели:
cc/claude-opus-4-6
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
```
**Совет:** Используйте Opus для сложных задач, Sonnet для скорости. OmniRoute отслеживает квоту по моделям!
### OpenAI Codex (Plus/Pro)
```bash
Dashboard → Провайдеры → Подключить Codex
→ OAuth вход (порт 1455)
→ Сброс 5ч + еженедельно
Модели:
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
```
### Gemini CLI (БЕСПЛАТНО 180K/мес!)
```bash
Dashboard → Провайдеры → Подключить Gemini CLI
→ Google OAuth
→ 180K completions/мес + 1K/день
Модели:
gc/gemini-3-flash-preview
gc/gemini-2.5-pro
```
**Лучшая ценность:** Огромный бесплатный уровень! Используйте перед платными.
### GitHub Copilot
```bash
Dashboard → Провайдеры → Подключить GitHub
→ OAuth через GitHub
→ Ежемесячный сброс (1-е число)
Модели:
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3-pro
```
</details>
<details>
<summary><b>🔑 Провайдеры по API Key</b></summary>
### NVIDIA NIM (БЕСПЛАТНО 1000 кредитов!)
1. Регистрация: [build.nvidia.com](https://build.nvidia.com)
2. Получите бесплатный API key (1000 кредитов включены)
3. Dashboard → Добавить провайдера → NVIDIA NIM:
- API Key: `nvapi-your-key`
**Модели:** `nvidia/llama-3.3-70b-instruct`, `nvidia/mistral-7b-instruct` и 50+ других
**Совет:** OpenAI-совместимый API — работает идеально с трансляцией форматов OmniRoute!
### DeepSeek
1. Регистрация: [platform.deepseek.com](https://platform.deepseek.com)
2. Получите API key
3. Dashboard → Добавить провайдера → DeepSeek
**Модели:** `deepseek/deepseek-chat`, `deepseek/deepseek-coder`
### Groq (Бесплатный уровень доступен!)
1. Регистрация: [console.groq.com](https://console.groq.com)
2. Получите API key (бесплатный уровень включён)
3. Dashboard → Добавить провайдера → Groq
**Модели:** `groq/llama-3.3-70b`, `groq/mixtral-8x7b`
**Совет:** Сверхбыстрый вывод — лучший для программирования в реальном времени!
### OpenRouter (100+ моделей)
1. Регистрация: [openrouter.ai](https://openrouter.ai)
2. Получите API key
3. Dashboard → Добавить провайдера → OpenRouter
**Модели:** Доступ к 100+ моделям от всех основных провайдеров через один API key.
</details>
<details>
<summary><b>💰 Дешёвые провайдеры (Бэкап)</b></summary>
### GLM-4.7 (Ежедневный сброс, $0.6/1M)
1. Регистрация: [Zhipu AI](https://open.bigmodel.cn/)
2. Получите API key из Coding Plan
3. Dashboard → Добавить API Key:
- Провайдер: `glm`
- API Key: `your-key`
**Используйте:** `glm/glm-4.7`
**Совет:** Coding Plan предлагает 3× квоту по цене 1/7! Ежедневный сброс в 10:00.
### MiniMax M2.1 (Сброс 5ч, $0.20/1M)
1. Регистрация: [MiniMax](https://www.minimax.io/)
2. Получите API key
3. Dashboard → Добавить API Key
**Используйте:** `minimax/MiniMax-M2.1`
**Совет:** Самый дешёвый вариант для длинного контекста (1M токенов)!
### Kimi K2 ($9/мес фикс)
1. Подпишитесь: [Moonshot AI](https://platform.moonshot.ai/)
2. Получите API key
3. Dashboard → Добавить API Key
**Используйте:** `kimi/kimi-latest`
**Совет:** Фикс $9/мес за 10M токенов = $0.90/1M эффективная стоимость!
</details>
<details>
<summary><b>🆓 БЕСПЛАТНЫЕ провайдеры (Аварийный бэкап)</b></summary>
### iFlow (8 БЕСПЛАТНЫХ моделей)
```bash
Dashboard → Подключить iFlow
→ OAuth вход iFlow
→ Неограниченное использование
Модели:
if/kimi-k2-thinking
if/qwen3-coder-plus
if/glm-4.7
if/minimax-m2
if/deepseek-r1
```
### Qwen (3 БЕСПЛАТНЫЕ модели)
```bash
Dashboard → Подключить Qwen
→ Авторизация по коду устройства
→ Неограниченное использование
Модели:
qw/qwen3-coder-plus
qw/qwen3-coder-flash
```
### Kiro (Claude БЕСПЛАТНО)
```bash
Dashboard → Подключить Kiro
→ AWS Builder ID или Google/GitHub
→ Неограниченное использование
Модели:
kr/claude-sonnet-4.5
kr/claude-haiku-4.5
```
</details>
<details>
<summary><b>🎨 Создание комбо</b></summary>
### Пример 1: Максимизация подписки → Дешёвый бэкап
```
Dashboard → Комбо → Создать новое
Название: premium-coding
Модели:
1. cc/claude-opus-4-6 (Основная подписка)
2. glm/glm-4.7 (Дешёвый бэкап, $0.6/1M)
3. minimax/MiniMax-M2.1 (Самый дешёвый fallback, $0.20/1M)
Используйте в CLI: premium-coding
```
### Пример 2: Только бесплатные (Нулевая стоимость)
```
Название: free-combo
Модели:
1. gc/gemini-3-flash-preview (180K бесплатно/мес)
2. if/kimi-k2-thinking (неограниченно)
3. qw/qwen3-coder-plus (неограниченно)
Стоимость: $0 навсегда!
```
</details>
<details>
<summary><b>🔧 Интеграция с CLI</b></summary>
### Cursor IDE
```
Настройки → Модели → Расширенные:
OpenAI API Base URL: http://localhost:20128/v1
OpenAI API Key: [из dashboard OmniRoute]
Model: cc/claude-opus-4-6
```
### Claude Code
Используйте страницу **CLI Tools** в dashboard для настройки в один клик, или редактируйте `~/.claude/settings.json` вручную.
### Codex CLI
```bash
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
```
### OpenClaw
**Вариант 1 — Dashboard (рекомендуется):**
```
Dashboard → CLI Tools → OpenClaw → Выбрать модель → Применить
```
**Вариант 2 — Вручную:** Редактируйте `~/.openclaw/openclaw.json`:
```json
{
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://127.0.0.1:20128/v1",
"apiKey": "sk_omniroute",
"api": "openai-completions"
}
}
}
}
```
> **Примечание:** OpenClaw работает только с локальным OmniRoute. Используйте `127.0.0.1` вместо `localhost` для избежания проблем с IPv6.
### Cline / Continue / RooCode
```
Настройки → Конфигурация API:
Провайдер: OpenAI Compatible
Base URL: http://localhost:20128/v1
API Key: [из dashboard OmniRoute]
Model: if/kimi-k2-thinking
```
</details>
---
## 📊 Доступные модели
<details>
<summary><b>Посмотреть все доступные модели</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - БЕСПЛАТНО:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - БЕСПЛАТНЫЕ кредиты:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ моделей на [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - БЕСПЛАТНО:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - БЕСПЛАТНО:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - БЕСПЛАТНО:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ моделей:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- Любая модель с [openrouter.ai/models](https://openrouter.ai/models)
</details>
---
## 🧪 Оценки (Evals)
OmniRoute включает встроенный фреймворк оценки для тестирования качества ответов LLM по golden set. Доступ через **Analytics → Evals** в dashboard.
### Встроенный Golden Set
Предзагруженный «OmniRoute Golden Set» содержит 10 тестов:
- Приветствия, математика, география, генерация кода
- Соответствие формату JSON, перевод, markdown
- Отказ от небезопасного контента, подсчёт, булева логика
### Стратегии оценки
| Стратегия | Описание | Пример |
| ---------- | ----------------------------------------------------- | -------------------------------- |
| `exact` | Вывод должен совпадать точно | `"4"` |
| `contains` | Вывод должен содержать подстроку (без учёта регистра) | `"Paris"` |
| `regex` | Вывод должен соответствовать regex-паттерну | `"1.*2.*3"` |
| `custom` | Пользовательская JS-функция возвращает true/false | `(output) => output.length > 10` |
---
## 🐛 Устранение неполадок
<details>
<summary><b>Нажмите для раскрытия руководства</b></summary>
**«Language model did not provide messages»**
- Квота провайдера исчерпана → Проверьте трекер квот в dashboard
- Решение: Используйте комбо с fallback или переключитесь на более дешёвый уровень
**Rate limiting**
- Квота подписки исчерпана → Fallback на GLM/MiniMax
- Добавьте комбо: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**OAuth-токен истёк**
- Обновляется автоматически OmniRoute
- Если проблема сохраняется: Dashboard → Провайдер → Переподключить
**Высокие расходы**
- Проверьте статистику в Dashboard → Расходы
- Переключите основную модель на GLM/MiniMax
- Используйте бесплатный уровень (Gemini CLI, iFlow) для некритичных задач
**Dashboard открывается на неправильном порту**
- Установите `PORT=20128` и `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
**Ошибки cloud sync**
- Проверьте что `BASE_URL` указывает на ваш запущенный экземпляр
- Проверьте что `CLOUD_URL` указывает на правильный облачный endpoint
- Держите значения `NEXT_PUBLIC_*` синхронизированными с серверными значениями
**Первый вход не работает**
- Проверьте `INITIAL_PASSWORD` в `.env`
- Если не задан, пароль по умолчанию `123456`
**Нет логов запросов**
- Установите `ENABLE_REQUEST_LOGS=true` в `.env`
**Тест подключения показывает «Invalid» для OpenAI-совместимых провайдеров**
- Многие провайдеры не предоставляют endpoint `/models`
- OmniRoute v1.0.6+ включает fallback-валидацию через chat completions
- Убедитесь что base URL содержит суффикс `/v1`
</details>
---
## 🛠️ Технологический стек
- **Runtime**: Node.js 20+
- **Язык**: TypeScript 5.9 — **100% TypeScript** в `src/` и `open-sse/` (v1.0.6)
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
- **База данных**: LowDB (JSON) + SQLite (состояние домена + proxy-логи)
- **Стриминг**: Server-Sent Events (SSE)
- **Аутентификация**: OAuth 2.0 (PKCE) + JWT + API Keys
- **Тестирование**: Node.js test runner (368+ юнит-тестов)
- **CI/CD**: GitHub Actions (авто-публикация npm + Docker Hub при релизе)
- **Сайт**: [omniroute.online](https://omniroute.online)
- **Пакет**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
- **Устойчивость**: Circuit breaker, экспоненциальный backoff, anti-thundering herd, TLS-спуфинг
---
## 📖 Документация
| Документ | Описание |
| ----------------------------------------------- | ------------------------------------------------ |
| [Руководство пользователя](docs/USER_GUIDE.md) | Провайдеры, комбо, интеграция CLI, деплой |
| [Справка API](docs/API_REFERENCE.md) | Все endpoints с примерами |
| [Устранение неполадок](docs/TROUBLESHOOTING.md) | Частые проблемы и решения |
| [Архитектура](docs/ARCHITECTURE.md) | Архитектура системы и внутреннее устройство |
| [Как внести вклад](CONTRIBUTING.md) | Настройка разработки и руководящие принципы |
| [Спецификация OpenAPI](docs/openapi.yaml) | Спецификация OpenAPI 3.0 |
| [Политика безопасности](SECURITY.md) | Сообщение об уязвимостях и практики безопасности |
---
## 📧 Поддержка
> 💬 **Присоединяйтесь к сообществу!** [Группа WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Получайте помощь, делитесь советами и оставайтесь в курсе.
- **Сайт**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [Группа сообщества](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **Оригинальный проект**: [9router от decolua](https://github.com/decolua/9router)
---
## 👥 Участники
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
### Как внести вклад
1. Сделайте fork репозитория
2. Создайте ветку функции (`git checkout -b feature/amazing-feature`)
3. Зафиксируйте изменения (`git commit -m 'Add amazing feature'`)
4. Отправьте в ветку (`git push origin feature/amazing-feature`)
5. Откройте Pull Request
См. [CONTRIBUTING.md](CONTRIBUTING.md) для подробных рекомендаций.
### Выпуск новой версии
```bash
# Создайте релиз — публикация в npm происходит автоматически
gh release create v1.0.6 --title "v1.0.6" --generate-notes
```
---
## 📊 История звёзд
<a href="https://star-history.com/#diegosouzapw/OmniRoute&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
</picture>
</a>
---
## 🙏 Благодарности
Особая благодарность **[9router](https://github.com/decolua/9router)** от **[decolua](https://github.com/decolua)** — оригинальному проекту, вдохновившему этот форк. OmniRoute строится на этом невероятном фундаменте с дополнительными функциями, мультимодальными API и полной переписью на TypeScript.
Особая благодарность **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — оригинальной реализации на Go, вдохновившей этот порт на JavaScript.
---
## 📄 Лицензия
Лицензия MIT — см. [LICENSE](LICENSE) для подробностей.
---
<div align="center">
<sub>Сделано с ❤️ для разработчиков, которые программируют 24/7</sub>
<br/>
<sub><a href="https://omniroute.online">omniroute.online</a></sub>
</div>
-999
View File
@@ -1,999 +0,0 @@
<div align="center">
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
# 🚀 OmniRoute — 免费 AI 网关
### 永不停止编程。智能路由至**免费和低成本 AI 模型**,自动故障转移。
_您的通用 API 代理 — 一个端点,36+ 提供商,零停机时间。_
**Chat Completions • Embeddings • 图像生成 • 音频 • Reranking • 100% TypeScript**
---
### 🤖 为您最爱的编程代理提供免费 AI
_通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API 网关,无限编程。_
<table>
<tr>
<td align="center" width="110">
<a href="https://github.com/cline/cline">
<img src="./public/providers/openclaw.png" alt="OpenClaw" width="48"/><br/>
<b>OpenClaw</b>
</a><br/>
<sub>⭐ 205K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/HKUDS/nanobot">
<img src="./public/providers/nanobot.png" alt="NanoBot" width="48"/><br/>
<b>NanoBot</b>
</a><br/>
<sub>⭐ 20.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/sipeed/picoclaw">
<img src="./public/providers/picoclaw.jpg" alt="PicoClaw" width="48"/><br/>
<b>PicoClaw</b>
</a><br/>
<sub>⭐ 14.6K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/zeroclaw-labs/zeroclaw">
<img src="./public/providers/zeroclaw.png" alt="ZeroClaw" width="48"/><br/>
<b>ZeroClaw</b>
</a><br/>
<sub>⭐ 9.9K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/nearai/ironclaw">
<img src="./public/providers/ironclaw.png" alt="IronClaw" width="48"/><br/>
<b>IronClaw</b>
</a><br/>
<sub>⭐ 2.1K</sub>
</td>
</tr>
<tr>
<td align="center" width="110">
<a href="https://github.com/anomalyco/opencode">
<img src="./public/providers/opencode.svg" alt="OpenCode" width="48"/><br/>
<b>OpenCode</b>
</a><br/>
<sub>⭐ 106K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/openai/codex">
<img src="./public/providers/codex.png" alt="Codex CLI" width="48"/><br/>
<b>Codex CLI</b>
</a><br/>
<sub>⭐ 60.8K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/anthropics/claude-code">
<img src="./public/providers/claude.png" alt="Claude Code" width="48"/><br/>
<b>Claude Code</b>
</a><br/>
<sub>⭐ 67.3K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/google-gemini/gemini-cli">
<img src="./public/providers/gemini-cli.png" alt="Gemini CLI" width="48"/><br/>
<b>Gemini CLI</b>
</a><br/>
<sub>⭐ 94.7K</sub>
</td>
<td align="center" width="110">
<a href="https://github.com/Kilo-Org/kilocode">
<img src="./public/providers/kilocode.png" alt="Kilo Code" width="48"/><br/>
<b>Kilo Code</b>
</a><br/>
<sub>⭐ 15.5K</sub>
</td>
</tr>
</table>
<sub>📡 所有代理通过 <code>http://localhost:20128/v1</code> 或 <code>http://cloud.omniroute.online/v1</code> 连接 — 一个配置,无限模型和配额</sub>
---
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License](https://img.shields.io/github/license/diegosouzapw/OmniRoute)](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp](https://img.shields.io/badge/WhatsApp-Community-25D366?logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[🌐 网站](https://omniroute.online) • [🚀 快速开始](#-快速开始) • [💡 功能特性](#-核心功能) • [📖 文档](#-文档) • [💰 定价](#-定价概览)
🌐 **多语言版本:** [English](README.md) | [Português](README.pt-BR.md) | [Español](README.es.md) | [Русский](README.ru.md) | [中文](README.zh-CN.md) | [Deutsch](README.de.md) | [Français](README.fr.md) | [Italiano](README.it.md)
</div>
---
## 🤔 为什么选择 OmniRoute
**停止浪费金钱和遭遇限制:**
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> 订阅配额每月未使用就过期
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> 速率限制在编程中途停止你
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> 昂贵的 API(每个提供商 $20-50/月)
- <img src="https://img.shields.io/badge/✗-e74c3c?style=flat-square" height="16"/> 手动在提供商间切换
**OmniRoute 解决这些问题:**
- ✅ **最大化订阅** — 追踪配额,在重置前用完每一点
- ✅ **自动故障转移** — 订阅 → API Key → 低价 → 免费,零停机
- ✅ **多账号** — 每个提供商的账号轮询
- ✅ **通用** — 适用于 Claude Code、Codex、Gemini CLI、Cursor、Cline、OpenClaw、任何 CLI 工具
---
## 🔄 工作原理
```
┌─────────────┐
│ 您的 CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...)
│ 工具 │
└──────┬──────┘
│ http://localhost:20128/v1
┌─────────────────────────────────────────┐
│ OmniRoute(智能路由器) │
│ • 格式转换(OpenAI ↔ Claude
│ • 配额追踪 + Embeddings + 图像 │
│ • 自动令牌刷新 │
└──────┬──────────────────────────────────┘
├─→ [第1层: 订阅] Claude Code, Codex, Gemini CLI
│ ↓ 配额用完
├─→ [第2层: API KEY] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM 等
│ ↓ 预算限制
├─→ [第3层: 低价] GLM ($0.6/1M), MiniMax ($0.2/1M)
│ ↓ 预算限制
└─→ [第4层: 免费] iFlow, Qwen, Kiro(无限制)
结果:永不停止编程,成本最低
```
---
## ⚡ 快速开始
**1. 全局安装:**
```bash
npm install -g omniroute
omniroute
```
🎉 仪表板在 `http://localhost:20128` 打开
| 命令 | 描述 |
| ----------------------- | ---------------------------- |
| `omniroute` | 启动服务器(默认端口 20128) |
| `omniroute --port 3000` | 使用自定义端口 |
| `omniroute --no-open` | 不自动打开浏览器 |
| `omniroute --help` | 显示帮助 |
**2. 连接免费提供商:**
仪表板 → 提供商 → 连接 **Claude Code****Antigravity** → OAuth 登录 → 完成!
**3. 在 CLI 工具中使用:**
```
Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline 设置:
Endpoint: http://localhost:20128/v1
API Key: [从仪表板复制]
Model: if/kimi-k2-thinking
```
**完成!** 开始使用免费 AI 模型编程。
**替代方案 — 从源代码运行:**
```bash
cp .env.example .env
npm install
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
---
## 🐳 Docker
OmniRoute 作为公共 Docker 镜像在 [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute) 上可用。
**快速运行:**
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**使用环境文件:**
```bash
# 先复制并编辑 .env
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
**使用 Docker Compose**
```bash
# 基础配置(无 CLI 工具)
docker compose --profile base up -d
# CLI 配置(内置 Claude Code、Codex、OpenClaw
docker compose --profile cli up -d
```
| 镜像 | 标签 | 大小 | 描述 |
| ------------------------ | -------- | ------ | ---------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | 最新稳定版 |
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | 当前版本 |
---
## 💰 定价概览
| 层级 | 提供商 | 费用 | 配额重置 | 最适合 |
| -------------- | ----------------- | --------------------- | --------------- | ------------ |
| **💳 订阅** | Claude Code (Pro) | $20/月 | 5小时 + 每周 | 已订阅用户 |
| | Codex (Plus/Pro) | $20-200/月 | 5小时 + 每周 | OpenAI 用户 |
| | Gemini CLI | **免费** | 180K/月 + 1K/天 | 所有人! |
| | GitHub Copilot | $10-19/月 | 每月 | GitHub 用户 |
| **🔑 API KEY** | NVIDIA NIM | **免费**(1000 积分) | 一次性 | 免费测试 |
| | DeepSeek | 按使用量 | 无 | 最佳性价比 |
| | Groq | 免费层 + 付费 | 限速 | 超快推理 |
| | xAI (Grok) | 按使用量 | 无 | Grok 模型 |
| | Mistral | 免费层 + 付费 | 限速 | 欧洲 AI |
| | OpenRouter | 按使用量 | 无 | 100+ 模型 |
| **💰 低价** | GLM-4.7 | $0.6/1M | 每日 10时 | 经济备用 |
| | MiniMax M2.1 | $0.2/1M | 5小时滚动 | 最便宜选项 |
| | Kimi K2 | $9/月固定 | 每月 10M Token | 可预测成本 |
| **🆓 免费** | iFlow | $0 | 无限制 | 8 个免费模型 |
| | Qwen | $0 | 无限制 | 3 个免费模型 |
| | Kiro | $0 | 无限制 | 免费 Claude |
**💡 专业建议:** 从 Gemini CLI(每月 180K 免费)+ iFlow(无限免费)开始 = $0 成本!
---
## 🎯 使用场景
### 场景 1"我有 Claude Pro 订阅"
**问题:** 配额未使用就过期,编程高峰期遇到速率限制
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (充分使用订阅)
2. glm/glm-4.7 (配额用完时的便宜备用)
3. if/kimi-k2-thinking (免费应急后备)
每月成本:$20(订阅)+ ~$5(备用)= $25 总计
对比:$20 + 遇到限制 = 受挫
```
### 场景 2"我想要零成本"
**问题:** 无法承担订阅费用,需要可靠的 AI 编程
```
Combo: "free-forever"
1. gc/gemini-3-flash (每月 180K 免费)
2. if/kimi-k2-thinking (无限免费)
3. qw/qwen3-coder-plus (无限免费)
每月成本:$0
质量:生产级模型
```
### 场景 3"我需要 24/7 编程,不中断"
**问题:** 截止日期紧迫,不能有停机时间
```
Combo: "always-on"
1. cc/claude-opus-4-6 (最佳质量)
2. cx/gpt-5.2-codex (第二个订阅)
3. glm/glm-4.7 (便宜,每日重置)
4. minimax/MiniMax-M2.1 (最便宜,5小时重置)
5. if/kimi-k2-thinking (免费无限制)
结果:5 层故障转移 = 零停机
```
### 场景 4"我想在 OpenClaw 中使用免费 AI"
**问题:** 需要在消息应用中使用 AI 助手,完全免费
```
Combo: "openclaw-free"
1. if/glm-4.7 (无限免费)
2. if/minimax-m2.1 (无限免费)
3. if/kimi-k2-thinking (无限免费)
每月成本:$0
访问方式:WhatsApp、Telegram、Slack、Discord、iMessage、Signal...
```
---
## 💡 核心功能
### 🧠 路由与智能
| 功能 | 功能描述 |
| ------------------------- | -------------------------------------------------------------------------- |
| 🎯 **智能 4 层故障转移** | 自动路由:订阅 → API Key → 低价 → 免费 |
| 📊 **实时配额追踪** | 实时 Token 计数 + 每个提供商的重置倒计时 |
| 🔄 **格式转换** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro 无缝切换 |
| 👥 **多账号支持** | 每个提供商多个账号,智能选择 |
| 🔄 **自动令牌刷新** | OAuth 令牌自动刷新并重试 |
| 🎨 **自定义组合** | 6 种策略:fill-first、round-robin、p2c、random、least-used、cost-optimized |
| 🧩 **自定义模型** | 为任何提供商添加任何模型 ID |
| 🌐 **通配符路由** | 动态路由 `provider/*` 模式到任何提供商 |
| 🧠 **推理预算** | passthrough、auto、custom 和 adaptive 模式用于推理模型 |
| 💬 **System Prompt 注入** | 全局 System Prompt 应用于所有请求 |
| 📄 **Responses API** | 完整支持 OpenAI Responses API (`/v1/responses`) 用于 Codex |
### 🎵 多模态 API
| 功能 | 功能描述 |
| ----------------- | ---------------------------------------------- |
| 🖼️ **图像生成** | `/v1/images/generations` — 4 个提供商,9+ 模型 |
| 📐 **Embeddings** | `/v1/embeddings` — 6 个提供商,9+ 模型 |
| 🎤 **音频转录** | `/v1/audio/transcriptions` — Whisper 兼容 |
| 🔊 **文字转语音** | `/v1/audio/speech` — 多提供商音频合成 |
| 🛡️ **内容审核** | `/v1/moderations` — 内容安全检查 |
| 🔀 **重排序** | `/v1/rerank` — 文档相关性重排序 |
### 🛡️ 弹性与安全
| 功能 | 功能描述 |
| --------------------- | -------------------------------------- |
| 🔌 **断路器** | 每个提供商自动打开/关闭,可配置阈值 |
| 🛡️ **反惊群** | Mutex + 信号量限速用于 API Key 提供商 |
| 🧠 **语义缓存** | 两层缓存(签名 + 语义)降低成本和延迟 |
| ⚡ **请求幂等性** | 5 秒去重窗口防止重复请求 |
| 🔒 **TLS 指纹伪装** | 通过 wreq-js 绕过基于 TLS 的机器人检测 |
| 🌐 **IP 过滤** | 白名单/黑名单用于 API 访问控制 |
| 📊 **可编辑速率限制** | 可配置的 RPM、最小间隔和最大并发 |
### 📊 可观察性与分析
| 功能 | 功能描述 |
| ------------------ | ------------------------------------------ |
| 📝 **请求日志** | 调试模式,完整的请求/响应日志 |
| 💾 **SQLite 日志** | 持久化代理日志,服务器重启后仍然保留 |
| 📊 **分析仪表板** | Recharts:统计卡片、使用量图表、提供商表格 |
| 📈 **进度追踪** | 流式传输的 SSE 进度事件(可选) |
| 🧪 **LLM 评估** | 黄金集测试,4 种匹配策略 |
| 🔍 **请求遥测** | p50/p95/p99 延迟聚合 + X-Request-Id 追踪 |
| 📋 **日志 + 配额** | 专用页面用于日志浏览和配额追踪 |
| 🏥 **健康仪表板** | 运行时间、断路器状态、锁定、缓存统计 |
| 💰 **成本追踪** | 预算管理 + 每模型定价配置 |
### ☁️ 部署与同步
| 功能 | 功能描述 |
| --------------------- | ---------------------------------------------------------- |
| 💾 **Cloud Sync** | 通过 Cloudflare Workers 在设备间同步配置 |
| 🌐 **随处部署** | Localhost、VPS、Docker、Cloudflare Workers |
| 🔑 **API Key 管理** | 按提供商生成、轮换和设定 API Key 范围 |
| 🧙 **配置向导** | 4 步引导式设置,面向新用户 |
| 🔧 **CLI 工具仪表板** | 一键配置 Claude、Codex、Cline、OpenClaw、Kilo、Antigravity |
| 🔄 **数据库备份** | 自动备份和恢复所有设置 |
<details>
<summary><b>📖 功能详情</b></summary>
### 🎯 智能 4 层故障转移
创建带自动故障转移的组合:
```
Combo: "my-coding-stack"
1. cc/claude-opus-4-6 (您的订阅)
2. nvidia/llama-3.3-70b (免费 NVIDIA API
3. glm/glm-4.7 (便宜备用,$0.6/1M)
4. if/kimi-k2-thinking (免费后备)
→ 配额用完或出错时自动切换
```
### 📊 实时配额追踪
- 每个提供商的 Token 消耗
- 重置倒计时(5 小时、每日、每周)
- 付费层级的成本估算
- 月度支出报告
### 🔄 格式转换
格式间的无缝转换:
- **OpenAI****Claude****Gemini** ↔ **OpenAI Responses**
- 您的 CLI 发送 OpenAI 格式 → OmniRoute 转换 → 提供商接收原生格式
- 适用于任何支持自定义 OpenAI 端点的工具
### 👥 多账号支持
- 每个提供商添加多个账号
- 自动轮询或基于优先级的路由
- 当一个账号达到配额时自动切换到下一个
### 🔄 自动令牌刷新
- OAuth 令牌在过期前自动刷新
- 无需手动重新认证
- 所有提供商的无缝体验
### 🎨 自定义组合
- 创建无限模型组合
- 6 种策略:fill-first、round-robin、power-of-two-choices、random、least-used、cost-optimized
- 通过 Cloud Sync 在设备间共享组合
### 🏥 健康仪表板
- 系统状态(运行时间、版本、内存使用)
- 每个提供商的断路器状态(Closed/Open/Half-Open
- 速率限制状态和活动锁定
- 签名缓存统计
- 延迟遥测(p50/p95/p99+ 提示缓存
- 一键重置健康状态
### 🔧 翻译器 Playground
- 调试、测试和可视化 API 格式转换
- 发送请求并查看 OmniRoute 如何在提供商格式间转换
- 对排查集成问题非常有价值
### 💾 Cloud Sync
- 在设备间同步提供商、组合和设置
- 自动后台同步
- 安全加密存储
</details>
---
## 📖 设置指南
<details>
<summary><b>💳 订阅提供商</b></summary>
### Claude Code (Pro/Max)
```bash
仪表板 → 提供商 → 连接 Claude Code
→ OAuth 登录 → 自动令牌刷新
→ 5 小时 + 每周配额追踪
模型:
cc/claude-opus-4-6
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
```
**专业建议:** 复杂任务用 Opus,追求速度用 Sonnet。OmniRoute 按模型追踪配额!
### OpenAI Codex (Plus/Pro)
```bash
仪表板 → 提供商 → 连接 Codex
→ OAuth 登录(端口 1455
→ 5 小时 + 每周重置
模型:
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
```
### Gemini CLI(免费 180K/月!)
```bash
仪表板 → 提供商 → 连接 Gemini CLI
→ Google OAuth
→ 每月 180K completions + 每天 1K
模型:
gc/gemini-3-flash-preview
gc/gemini-2.5-pro
```
**最佳价值:** 巨大的免费额度!在付费层级之前使用。
### GitHub Copilot
```bash
仪表板 → 提供商 → 连接 GitHub
→ 通过 GitHub OAuth
→ 每月重置(每月 1 日)
模型:
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3-pro
```
</details>
<details>
<summary><b>🔑 API Key 提供商</b></summary>
### NVIDIA NIM(免费 1000 积分!)
1. 注册:[build.nvidia.com](https://build.nvidia.com)
2. 获取免费 API key(包含 1000 推理积分)
3. 仪表板 → 添加提供商 → NVIDIA NIM
- API Key`nvapi-your-key`
**模型:** `nvidia/llama-3.3-70b-instruct``nvidia/mistral-7b-instruct` 及 50+ 更多
**专业建议:** OpenAI 兼容的 API — 与 OmniRoute 的格式转换完美配合!
### DeepSeek
1. 注册:[platform.deepseek.com](https://platform.deepseek.com)
2. 获取 API key
3. 仪表板 → 添加提供商 → DeepSeek
**模型:** `deepseek/deepseek-chat``deepseek/deepseek-coder`
### Groq(免费层可用!)
1. 注册:[console.groq.com](https://console.groq.com)
2. 获取 API key(包含免费层)
3. 仪表板 → 添加提供商 → Groq
**模型:** `groq/llama-3.3-70b``groq/mixtral-8x7b`
**专业建议:** 超快推理 — 最适合实时编程!
### OpenRouter100+ 模型)
1. 注册:[openrouter.ai](https://openrouter.ai)
2. 获取 API key
3. 仪表板 → 添加提供商 → OpenRouter
**模型:** 通过一个 API key 访问所有主要提供商的 100+ 模型。
</details>
<details>
<summary><b>💰 低价提供商(备用)</b></summary>
### GLM-4.7(每日重置,$0.6/1M
1. 注册:[Zhipu AI](https://open.bigmodel.cn/)
2. 从 Coding Plan 获取 API key
3. 仪表板 → 添加 API Key
- 提供商:`glm`
- API Key`your-key`
**使用:** `glm/glm-4.7`
**专业建议:** Coding Plan 以 1/7 的价格提供 3 倍配额!每日 10:00 AM 重置。
### MiniMax M2.15 小时重置,$0.20/1M
1. 注册:[MiniMax](https://www.minimax.io/)
2. 获取 API key
3. 仪表板 → 添加 API Key
**使用:** `minimax/MiniMax-M2.1`
**专业建议:** 长上下文(1M Token)最便宜的选项!
### Kimi K2$9/月固定)
1. 订阅:[Moonshot AI](https://platform.moonshot.ai/)
2. 获取 API key
3. 仪表板 → 添加 API Key
**使用:** `kimi/kimi-latest`
**专业建议:** 固定 $9/月 10M Token = $0.90/1M 有效成本!
</details>
<details>
<summary><b>🆓 免费提供商(应急备用)</b></summary>
### iFlow8 个免费模型)
```bash
仪表板 → 连接 iFlow
→ iFlow OAuth 登录
→ 无限使用
模型:
if/kimi-k2-thinking
if/qwen3-coder-plus
if/glm-4.7
if/minimax-m2
if/deepseek-r1
```
### Qwen3 个免费模型)
```bash
仪表板 → 连接 Qwen
→ 设备码授权
→ 无限使用
模型:
qw/qwen3-coder-plus
qw/qwen3-coder-flash
```
### Kiro(免费 Claude
```bash
仪表板 → 连接 Kiro
→ AWS Builder ID 或 Google/GitHub
→ 无限使用
模型:
kr/claude-sonnet-4.5
kr/claude-haiku-4.5
```
</details>
<details>
<summary><b>🎨 创建组合</b></summary>
### 示例 1:最大化订阅 → 便宜备用
```
仪表板 → 组合 → 创建新的
名称:premium-coding
模型:
1. cc/claude-opus-4-6(订阅主力)
2. glm/glm-4.7(便宜备用,$0.6/1M
3. minimax/MiniMax-M2.1(最便宜的后备,$0.20/1M
在 CLI 中使用:premium-coding
```
### 示例 2:仅免费(零成本)
```
名称:free-combo
模型:
1. gc/gemini-3-flash-preview(每月 180K 免费)
2. if/kimi-k2-thinking(无限制)
3. qw/qwen3-coder-plus(无限制)
成本:永远 $0
```
</details>
<details>
<summary><b>🔧 CLI 集成</b></summary>
### Cursor IDE
```
设置 → 模型 → 高级:
OpenAI API Base URL: http://localhost:20128/v1
OpenAI API Key: [从 OmniRoute 仪表板获取]
Model: cc/claude-opus-4-6
```
### Claude Code
使用仪表板中的 **CLI Tools** 页面一键配置,或手动编辑 `~/.claude/settings.json`
### Codex CLI
```bash
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
```
### OpenClaw
**选项 1 — 仪表板(推荐):**
```
仪表板 → CLI Tools → OpenClaw → 选择模型 → 应用
```
**选项 2 — 手动:** 编辑 `~/.openclaw/openclaw.json`
```json
{
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://127.0.0.1:20128/v1",
"apiKey": "sk_omniroute",
"api": "openai-completions"
}
}
}
}
```
> **注意:** OpenClaw 仅支持本地 OmniRoute。使用 `127.0.0.1` 而非 `localhost` 以避免 IPv6 解析问题。
### Cline / Continue / RooCode
```
设置 → API 配置:
提供商:OpenAI Compatible
Base URL: http://localhost:20128/v1
API Key: [从 OmniRoute 仪表板获取]
Model: if/kimi-k2-thinking
```
</details>
---
## 📊 可用模型
<details>
<summary><b>查看所有可用模型</b></summary>
**Claude Code (`cc/`)** - Pro/Max:
- `cc/claude-opus-4-6`
- `cc/claude-sonnet-4-5-20250929`
- `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** - Plus/Pro:
- `cx/gpt-5.2-codex`
- `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** - 免费:
- `gc/gemini-3-flash-preview`
- `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**:
- `gh/gpt-5`
- `gh/claude-4.5-sonnet`
**NVIDIA NIM (`nvidia/`)** - 免费积分:
- `nvidia/llama-3.3-70b-instruct`
- `nvidia/mistral-7b-instruct`
- 50+ 更多模型在 [build.nvidia.com](https://build.nvidia.com)
**GLM (`glm/`)** - $0.6/1M:
- `glm/glm-4.7`
**MiniMax (`minimax/`)** - $0.2/1M:
- `minimax/MiniMax-M2.1`
**iFlow (`if/`)** - 免费:
- `if/kimi-k2-thinking`
- `if/qwen3-coder-plus`
- `if/deepseek-r1`
- `if/glm-4.7`
- `if/minimax-m2`
**Qwen (`qw/`)** - 免费:
- `qw/qwen3-coder-plus`
- `qw/qwen3-coder-flash`
**Kiro (`kr/`)** - 免费:
- `kr/claude-sonnet-4.5`
- `kr/claude-haiku-4.5`
**OpenRouter (`or/`)** - 100+ 模型:
- `or/anthropic/claude-4-sonnet`
- `or/google/gemini-2.5-pro`
- [openrouter.ai/models](https://openrouter.ai/models) 上的任何模型
</details>
---
## 🧪 评估 (Evals)
OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质量。通过仪表板中的 **Analytics → Evals** 访问。
### 内置黄金集
预加载的「OmniRoute Golden Set」包含 10 个测试用例:
- 问候、数学、地理、代码生成
- JSON 格式合规性、翻译、markdown
- 安全拒绝(有害内容)、计数、布尔逻辑
### 评估策略
| 策略 | 描述 | 示例 |
| ---------- | -------------------------------- | -------------------------------- |
| `exact` | 输出必须完全匹配 | `"4"` |
| `contains` | 输出必须包含子串(不区分大小写) | `"Paris"` |
| `regex` | 输出必须匹配正则表达式模式 | `"1.*2.*3"` |
| `custom` | 自定义 JS 函数返回 true/false | `(output) => output.length > 10` |
---
## 🐛 故障排除
<details>
<summary><b>点击展开故障排除指南</b></summary>
**"Language model did not provide messages"**
- 提供商配额已耗尽 → 检查仪表板配额追踪器
- 解决方案:使用组合故障转移或切换到更便宜的层级
**速率限制**
- 订阅配额耗尽 → 回退到 GLM/MiniMax
- 添加组合:`cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
**OAuth 令牌过期**
- OmniRoute 自动刷新
- 如果问题持续:仪表板 → 提供商 → 重新连接
**高成本**
- 在仪表板 → 成本中检查使用统计
- 将主要模型切换为 GLM/MiniMax
- 对非关键任务使用免费层(Gemini CLI、iFlow
**仪表板在错误端口打开**
- 设置 `PORT=20128``NEXT_PUBLIC_BASE_URL=http://localhost:20128`
**Cloud sync 错误**
- 验证 `BASE_URL` 指向您正在运行的实例
- 验证 `CLOUD_URL` 指向预期的云端点
- 保持 `NEXT_PUBLIC_*` 值与服务器端值一致
**首次登录不工作**
- 检查 `.env` 中的 `INITIAL_PASSWORD`
- 如未设置,默认密码为 `123456`
**没有请求日志**
- 在 `.env` 中设置 `ENABLE_REQUEST_LOGS=true`
**兼容 OpenAI 的提供商连接测试显示 "Invalid"**
- 许多提供商不暴露 `/models` 端点
- OmniRoute v1.0.6+ 包含通过 chat completions 的回退验证
- 确保 base URL 包含 `/v1` 后缀
</details>
---
## 🛠️ 技术栈
- **运行时**: Node.js 20+
- **语言**: TypeScript 5.9 — `src/``open-sse/`**100% TypeScript**v1.0.6
- **框架**: Next.js 16 + React 19 + Tailwind CSS 4
- **数据库**: LowDB (JSON) + SQLite(领域状态 + 代理日志)
- **流式传输**: Server-Sent Events (SSE)
- **认证**: OAuth 2.0 (PKCE) + JWT + API Keys
- **测试**: Node.js test runner368+ 单元测试)
- **CI/CD**: GitHub Actions(发布时自动 npm 发布 + Docker Hub
- **网站**: [omniroute.online](https://omniroute.online)
- **包**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
- **弹性**: 断路器、指数退避、反惊群、TLS 伪装
---
## 📖 文档
| 文档 | 描述 |
| ----------------------------------- | ---------------------------- |
| [用户指南](docs/USER_GUIDE.md) | 提供商、组合、CLI 集成、部署 |
| [API 参考](docs/API_REFERENCE.md) | 所有端点及示例 |
| [故障排除](docs/TROUBLESHOOTING.md) | 常见问题和解决方案 |
| [架构](docs/ARCHITECTURE.md) | 系统架构和内部机制 |
| [贡献指南](CONTRIBUTING.md) | 开发设置和指南 |
| [OpenAPI 规范](docs/openapi.yaml) | OpenAPI 3.0 规范 |
| [安全策略](SECURITY.md) | 漏洞报告和安全实践 |
---
## 📧 支持
> 💬 **加入我们的社区!** [WhatsApp 群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — 获取帮助、分享技巧、了解最新动态。
- **网站**: [omniroute.online](https://omniroute.online)
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **WhatsApp**: [社区群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
- **原始项目**: [decolua 的 9router](https://github.com/decolua/9router)
---
## 👥 贡献者
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
### 如何贡献
1. Fork 仓库
2. 创建功能分支(`git checkout -b feature/amazing-feature`
3. 提交更改(`git commit -m 'Add amazing feature'`
4. 推送到分支(`git push origin feature/amazing-feature`
5. 打开 Pull Request
详细指南请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
### 发布新版本
```bash
# 创建发布 — npm 发布自动完成
gh release create v1.0.6 --title "v1.0.6" --generate-notes
```
---
## 📊 Star 历史
<a href="https://star-history.com/#diegosouzapw/OmniRoute&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=diegosouzapw/OmniRoute&type=Date" />
</picture>
</a>
---
## 🙏 致谢
特别感谢 **[decolua](https://github.com/decolua)** 的 **[9router](https://github.com/decolua/9router)** — 启发了本 fork 的原始项目。OmniRoute 在这个令人难以置信的基础上添加了额外功能、多模态 API 和完整的 TypeScript 重写。
特别感谢 **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — 启发了本 JavaScript 移植的原始 Go 实现。
---
## 📄 许可证
MIT 许可证 — 详见 [LICENSE](LICENSE)。
---
<div align="center">
<sub>用 ❤️ 为 24/7 编程的开发者打造</sub>
<br/>
<sub><a href="https://omniroute.online">omniroute.online</a></sub>
</div>
+13 -161
View File
@@ -1,169 +1,21 @@
# Security Policy
## Reporting Vulnerabilities
If you discover a security vulnerability in OmniRoute, please report it responsibly:
1. **DO NOT** open a public GitHub issue
2. Use [GitHub Security Advisories](https://github.com/diegosouzapw/OmniRoute/security/advisories/new)
3. Include: description, reproduction steps, and potential impact
## Response Timeline
| Stage | Target |
| ------------------- | --------------------------- |
| Acknowledgment | 48 hours |
| Triage & Assessment | 5 business days |
| Patch Release | 14 business days (critical) |
## Supported Versions
| Version | Support Status |
| ------- | -------------- |
| 1.0.x | ✅ Active |
| 0.8.x | ✅ Security |
| < 0.8.0 | ❌ Unsupported |
Use this section to tell people about which versions of your project are
currently being supported with security updates.
---
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Security Architecture
## Reporting a Vulnerability
OmniRoute implements a multi-layered security model:
Use this section to tell people how to report a vulnerability.
```
Request → CORS → API Key Auth → Prompt Injection Guard → Input Sanitizer → Rate Limiter → Circuit Breaker → Provider
```
### 🔐 Authentication & Authorization
| Feature | Implementation |
| -------------------- | ---------------------------------------------------------- |
| **Dashboard Login** | Password-based auth with JWT tokens (HttpOnly cookies) |
| **API Key Auth** | HMAC-signed keys with CRC validation |
| **OAuth 2.0 + PKCE** | Secure provider auth (Claude, Codex, Gemini, Cursor, etc.) |
| **Token Refresh** | Automatic OAuth token refresh before expiry |
| **Secure Cookies** | `AUTH_COOKIE_SECURE=true` for HTTPS environments |
### 🛡️ Encryption at Rest
All sensitive data stored in SQLite is encrypted using **AES-256-GCM** with scrypt key derivation:
- API keys, access tokens, refresh tokens, and ID tokens
- Versioned format: `enc:v1:<iv>:<ciphertext>:<authTag>`
- Passthrough mode (plaintext) when `STORAGE_ENCRYPTION_KEY` is not set
```bash
# Generate encryption key:
STORAGE_ENCRYPTION_KEY=$(openssl rand -hex 32)
```
### 🧠 Prompt Injection Guard
Middleware that detects and blocks prompt injection attacks in LLM requests:
| Pattern Type | Severity | Example |
| ------------------- | -------- | ---------------------------------------------- |
| System Override | High | "ignore all previous instructions" |
| Role Hijack | High | "you are now DAN, you can do anything" |
| Delimiter Injection | Medium | Encoded separators to break context boundaries |
| DAN/Jailbreak | High | Known jailbreak prompt patterns |
| Instruction Leak | Medium | "show me your system prompt" |
Configure via dashboard (Settings → Security) or `.env`:
```env
INPUT_SANITIZER_ENABLED=true
INPUT_SANITIZER_MODE=block # warn | block | redact
```
### 🔒 PII Redaction
Automatic detection and optional redaction of personally identifiable information:
| PII Type | Pattern | Replacement |
| ------------- | --------------------- | ------------------ |
| Email | `user@domain.com` | `[EMAIL_REDACTED]` |
| CPF (Brazil) | `123.456.789-00` | `[CPF_REDACTED]` |
| CNPJ (Brazil) | `12.345.678/0001-00` | `[CNPJ_REDACTED]` |
| Credit Card | `4111-1111-1111-1111` | `[CC_REDACTED]` |
| Phone | `+55 11 99999-9999` | `[PHONE_REDACTED]` |
| SSN (US) | `123-45-6789` | `[SSN_REDACTED]` |
```env
PII_REDACTION_ENABLED=true
```
### 🌐 Network Security
| Feature | Description |
| ------------------------ | ---------------------------------------------------------------- |
| **CORS** | Configurable origin control (`CORS_ORIGIN` env var, default `*`) |
| **IP Filtering** | Whitelist/blacklist IP ranges in dashboard |
| **Rate Limiting** | Per-provider rate limits with automatic backoff |
| **Anti-Thundering Herd** | Mutex + per-connection locking prevents cascading 502s |
### 🔌 Resilience & Availability
| Feature | Description |
| ----------------------- | ------------------------------------------------------------------ |
| **Circuit Breaker** | 3-state (Closed → Open → Half-Open) per provider, SQLite-persisted |
| **Request Idempotency** | 5-second dedup window for duplicate requests |
| **Exponential Backoff** | Automatic retry with increasing delays |
| **Health Dashboard** | Real-time provider health monitoring |
### 📋 Compliance
| Feature | Description |
| ------------------ | --------------------------------------------------- |
| **Log Retention** | Automatic cleanup after `LOG_RETENTION_DAYS` |
| **No-Log Opt-out** | Per API key `noLog` flag disables request logging |
| **Audit Log** | Administrative actions tracked in `audit_log` table |
---
## Required Environment Variables
All secrets must be set before starting the server. The server will **fail fast** if they are missing or weak.
```bash
# REQUIRED — server will not start without these:
JWT_SECRET=$(openssl rand -base64 48) # min 32 chars
API_KEY_SECRET=$(openssl rand -hex 32) # min 16 chars
# RECOMMENDED — enables encryption at rest:
STORAGE_ENCRYPTION_KEY=$(openssl rand -hex 32)
```
The server actively rejects known-weak values like `changeme`, `secret`, or `password`.
---
## Docker Security
- Use non-root user in production
- Mount secrets as read-only volumes
- Never copy `.env` files into Docker images
- Use `.dockerignore` to exclude sensitive files
- Set `AUTH_COOKIE_SECURE=true` when behind HTTPS
```bash
docker run -d \
--name omniroute \
--restart unless-stopped \
--read-only \
-p 20128:20128 \
-v omniroute-data:/app/data \
-e JWT_SECRET="$(openssl rand -base64 48)" \
-e API_KEY_SECRET="$(openssl rand -hex 32)" \
-e STORAGE_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
diegosouzapw/omniroute:latest
```
---
## Dependencies
- Run `npm audit` regularly
- Keep dependencies updated
- The project uses `husky` + `lint-staged` for pre-commit checks
- CI pipeline runs ESLint security rules on every push
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-116
View File
@@ -1,116 +0,0 @@
#!/usr/bin/env node
/**
* Password Reset CLI T-38
*
* Usage:
* node bin/reset-password.mjs
* npx omniroute reset-password
*
* Resets the admin password for OmniRoute.
* Prompts for a new password and updates the database directly.
*
* @module bin/reset-password
*/
import { createInterface } from "node:readline";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Resolve data directory — same logic as the server
const DATA_DIR = process.env.DATA_DIR || resolve(__dirname, "..", "data");
const DB_PATH = resolve(DATA_DIR, "settings.db");
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
function ask(question) {
return new Promise((resolve) => rl.question(question, resolve));
}
function hashPassword(password) {
return createHash("sha256").update(password).digest("hex");
}
console.log("\n🔑 OmniRoute — Password Reset\n");
async function main() {
// Check if database exists
if (!existsSync(DB_PATH)) {
console.error(`❌ Database not found at: ${DB_PATH}`);
console.error(` Make sure OmniRoute has been started at least once.`);
console.error(` Or set DATA_DIR env var to your data directory.\n`);
process.exit(1);
}
let Database;
try {
Database = (await import("better-sqlite3")).default;
} catch {
console.error("❌ better-sqlite3 not installed. Run: npm install");
process.exit(1);
}
const db = new Database(DB_PATH);
// Check current settings
const row = db.prepare("SELECT value FROM settings WHERE key = 'password'").get();
if (row) {
console.log("️ A password is currently set.");
} else {
console.log("️ No password is currently set.");
}
const password = await ask("Enter new password (min 4 chars): ");
if (!password || password.length < 4) {
console.error("\n❌ Password must be at least 4 characters.\n");
db.close();
rl.close();
process.exit(1);
}
const confirm = await ask("Confirm new password: ");
if (password !== confirm) {
console.error("\n❌ Passwords do not match.\n");
db.close();
rl.close();
process.exit(1);
}
const hashed = hashPassword(password);
// Upsert the password
const stmt = db.prepare(`
INSERT INTO settings (key, value) VALUES ('password', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`);
stmt.run(hashed);
// Also ensure requireLogin is true
const loginStmt = db.prepare(`
INSERT INTO settings (key, value) VALUES ('requireLogin', 'true')
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`);
loginStmt.run();
db.close();
rl.close();
console.log("\n✅ Password reset successfully!");
console.log(" Restart OmniRoute for changes to take effect.\n");
}
main().catch((err) => {
console.error(`\n❌ Error: ${err.message}\n`);
rl.close();
process.exit(1);
});
Submodule clipr/9router deleted from bc91be7305
Submodule clipr/CLIProxyAPI deleted from 068630dbd0
-2
View File
@@ -18,8 +18,6 @@
x-common: &common
restart: unless-stopped
env_file: .env
environment:
- DATA_DIR=/app/data # Must match the volume mount below
volumes:
- omniroute-data:/app/data
healthcheck:
-439
View File
@@ -1,439 +0,0 @@
# API Reference
Complete reference for all OmniRoute API endpoints.
---
## Table of Contents
- [Chat Completions](#chat-completions)
- [Embeddings](#embeddings)
- [Image Generation](#image-generation)
- [List Models](#list-models)
- [Compatibility Endpoints](#compatibility-endpoints)
- [Semantic Cache](#semantic-cache)
- [Dashboard & Management](#dashboard--management)
- [Request Processing](#request-processing)
- [Authentication](#authentication)
---
## Chat Completions
```bash
POST /v1/chat/completions
Authorization: Bearer your-api-key
Content-Type: application/json
{
"model": "cc/claude-opus-4-6",
"messages": [
{"role": "user", "content": "Write a function to..."}
],
"stream": true
}
```
### Custom Headers
| Header | Direction | Description |
| ------------------------ | --------- | --------------------------------- |
| `X-OmniRoute-No-Cache` | Request | Set to `true` to bypass cache |
| `X-OmniRoute-Progress` | Request | Set to `true` for progress events |
| `Idempotency-Key` | Request | Dedup key (5s window) |
| `X-Request-Id` | Request | Alternative dedup key |
| `X-OmniRoute-Cache` | Response | `HIT` or `MISS` (non-streaming) |
| `X-OmniRoute-Idempotent` | Response | `true` if deduplicated |
| `X-OmniRoute-Progress` | Response | `enabled` if progress tracking on |
---
## Embeddings
```bash
POST /v1/embeddings
Authorization: Bearer your-api-key
Content-Type: application/json
{
"model": "nebius/Qwen/Qwen3-Embedding-8B",
"input": "The food was delicious"
}
```
Available providers: Nebius, OpenAI, Mistral, Together AI, Fireworks, NVIDIA.
```bash
# List all embedding models
GET /v1/embeddings
```
---
## Image Generation
```bash
POST /v1/images/generations
Authorization: Bearer your-api-key
Content-Type: application/json
{
"model": "openai/dall-e-3",
"prompt": "A beautiful sunset over mountains",
"size": "1024x1024"
}
```
Available providers: OpenAI (DALL-E), xAI (Grok Image), Together AI (FLUX), Fireworks AI.
```bash
# List all image models
GET /v1/images/generations
```
---
## List Models
```bash
GET /v1/models
Authorization: Bearer your-api-key
→ Returns all chat, embedding, and image models + combos in OpenAI format
```
---
## Compatibility Endpoints
| Method | Path | Format |
| ------ | --------------------------- | ---------------------- |
| POST | `/v1/chat/completions` | OpenAI |
| POST | `/v1/messages` | Anthropic |
| POST | `/v1/responses` | OpenAI Responses |
| POST | `/v1/embeddings` | OpenAI |
| POST | `/v1/images/generations` | OpenAI |
| GET | `/v1/models` | OpenAI |
| POST | `/v1/messages/count_tokens` | Anthropic |
| GET | `/v1beta/models` | Gemini |
| POST | `/v1beta/models/{...path}` | Gemini generateContent |
| POST | `/v1/api/chat` | Ollama |
### Dedicated Provider Routes
```bash
POST /v1/providers/{provider}/chat/completions
POST /v1/providers/{provider}/embeddings
POST /v1/providers/{provider}/images/generations
```
The provider prefix is auto-added if missing. Mismatched models return `400`.
---
## Semantic Cache
```bash
# Get cache stats
GET /api/cache
# Clear all caches
DELETE /api/cache
```
Response example:
```json
{
"semanticCache": {
"memorySize": 42,
"memoryMaxSize": 500,
"dbSize": 128,
"hitRate": 0.65
},
"idempotency": {
"activeKeys": 3,
"windowMs": 5000
}
}
```
---
## Dashboard & Management
### Authentication
| Endpoint | Method | Description |
| ----------------------------- | ------- | --------------------- |
| `/api/auth/login` | POST | Login |
| `/api/auth/logout` | POST | Logout |
| `/api/settings/require-login` | GET/PUT | Toggle login required |
### Provider Management
| Endpoint | Method | Description |
| ---------------------------- | --------------- | ------------------------ |
| `/api/providers` | GET/POST | List / create providers |
| `/api/providers/[id]` | GET/PUT/DELETE | Manage a provider |
| `/api/providers/[id]/test` | POST | Test provider connection |
| `/api/providers/[id]/models` | GET | List provider models |
| `/api/providers/validate` | POST | Validate provider config |
| `/api/provider-nodes*` | Various | Provider node management |
| `/api/provider-models` | GET/POST/DELETE | Custom models |
### OAuth Flows
| Endpoint | Method | Description |
| -------------------------------- | ------- | ----------------------- |
| `/api/oauth/[provider]/[action]` | Various | Provider-specific OAuth |
### Routing & Config
| Endpoint | Method | Description |
| --------------------- | -------- | ----------------------------- |
| `/api/models/alias` | GET/POST | Model aliases |
| `/api/models/catalog` | GET | All models by provider + type |
| `/api/combos*` | Various | Combo management |
| `/api/keys*` | Various | API key management |
| `/api/pricing` | GET | Model pricing |
### Usage & Analytics
| Endpoint | Method | Description |
| --------------------------- | ------ | -------------------- |
| `/api/usage/history` | GET | Usage history |
| `/api/usage/logs` | GET | Usage logs |
| `/api/usage/request-logs` | GET | Request-level logs |
| `/api/usage/[connectionId]` | GET | Per-connection usage |
### Settings
| Endpoint | Method | Description |
| ------------------------------- | ------- | ---------------------- |
| `/api/settings` | GET/PUT | General settings |
| `/api/settings/proxy` | GET/PUT | Network proxy config |
| `/api/settings/proxy/test` | POST | Test proxy connection |
| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist |
| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget |
| `/api/settings/system-prompt` | GET/PUT | Global system prompt |
### Monitoring
| Endpoint | Method | Description |
| ------------------------ | ---------- | ----------------------- |
| `/api/sessions` | GET | Active session tracking |
| `/api/rate-limits` | GET | Per-account rate limits |
| `/api/monitoring/health` | GET | Health check |
| `/api/cache` | GET/DELETE | Cache stats / clear |
### Backup & Export/Import
| Endpoint | Method | Description |
| --------------------------- | ------ | --------------------------------------- |
| `/api/db-backups` | GET | List available backups |
| `/api/db-backups` | PUT | Create a manual backup |
| `/api/db-backups` | POST | Restore from a specific backup |
| `/api/db-backups/export` | GET | Download database as .sqlite file |
| `/api/db-backups/import` | POST | Upload .sqlite file to replace database |
| `/api/db-backups/exportAll` | GET | Download full backup as .tar.gz archive |
### Cloud Sync
| Endpoint | Method | Description |
| ---------------------- | ------- | --------------------- |
| `/api/sync/cloud` | Various | Cloud sync operations |
| `/api/sync/initialize` | POST | Initialize sync |
| `/api/cloud/*` | Various | Cloud management |
### CLI Tools
| Endpoint | Method | Description |
| ---------------------------------- | ------ | ------------------- |
| `/api/cli-tools/claude-settings` | GET | Claude CLI status |
| `/api/cli-tools/codex-settings` | GET | Codex CLI status |
| `/api/cli-tools/droid-settings` | GET | Droid CLI status |
| `/api/cli-tools/openclaw-settings` | GET | OpenClaw CLI status |
| `/api/cli-tools/runtime/[toolId]` | GET | Generic CLI runtime |
CLI responses include: `installed`, `runnable`, `command`, `commandPath`, `runtimeMode`, `reason`.
### Resilience & Rate Limits
| Endpoint | Method | Description |
| ----------------------- | ------- | ------------------------------- |
| `/api/resilience` | GET/PUT | Get/update resilience profiles |
| `/api/resilience/reset` | POST | Reset circuit breakers |
| `/api/rate-limits` | GET | Per-account rate limit status |
| `/api/rate-limit` | GET | Global rate limit configuration |
### Evals
| Endpoint | Method | Description |
| ------------ | -------- | --------------------------------- |
| `/api/evals` | GET/POST | List eval suites / run evaluation |
### Policies
| Endpoint | Method | Description |
| --------------- | --------------- | ----------------------- |
| `/api/policies` | GET/POST/DELETE | Manage routing policies |
### Compliance
| Endpoint | Method | Description |
| --------------------------- | ------ | ----------------------------- |
| `/api/compliance/audit-log` | GET | Compliance audit log (last N) |
### v1beta (Gemini-Compatible)
| Endpoint | Method | Description |
| -------------------------- | ------ | --------------------------------- |
| `/v1beta/models` | GET | List models in Gemini format |
| `/v1beta/models/{...path}` | POST | Gemini `generateContent` endpoint |
These endpoints mirror Gemini's API format for clients that expect native Gemini SDK compatibility.
### Internal / System APIs
| Endpoint | Method | Description |
| --------------- | ------ | ---------------------------------------------------- |
| `/api/init` | GET | Application initialization check (used on first run) |
| `/api/tags` | GET | Ollama-compatible model tags (for Ollama clients) |
| `/api/restart` | POST | Trigger graceful server restart |
| `/api/shutdown` | POST | Trigger graceful server shutdown |
> **Note:** These endpoints are used internally by the system or for Ollama client compatibility. They are not typically called by end users.
---
## Audio Transcription
```bash
POST /v1/audio/transcriptions
Authorization: Bearer your-api-key
Content-Type: multipart/form-data
```
Transcribe audio files using Deepgram or AssemblyAI.
**Request:**
```bash
curl -X POST http://localhost:20128/v1/audio/transcriptions \
-H "Authorization: Bearer your-api-key" \
-F "file=@recording.mp3" \
-F "model=deepgram/nova-3"
```
**Response:**
```json
{
"text": "Hello, this is the transcribed audio content.",
"task": "transcribe",
"language": "en",
"duration": 12.5
}
```
**Supported providers:** `deepgram/nova-3`, `assemblyai/best`.
**Supported formats:** `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`.
---
## Ollama Compatibility
For clients that use Ollama's API format:
```bash
# Chat endpoint (Ollama format)
POST /v1/api/chat
# Model listing (Ollama format)
GET /api/tags
```
Requests are automatically translated between Ollama and internal formats.
---
## Telemetry
```bash
# Get latency telemetry summary (p50/p95/p99 per provider)
GET /api/telemetry/summary
```
**Response:**
```json
{
"providers": {
"claudeCode": { "p50": 245, "p95": 890, "p99": 1200, "count": 150 },
"github": { "p50": 180, "p95": 620, "p99": 950, "count": 320 }
}
}
```
---
## Budget
```bash
# Get budget status for all API keys
GET /api/usage/budget
# Set or update a budget
POST /api/usage/budget
Content-Type: application/json
{
"keyId": "key-123",
"limit": 50.00,
"period": "monthly"
}
```
---
## Model Availability
```bash
# Get real-time model availability across all providers
GET /api/models/availability
# Check availability for a specific model
POST /api/models/availability
Content-Type: application/json
{
"model": "claude-sonnet-4-5-20250929"
}
```
---
## Request Processing
1. Client sends request to `/v1/*`
2. Route handler calls `handleChat`, `handleEmbedding`, `handleAudioTranscription`, or `handleImageGeneration`
3. Model is resolved (direct provider/model or alias/combo)
4. Credentials selected from local DB with account availability filtering
5. For chat: `handleChatCore` — format detection, translation, cache check, idempotency check
6. Provider executor sends upstream request
7. Response translated back to client format (chat) or returned as-is (embeddings/images/audio)
8. Usage/logging recorded
9. Fallback applies on errors according to combo rules
Full architecture reference: [`ARCHITECTURE.md`](ARCHITECTURE.md)
---
## Authentication
- Dashboard routes (`/dashboard/*`) use `auth_token` cookie
- Login uses saved password hash; fallback to `INITIAL_PASSWORD`
- `requireLogin` toggleable via `/api/settings/require-login`
- `/v1/*` routes optionally require Bearer API key when `REQUIRE_API_KEY=true`
+56 -169
View File
@@ -1,6 +1,6 @@
# OmniRoute Architecture
_Last updated: 2026-02-18_
_Last updated: 2026-02-09_
## Executive Summary
@@ -17,29 +17,9 @@ Core capabilities:
- Embedding generation via `/v1/embeddings` (6 providers, 9 models)
- Image generation via `/v1/images/generations` (4 providers, 9 models)
- Think tag parsing (`<think>...</think>`) for reasoning models
- Response sanitization for strict OpenAI SDK compatibility
- Role normalization (developer→system, system→user) for cross-provider compatibility
- Structured output conversion (json_schema → Gemini responseSchema)
- Local persistence for providers, keys, aliases, combos, settings, pricing
- Usage/cost tracking and request logging
- Optional cloud sync for multi-device/state sync
- IP allowlist/blocklist for API access control
- Thinking budget management (passthrough/auto/custom/adaptive)
- Global system prompt injection
- Session tracking and fingerprinting
- Per-account enhanced rate limiting with provider-specific profiles
- Circuit breaker pattern for provider resilience
- Anti-thundering herd protection with mutex locking
- Signature-based request deduplication cache
- Domain layer: model availability, cost rules, fallback policy, lockout policy
- Domain state persistence (SQLite write-through cache for fallbacks, budgets, lockouts, circuit breakers)
- Policy engine for centralized request evaluation (lockout → budget → fallback)
- Request telemetry with p50/p95/p99 latency aggregation
- Correlation ID (X-Request-Id) for end-to-end tracing
- Compliance audit logging with opt-out per API key
- Eval framework for LLM quality assurance
- Resilience UI dashboard with real-time circuit breaker status
- Modular OAuth providers (12 individual modules under `src/lib/oauth/providers/`)
Primary runtime model:
@@ -123,18 +103,18 @@ Main directories:
Important compatibility routes:
- `src/app/api/v1/chat/completions/route.ts`
- `src/app/api/v1/messages/route.ts`
- `src/app/api/v1/responses/route.ts`
- `src/app/api/v1/models/route.ts` — includes custom models with `custom: true`
- `src/app/api/v1/embeddings/route.ts` — embedding generation (6 providers)
- `src/app/api/v1/images/generations/route.ts` — image generation (4+ providers incl. Antigravity/Nebius)
- `src/app/api/v1/messages/count_tokens/route.ts`
- `src/app/api/v1/providers/[provider]/chat/completions/route.ts` — dedicated per-provider chat
- `src/app/api/v1/providers/[provider]/embeddings/route.ts` — dedicated per-provider embeddings
- `src/app/api/v1/providers/[provider]/images/generations/route.ts` — dedicated per-provider images
- `src/app/api/v1beta/models/route.ts`
- `src/app/api/v1beta/models/[...path]/route.ts`
- `src/app/api/v1/chat/completions/route.js`
- `src/app/api/v1/messages/route.js`
- `src/app/api/v1/responses/route.js`
- `src/app/api/v1/models/route.js` — includes custom models with `custom: true`
- `src/app/api/v1/embeddings/route.js` — embedding generation (6 providers)
- `src/app/api/v1/images/generations/route.js` — image generation (4+ providers incl. Antigravity/Nebius)
- `src/app/api/v1/messages/count_tokens/route.js`
- `src/app/api/v1/providers/[provider]/chat/completions/route.js` — dedicated per-provider chat
- `src/app/api/v1/providers/[provider]/embeddings/route.js` — dedicated per-provider embeddings
- `src/app/api/v1/providers/[provider]/images/generations/route.js` — dedicated per-provider images
- `src/app/api/v1beta/models/route.js`
- `src/app/api/v1beta/models/[...path]/route.js`
Management domains:
@@ -149,111 +129,52 @@ Management domains:
- Usage: `src/app/api/usage/*`
- Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*`
- CLI tooling helpers: `src/app/api/cli-tools/*`
- IP filter: `src/app/api/settings/ip-filter` (GET/PUT)
- Thinking budget: `src/app/api/settings/thinking-budget` (GET/PUT)
- System prompt: `src/app/api/settings/system-prompt` (GET/PUT)
- Sessions: `src/app/api/sessions` (GET)
- Rate limits: `src/app/api/rate-limits` (GET)
- Resilience: `src/app/api/resilience` (GET/PATCH) — provider profiles, circuit breaker, rate limit state
- Resilience reset: `src/app/api/resilience/reset` (POST) — reset breakers + cooldowns
- Cache stats: `src/app/api/cache/stats` (GET/DELETE)
- Model availability: `src/app/api/models/availability` (GET/POST)
- Telemetry: `src/app/api/telemetry/summary` (GET)
- Budget: `src/app/api/usage/budget` (GET/POST)
- Fallback chains: `src/app/api/fallback/chains` (GET/POST/DELETE)
- Compliance audit: `src/app/api/compliance/audit-log` (GET)
- Evals: `src/app/api/evals` (GET/POST), `src/app/api/evals/[suiteId]` (GET)
- Policies: `src/app/api/policies` (GET/POST)
## 2) SSE + Translation Core
Main flow modules:
- Entry: `src/sse/handlers/chat.ts`
- Core orchestration: `open-sse/handlers/chatCore.ts`
- Entry: `src/sse/handlers/chat.js`
- Core orchestration: `open-sse/handlers/chatCore.js`
- Provider execution adapters: `open-sse/executors/*`
- Format detection/provider config: `open-sse/services/provider.ts`
- Model parse/resolve: `src/sse/services/model.ts`, `open-sse/services/model.ts`
- Account fallback logic: `open-sse/services/accountFallback.ts`
- Translation registry: `open-sse/translator/index.ts`
- Stream transformations: `open-sse/utils/stream.ts`, `open-sse/utils/streamHandler.ts`
- Usage extraction/normalization: `open-sse/utils/usageTracking.ts`
- Think tag parser: `open-sse/utils/thinkTagParser.ts`
- Embedding handler: `open-sse/handlers/embeddings.ts`
- Embedding provider registry: `open-sse/config/embeddingRegistry.ts`
- Image generation handler: `open-sse/handlers/imageGeneration.ts`
- Image provider registry: `open-sse/config/imageRegistry.ts`
- Response sanitization: `open-sse/handlers/responseSanitizer.ts`
- Role normalization: `open-sse/services/roleNormalizer.ts`
Services (business logic):
- Account selection/scoring: `open-sse/services/accountSelector.ts`
- Context lifecycle management: `open-sse/services/contextManager.ts`
- IP filter enforcement: `open-sse/services/ipFilter.ts`
- Session tracking: `open-sse/services/sessionManager.ts`
- Request deduplication: `open-sse/services/signatureCache.ts`
- System prompt injection: `open-sse/services/systemPrompt.ts`
- Thinking budget management: `open-sse/services/thinkingBudget.ts`
- Wildcard model routing: `open-sse/services/wildcardRouter.ts`
- Rate limit management: `open-sse/services/rateLimitManager.ts`
- Circuit breaker: `open-sse/services/circuitBreaker.ts`
Domain layer modules:
- Model availability: `src/lib/domain/modelAvailability.ts`
- Cost rules/budgets: `src/lib/domain/costRules.ts`
- Fallback policy: `src/lib/domain/fallbackPolicy.ts`
- Combo resolver: `src/lib/domain/comboResolver.ts`
- Lockout policy: `src/lib/domain/lockoutPolicy.ts`
- Policy engine: `src/domain/policyEngine.ts` — centralized lockout → budget → fallback evaluation
- Error codes catalog: `src/lib/domain/errorCodes.ts`
- Request ID: `src/lib/domain/requestId.ts`
- Fetch timeout: `src/lib/domain/fetchTimeout.ts`
- Request telemetry: `src/lib/domain/requestTelemetry.ts`
- Compliance/audit: `src/lib/domain/compliance/index.ts`
- Eval runner: `src/lib/domain/evalRunner.ts`
- Domain state persistence: `src/lib/db/domainState.ts` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers
OAuth provider modules (12 individual files under `src/lib/oauth/providers/`):
- Registry index: `src/lib/oauth/providers/index.ts`
- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `iflow.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts`
- Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules
- Format detection/provider config: `open-sse/services/provider.js`
- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js`
- Account fallback logic: `open-sse/services/accountFallback.js`
- Translation registry: `open-sse/translator/index.js`
- Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js`
- Usage extraction/normalization: `open-sse/utils/usageTracking.js`
- Think tag parser: `open-sse/utils/thinkTagParser.js`
- Embedding handler: `open-sse/handlers/embeddings.js`
- Embedding provider registry: `open-sse/config/embeddingRegistry.js`
- Image generation handler: `open-sse/handlers/imageGeneration.js`
- Image provider registry: `open-sse/config/imageRegistry.js`
## 3) Persistence Layer
Primary state DB:
- `src/lib/localDb.ts`
- `src/lib/localDb.js`
- file: `${DATA_DIR}/db.json` (or `$XDG_CONFIG_HOME/omniroute/db.json` when set, else `~/.omniroute/db.json`)
- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing, **customModels**, **proxyConfig**, **ipFilter**, **thinkingBudget**, **systemPrompt**
- entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing, **customModels**, **proxyConfig**
Usage DB:
- `src/lib/usageDb.ts`
- `src/lib/usageDb.js`
- files: `${DATA_DIR}/usage.json`, `${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/`
- follows same base directory policy as `localDb` (`DATA_DIR`, then `XDG_CONFIG_HOME/omniroute` when set)
- decomposed into focused sub-modules: `migrations.ts`, `usageHistory.ts`, `costCalculator.ts`, `usageStats.ts`, `callLogs.ts`
Domain State DB (SQLite):
- `src/lib/db/domainState.ts` — CRUD operations for domain state
- Tables (created in `src/lib/db/core.ts`): `domain_fallback_chains`, `domain_budgets`, `domain_cost_history`, `domain_lockout_state`, `domain_circuit_breakers`
- Write-through cache pattern: in-memory Maps are authoritative at runtime; mutations are written synchronously to SQLite; state is restored from DB on cold start
## 4) Auth + Security Surfaces
- Dashboard cookie auth: `src/proxy.ts`, `src/app/api/auth/login/route.ts`
- API key generation/verification: `src/shared/utils/apiKey.ts`
- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js`
- API key generation/verification: `src/shared/utils/apiKey.js`
- Provider secrets persisted in `providerConnections` entries
- Outbound proxy support via `open-sse/utils/proxyFetch.ts` (env vars) and `open-sse/utils/networkProxy.ts` (configurable per-provider or global)
- Outbound proxy support via `open-sse/utils/proxyFetch.js` (env vars) and `open-sse/utils/networkProxy.js` (configurable per-provider or global)
## 5) Cloud Sync
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`
- Periodic task: `src/shared/services/cloudSyncScheduler.ts`
- Control route: `src/app/api/sync/cloud/route.ts`
- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js`
- Periodic task: `src/shared/services/cloudSyncScheduler.js`
- Control route: `src/app/api/sync/cloud/route.js`
## Request Lifecycle (`/v1/chat/completions`)
@@ -332,7 +253,7 @@ flowchart TD
Q -- No --> R[Return all unavailable]
```
Fallback decisions are driven by `open-sse/services/accountFallback.ts` using status codes and error-message heuristics.
Fallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.
## OAuth Onboarding and Token Refresh Lifecycle
@@ -364,7 +285,7 @@ sequenceDiagram
Test-->>UI: validation result
```
Refresh during live traffic is executed inside `open-sse/handlers/chatCore.ts` via executor `refreshCredentials()`.
Refresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.
## Cloud Sync Lifecycle (Enable / Sync / Disable)
@@ -413,9 +334,6 @@ erDiagram
number stickyRoundRobinLimit
boolean requireLogin
string password_hash
string fallbackStrategy
json rateLimitDefaults
json providerProfiles
}
PROVIDER_CONNECTION {
@@ -481,24 +399,6 @@ erDiagram
string global
json providers
}
IP_FILTER {
string mode
string[] allowlist
string[] blocklist
}
THINKING_BUDGET {
string mode
number customBudget
string effortLevel
}
SYSTEM_PROMPT {
boolean enabled
string prompt
string position
}
```
Physical storage files:
@@ -559,33 +459,28 @@ flowchart LR
- `src/app/api/usage/*`: usage and logs APIs
- `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers
- `src/app/api/cli-tools/*`: local CLI config writers/checkers
- `src/app/api/settings/ip-filter`: IP allowlist/blocklist (GET/PUT)
- `src/app/api/settings/thinking-budget`: thinking token budget config (GET/PUT)
- `src/app/api/settings/system-prompt`: global system prompt (GET/PUT)
- `src/app/api/sessions`: active session listing (GET)
- `src/app/api/rate-limits`: per-account rate limit status (GET)
### Routing and Execution Core
- `src/sse/handlers/chat.ts`: request parse, combo handling, account selection loop
- `open-sse/handlers/chatCore.ts`: translation, executor dispatch, retry/refresh handling, stream setup
- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop
- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup
- `open-sse/executors/*`: provider-specific network and format behavior
### Translation Registry and Format Converters
- `open-sse/translator/index.ts`: translator registry and orchestration
- `open-sse/translator/index.js`: translator registry and orchestration
- Request translators: `open-sse/translator/request/*`
- Response translators: `open-sse/translator/response/*`
- Format constants: `open-sse/translator/formats.ts`
- Format constants: `open-sse/translator/formats.js`
### Persistence
- `src/lib/localDb.ts`: persistent config/state
- `src/lib/usageDb.ts`: usage history and rolling request logs
- `src/lib/localDb.js`: persistent config/state
- `src/lib/usageDb.js`: usage history and rolling request logs
## Provider Executor Coverage (Strategy Pattern)
Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/executors/base.ts`), which provides URL building, header construction, retry with exponential backoff, credential refresh hooks, and the `execute()` orchestration method.
Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/executors/base.js`), which provides URL building, header construction, retry with exponential backoff, credential refresh hooks, and the `execute()` orchestration method.
| Executor | Provider(s) | Special Handling |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
@@ -652,23 +547,16 @@ Source Format → OpenAI (hub) → Target Format
Translations are selected dynamically based on source payload shape and provider target format.
Additional processing layers in the translation pipeline:
- **Response sanitization** — Strips non-standard fields from OpenAI-format responses (both streaming and non-streaming) to ensure strict SDK compliance
- **Role normalization** — Converts `developer``system` for non-OpenAI targets; merges `system``user` for models that reject the system role (GLM, ERNIE)
- **Think tag extraction** — Parses `<think>...</think>` blocks from content into `reasoning_content` field
- **Structured output** — Converts OpenAI `response_format.json_schema` to Gemini's `responseMimeType` + `responseSchema`
## Supported API Endpoints
| Endpoint | Format | Handler |
| -------------------------------------------------- | ------------------ | ---------------------------------------------------- |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` |
| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.js` |
| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.js` |
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.js` |
| `GET /v1/embeddings` | Model listing | API route |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.js` |
| `GET /v1/images/generations` | Model listing | API route |
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation |
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation |
@@ -683,11 +571,11 @@ Additional processing layers in the translation pipeline:
## Bypass Handler
The bypass handler (`open-sse/utils/bypassHandler.ts`) intercepts known "throwaway" requests from Claude CLI — warmup pings, title extractions, and token counts — and returns a **fake response** without consuming upstream provider tokens. This is triggered only when `User-Agent` contains `claude-cli`.
The bypass handler (`open-sse/utils/bypassHandler.js`) intercepts known "throwaway" requests from Claude CLI — warmup pings, title extractions, and token counts — and returns a **fake response** without consuming upstream provider tokens. This is triggered only when `User-Agent` contains `claude-cli`.
## Request Logger Pipeline
The request logger (`open-sse/utils/requestLogger.ts`) provides a 7-stage debug logging pipeline, disabled by default, enabled via `ENABLE_REQUEST_LOGS=true`:
The request logger (`open-sse/utils/requestLogger.js`) provides a 7-stage debug logging pipeline, disabled by default, enabled via `ENABLE_REQUEST_LOGS=true`:
```
1_req_client.json → 2_req_source.json → 3_req_openai.json → 4_req_target.json
@@ -729,7 +617,7 @@ Files are written to `<repo>/logs/<session>/` for each request session.
Runtime visibility sources:
- console logs from `src/sse/utils/logger.ts`
- console logs from `src/sse/utils/logger.js`
- per-request usage aggregates in `usage.json`
- textual request status log in `log.txt`
- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`
@@ -761,18 +649,17 @@ Environment variables actively used by code:
## Known Architectural Notes
1. `usageDb` and `localDb` now share the same base directory policy (`DATA_DIR` -> `XDG_CONFIG_HOME/omniroute` -> `~/.omniroute`) with legacy file migration.
2. `/api/v1/route.ts` returns a static model list and is not the main models source used by `/v1/models`.
2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.
3. Request logger writes full headers/body when enabled; treat log directory as sensitive.
4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability.
5. The `open-sse/` directory is published as the `@omniroute/open-sse` **npm workspace package**. Source code imports it via `@omniroute/open-sse/...` (resolved by Next.js `transpilePackages`). File paths in this document still use the directory name `open-sse/` for consistency.
6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive analytics visualizations (model usage bar charts, provider breakdown tables with success rates).
7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`).
8. Settings page is organized into 5 tabs: Security, Routing (6 global strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized), Resilience (editable rate limits, circuit breaker, policies), AI (thinking budget, system prompt, prompt cache), Advanced (proxy).
6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive visualizations (bar charts, donut charts).
7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`.
## Operational Verification Checklist
- Build from source: `npm run build`
- Build Docker image: `docker build -t omniroute .`
- Build from source: `cd /root/dev/omniroute && npm run build`
- Build Docker image: `cd /root/dev/omniroute && docker build -t omniroute .`
- Start service and verify:
- `GET /api/settings`
- `GET /api/v1/models`
+38 -51
View File
@@ -110,18 +110,18 @@ The **single source of truth** for all provider configuration.
| File | Purpose |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `constants.ts` | `PROVIDERS` object with base URLs, OAuth credentials (defaults), headers, and default system prompts for every provider. Also defines `HTTP_STATUS`, `ERROR_TYPES`, `COOLDOWN_MS`, `BACKOFF_CONFIG`, and `SKIP_PATTERNS`. |
| `credentialLoader.ts` | Loads external credentials from `data/provider-credentials.json` and merges them over the hardcoded defaults in `PROVIDERS`. Keeps secrets out of source control while maintaining backwards compatibility. |
| `providerModels.ts` | Central model registry: maps provider aliases → model IDs. Functions like `getModels()`, `getProviderByAlias()`. |
| `codexInstructions.ts` | System instructions injected into Codex requests (editing constraints, sandbox rules, approval policies). |
| `defaultThinkingSignature.ts` | Default "thinking" signatures for Claude and Gemini models. |
| `ollamaModels.ts` | Schema definition for local Ollama models (name, size, family, quantization). |
| `constants.js` | `PROVIDERS` object with base URLs, OAuth credentials (defaults), headers, and default system prompts for every provider. Also defines `HTTP_STATUS`, `ERROR_TYPES`, `COOLDOWN_MS`, `BACKOFF_CONFIG`, and `SKIP_PATTERNS`. |
| `credentialLoader.js` | Loads external credentials from `data/provider-credentials.json` and merges them over the hardcoded defaults in `PROVIDERS`. Keeps secrets out of source control while maintaining backwards compatibility. |
| `providerModels.js` | Central model registry: maps provider aliases → model IDs. Functions like `getModels()`, `getProviderByAlias()`. |
| `codexInstructions.js` | System instructions injected into Codex requests (editing constraints, sandbox rules, approval policies). |
| `defaultThinkingSignature.js` | Default "thinking" signatures for Claude and Gemini models. |
| `ollamaModels.js` | Schema definition for local Ollama models (name, size, family, quantization). |
#### Credential Loading Flow
```mermaid
flowchart TD
A["App starts"] --> B["constants.ts defines PROVIDERS\nwith hardcoded defaults"]
A["App starts"] --> B["constants.js defines PROVIDERS\nwith hardcoded defaults"]
B --> C{"data/provider-credentials.json\nexists?"}
C -->|Yes| D["credentialLoader reads JSON"]
C -->|No| E["Use hardcoded defaults"]
@@ -194,15 +194,15 @@ classDiagram
| Executor | Provider | Key Specializations |
| ---------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `base.ts` | — | Abstract base: URL building, headers, retry logic, credential refresh |
| `default.ts` | Claude, Gemini, OpenAI, GLM, Kimi, MiniMax | Generic OAuth token refresh for standard providers |
| `antigravity.ts` | Google Cloud Code | Project/session ID generation, multi-URL fallback, custom retry parsing from error messages ("reset after 2h7m23s") |
| `cursor.ts` | Cursor IDE | **Most complex**: SHA-256 checksum auth, Protobuf request encoding, binary EventStream → SSE response parsing |
| `codex.ts` | OpenAI Codex | Injects system instructions, manages thinking levels, removes unsupported parameters |
| `gemini-cli.ts` | Google Gemini CLI | Custom URL building (`streamGenerateContent`), Google OAuth token refresh |
| `github.ts` | GitHub Copilot | Dual token system (GitHub OAuth + Copilot token), VSCode header mimicking |
| `kiro.ts` | AWS CodeWhisperer | AWS EventStream binary parsing, AMZN event frames, token estimation |
| `index.ts` | — | Factory: maps provider name → executor class, with default fallback |
| `base.js` | — | Abstract base: URL building, headers, retry logic, credential refresh |
| `default.js` | Claude, Gemini, OpenAI, GLM, Kimi, MiniMax | Generic OAuth token refresh for standard providers |
| `antigravity.js` | Google Cloud Code | Project/session ID generation, multi-URL fallback, custom retry parsing from error messages ("reset after 2h7m23s") |
| `cursor.js` | Cursor IDE | **Most complex**: SHA-256 checksum auth, Protobuf request encoding, binary EventStream → SSE response parsing |
| `codex.js` | OpenAI Codex | Injects system instructions, manages thinking levels, removes unsupported parameters |
| `gemini-cli.js` | Google Gemini CLI | Custom URL building (`streamGenerateContent`), Google OAuth token refresh |
| `github.js` | GitHub Copilot | Dual token system (GitHub OAuth + Copilot token), VSCode header mimicking |
| `kiro.js` | AWS CodeWhisperer | AWS EventStream binary parsing, AMZN event frames, token estimation |
| `index.js` | — | Factory: maps provider name → executor class, with default fallback |
---
@@ -212,12 +212,12 @@ The **orchestration layer** — coordinates translation, execution, streaming, a
| File | Purpose |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chatCore.ts` | **Central orchestrator** (~600 lines). Handles the complete request lifecycle: format detection → translation → executor dispatch → streaming/non-streaming response → token refresh → error handling → usage logging. |
| `responsesHandler.ts` | Adapter for OpenAI's Responses API: converts Responses format → Chat Completions → sends to `chatCore` → converts SSE back to Responses format. |
| `embeddings.ts` | Embedding generation handler: resolves embedding model → provider, dispatches to provider API, returns OpenAI-compatible embedding response. Supports 6+ providers. |
| `imageGeneration.ts` | Image generation handler: resolves image model → provider, supports OpenAI-compatible, Gemini-image (Antigravity), and fallback (Nebius) modes. Returns base64 or URL images. |
| `chatCore.js` | **Central orchestrator** (~600 lines). Handles the complete request lifecycle: format detection → translation → executor dispatch → streaming/non-streaming response → token refresh → error handling → usage logging. |
| `responsesHandler.js` | Adapter for OpenAI's Responses API: converts Responses format → Chat Completions → sends to `chatCore` → converts SSE back to Responses format. |
| `embeddings.js` | Embedding generation handler: resolves embedding model → provider, dispatches to provider API, returns OpenAI-compatible embedding response. Supports 6+ providers. |
| `imageGeneration.js` | Image generation handler: resolves image model → provider, supports OpenAI-compatible, Gemini-image (Antigravity), and fallback (Nebius) modes. Returns base64 or URL images. |
#### Request Lifecycle (chatCore.ts)
#### Request Lifecycle (chatCore.js)
```mermaid
sequenceDiagram
@@ -262,20 +262,12 @@ Business logic that supports the handlers and executors.
| File | Purpose |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider.ts` | **Format detection** (`detectFormat`): analyzes request body structure to identify Claude/OpenAI/Gemini/Antigravity/Responses formats (includes `max_tokens` heuristic for Claude). Also: URL building, header building, thinking config normalization. Supports `openai-compatible-*` and `anthropic-compatible-*` dynamic providers. |
| `model.ts` | Model string parsing (`claude/model-name``{provider: "claude", model: "model-name"}`), alias resolution with collision detection, input sanitization (rejects path traversal/control chars), and model info resolution with async alias getter support. |
| `accountFallback.ts` | Rate-limit handling: exponential backoff (1s → 2s → 4s → max 2min), account cooldown management, error classification (which errors trigger fallback vs. not). |
| `tokenRefresh.ts` | OAuth token refresh for **every provider**: Google (Gemini, Antigravity), Claude, Codex, Qwen, iFlow, GitHub (OAuth + Copilot dual-token), Kiro (AWS SSO OIDC + Social Auth). Includes in-flight promise deduplication cache and retry with exponential backoff. |
| `combo.ts` | **Combo models**: chains of fallback models. If model A fails with a fallback-eligible error, try model B, then C, etc. Returns actual upstream status codes. |
| `usage.ts` | Fetches quota/usage data from provider APIs (GitHub Copilot quotas, Antigravity model quotas, Codex rate limits, Kiro usage breakdowns, Claude settings). |
| `accountSelector.ts` | Smart account selection with scoring algorithm: considers priority, health status, round-robin position, and cooldown state to pick the optimal account for each request. |
| `contextManager.ts` | Request context lifecycle management: creates and tracks per-request context objects with metadata (request ID, timestamps, provider info) for debugging and logging. |
| `ipFilter.ts` | IP-based access control: supports allowlist and blocklist modes. Validates client IP against configured rules before processing API requests. |
| `sessionManager.ts` | Session tracking with client fingerprinting: tracks active sessions using hashed client identifiers, monitors request counts, and provides session metrics. |
| `signatureCache.ts` | Request signature-based deduplication cache: prevents duplicate requests by caching recent request signatures and returning cached responses for identical requests within a time window. |
| `systemPrompt.ts` | Global system prompt injection: prepends or appends a configurable system prompt to all requests, with per-provider compatibility handling. |
| `thinkingBudget.ts` | Reasoning token budget management: supports passthrough, auto (strip thinking config), custom (fixed budget), and adaptive (complexity-scaled) modes for controlling thinking/reasoning tokens. |
| `wildcardRouter.ts` | Wildcard model pattern routing: resolves wildcard patterns (e.g., `*/claude-*`) to concrete provider/model pairs based on availability and priority. |
| `provider.js` | **Format detection** (`detectFormat`): analyzes request body structure to identify Claude/OpenAI/Gemini/Antigravity/Responses formats (includes `max_tokens` heuristic for Claude). Also: URL building, header building, thinking config normalization. Supports `openai-compatible-*` and `anthropic-compatible-*` dynamic providers. |
| `model.js` | Model string parsing (`claude/model-name``{provider: "claude", model: "model-name"}`), alias resolution with collision detection, input sanitization (rejects path traversal/control chars), and model info resolution with async alias getter support. |
| `accountFallback.js` | Rate-limit handling: exponential backoff (1s → 2s → 4s → max 2min), account cooldown management, error classification (which errors trigger fallback vs. not). |
| `tokenRefresh.js` | OAuth token refresh for **every provider**: Google (Gemini, Antigravity), Claude, Codex, Qwen, iFlow, GitHub (OAuth + Copilot dual-token), Kiro (AWS SSO OIDC + Social Auth). Includes in-flight promise deduplication cache and retry with exponential backoff. |
| `combo.js` | **Combo models**: chains of fallback models. If model A fails with a fallback-eligible error, try model B, then C, etc. Returns actual upstream status codes. |
| `usage.js` | Fetches quota/usage data from provider APIs (GitHub Copilot quotas, Antigravity model quotas, Codex rate limits, Kiro usage breakdowns, Claude settings). |
#### Token Refresh Deduplication
@@ -377,8 +369,8 @@ graph TD
| `request/` | 8 translators | Convert request bodies between formats. Each file self-registers via `register(from, to, fn)` on import. |
| `response/` | 7 translators | Convert streaming response chunks between formats. Handles SSE event types, thinking blocks, tool calls. |
| `helpers/` | 6 helpers | Shared utilities: `claudeHelper` (system prompt extraction, thinking config), `geminiHelper` (parts/contents mapping), `openaiHelper` (format filtering), `toolCallHelper` (ID generation, missing response injection), `maxTokensHelper`, `responsesApiHelper`. |
| `index.ts` | — | Translation engine: `translateRequest()`, `translateResponse()`, state management, registry. |
| `formats.ts` | — | Format constants: `OPENAI`, `CLAUDE`, `GEMINI`, `ANTIGRAVITY`, `KIRO`, `CURSOR`, `OPENAI_RESPONSES`. |
| `index.js` | — | Translation engine: `translateRequest()`, `translateResponse()`, state management, registry. |
| `formats.js` | — | Format constants: `OPENAI`, `CLAUDE`, `GEMINI`, `ANTIGRAVITY`, `KIRO`, `CURSOR`, `OPENAI_RESPONSES`. |
#### Key Design: Self-Registering Plugins
@@ -397,13 +389,13 @@ import "./request/claude-to-openai.js"; // ← self-registers
| File | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `error.ts` | Error response building (OpenAI-compatible format), upstream error parsing, Antigravity retry-time extraction from error messages, SSE error streaming. |
| `stream.ts` | **SSE Transform Stream** — the core streaming pipeline. Two modes: `TRANSLATE` (full format translation) and `PASSTHROUGH` (normalize + extract usage). Handles chunk buffering, usage estimation, content length tracking. Per-stream encoder/decoder instances avoid shared state. |
| `streamHelpers.ts` | Low-level SSE utilities: `parseSSELine` (whitespace-tolerant), `hasValuableContent` (filters empty chunks for OpenAI/Claude/Gemini), `fixInvalidId`, `formatSSE` (format-aware SSE serialization with `perf_metrics` cleanup). |
| `usageTracking.ts` | Token usage extraction from any format (Claude/OpenAI/Gemini/Responses), estimation with separate tool/message char-per-token ratios, buffer addition (2000 tokens safety margin), format-specific field filtering, console logging with ANSI colors. |
| `requestLogger.ts` | File-based request logging (opt-in via `ENABLE_REQUEST_LOGS=true`). Creates session folders with numbered files: `1_req_client.json``7_res_client.txt`. All I/O is async (fire-and-forget). Masks sensitive headers. |
| `bypassHandler.ts` | Intercepts specific patterns from Claude CLI (title extraction, warmup, count) and returns fake responses without calling any provider. Supports both streaming and non-streaming. Intentionally limited to Claude CLI scope. |
| `networkProxy.ts` | Resolves outbound proxy URL for a given provider with precedence: provider-specific config → global config → environment variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`). Supports `NO_PROXY` exclusions. Caches config for 30s. |
| `error.js` | Error response building (OpenAI-compatible format), upstream error parsing, Antigravity retry-time extraction from error messages, SSE error streaming. |
| `stream.js` | **SSE Transform Stream** — the core streaming pipeline. Two modes: `TRANSLATE` (full format translation) and `PASSTHROUGH` (normalize + extract usage). Handles chunk buffering, usage estimation, content length tracking. Per-stream encoder/decoder instances avoid shared state. |
| `streamHelpers.js` | Low-level SSE utilities: `parseSSELine` (whitespace-tolerant), `hasValuableContent` (filters empty chunks for OpenAI/Claude/Gemini), `fixInvalidId`, `formatSSE` (format-aware SSE serialization with `perf_metrics` cleanup). |
| `usageTracking.js` | Token usage extraction from any format (Claude/OpenAI/Gemini/Responses), estimation with separate tool/message char-per-token ratios, buffer addition (2000 tokens safety margin), format-specific field filtering, console logging with ANSI colors. |
| `requestLogger.js` | File-based request logging (opt-in via `ENABLE_REQUEST_LOGS=true`). Creates session folders with numbered files: `1_req_client.json``7_res_client.txt`. All I/O is async (fire-and-forget). Masks sensitive headers. |
| `bypassHandler.js` | Intercepts specific patterns from Claude CLI (title extraction, warmup, count) and returns fake responses without calling any provider. Supports both streaming and non-streaming. Intentionally limited to Claude CLI scope. |
| `networkProxy.js` | Resolves outbound proxy URL for a given provider with precedence: provider-specific config → global config → environment variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`). Supports `NO_PROXY` exclusions. Caches config for 30s. |
#### SSE Streaming Pipeline
@@ -450,7 +442,7 @@ logs/
| Directory | Purpose |
| ------------- | ---------------------------------------------------------------------- |
| `src/app/` | Web UI, API routes, Express middleware, OAuth callback handlers |
| `src/lib/` | Database access (`localDb.ts`, `usageDb.ts`), authentication, shared |
| `src/lib/` | Database access (`localDb.js`, `usageDb.js`), authentication, shared |
| `src/mitm/` | Man-in-the-middle proxy utilities for intercepting provider traffic |
| `src/models/` | Database model definitions |
| `src/shared/` | Wrappers around open-sse functions (provider, stream, error, etc.) |
@@ -468,11 +460,6 @@ logs/
| `/v1/providers/[provider]/chat/completions` | POST | Dedicated per-provider chat completions with model validation |
| `/v1/providers/[provider]/embeddings` | POST | Dedicated per-provider embeddings with model validation |
| `/v1/providers/[provider]/images/generations` | POST | Dedicated per-provider image generation with model validation |
| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist management |
| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget configuration (passthrough/auto/custom/adaptive) |
| `/api/settings/system-prompt` | GET/PUT | Global system prompt injection for all requests |
| `/api/sessions` | GET | Active session tracking and metrics |
| `/api/rate-limits` | GET | Per-account rate limit status |
---
@@ -484,7 +471,7 @@ All formats translate through **OpenAI format as the hub**. Adding a new provide
### 5.2 Executor Strategy Pattern
Each provider has a dedicated executor class inheriting from `BaseExecutor`. The factory in `executors/index.ts` selects the right one at runtime.
Each provider has a dedicated executor class inheriting from `BaseExecutor`. The factory in `executors/index.js` selects the right one at runtime.
### 5.3 Self-Registering Plugin System
-75
View File
@@ -1,75 +0,0 @@
# OmniRoute — Dashboard Features Gallery
Visual guide to every section of the OmniRoute dashboard.
---
## 🔌 Providers
Manage AI provider connections: OAuth providers (Claude Code, Codex, Gemini CLI), API key providers (Groq, DeepSeek, OpenRouter), and free providers (iFlow, Qwen, Kiro).
![Providers Dashboard](screenshots/01-providers.png)
---
## 🎨 Combos
Create model routing combos with 6 strategies: fill-first, round-robin, power-of-two-choices, random, least-used, and cost-optimized. Each combo chains multiple models with automatic fallback.
![Combos Dashboard](screenshots/02-combos.png)
---
## 📊 Analytics
Comprehensive usage analytics with token consumption, cost estimates, activity heatmaps, weekly distribution charts, and per-provider breakdowns.
![Analytics Dashboard](screenshots/03-analytics.png)
---
## 🏥 System Health
Real-time monitoring: uptime, memory, version, latency percentiles (p50/p95/p99), cache statistics, and provider circuit breaker states.
![Health Dashboard](screenshots/04-health.png)
---
## 🔧 Translator Playground
Four modes for debugging API translations: **Playground** (format converter), **Chat Tester** (live requests), **Test Bench** (batch tests), and **Live Monitor** (real-time stream).
![Translator Playground](screenshots/05-translator.png)
---
## ⚙️ Settings
General settings, system storage, backup management (export/import database), appearance (dark/light mode), security (includes API endpoint protection and custom provider blocking), routing, resilience, and advanced configuration.
![Settings Dashboard](screenshots/06-settings.png)
---
## 🔧 CLI Tools
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, and Antigravity.
![CLI Tools Dashboard](screenshots/07-cli-tools.png)
---
## 📝 Request Logs
Real-time request logging with filtering by provider, model, account, and API key. Shows status codes, token usage, latency, and response details.
![Usage Logs](screenshots/08-usage.png)
---
## 🌐 API Endpoint
Your unified API endpoint with capability breakdown: Chat Completions, Embeddings, Image Generation, Reranking, Audio Transcription, and registered API keys.
![Endpoint Dashboard](screenshots/09-endpoint.png)
-113
View File
@@ -1,113 +0,0 @@
# Rate Limiting & Flow Control Overhaul — Tasks
> Referência: [Relatório de Análise](../walkthrough.md) · Fase docs em `/docs/phases/`
---
## Fase 1 — Error Classification & Provider Profiles
### Backend Core
- [x] `constants.ts` — Substituir `COOLDOWN_MS.transient` por `transientInitial` (5s) + `transientMax` (60s)
- [x] `constants.ts` — Adicionar `PROVIDER_PROFILES` (oauth / apikey) com cooldowns diferenciados
- [x] `constants.ts` — Adicionar `DEFAULT_API_LIMITS` (100 RPM, 200ms minTime)
- [x] `providerRegistry.ts` — Criar helper `getProviderCategory(providerId)``"oauth"` | `"apikey"`
- [x] `accountFallback.ts` — Aceitar `provider` como parâmetro em `checkFallbackError`
- [x] `accountFallback.ts` — Implementar backoff exponencial para 502/503/504 transientes
- [x] `accountFallback.ts` — Calcular cooldown baseado no perfil do provedor
- [x] `accountFallback.ts` — Adicionar helper `getProviderProfile(provider)`
### Callers (propagar `provider`)
- [x] `auth.ts``markAccountUnavailable` — Passar `provider` para `checkFallbackError`
- [x] `combo.ts``handleComboChat` / `handleRoundRobinCombo` — Passar `provider` nos erros
### Testes
- [x] Atualizar `rate-limit-enhanced.test.mjs` — Teste "transient errors don't increase backoff" → `newBackoffLevel = 1`
- [x] Criar `error-classification.test.mjs` — Cooldown exponencial 502, perfis OAuth/API, helper `getProviderCategory`
---
## Fase 2 — Circuit Breaker no Combo Pipeline
### Backend
- [x] `combo.ts` — Importar `getCircuitBreaker` e `CircuitBreakerOpenError`
- [x] `combo.ts``handleComboChat` — Verificar `breaker.canExecute()` antes de cada modelo
- [x] `combo.ts``handleRoundRobinCombo` — Integrar breaker per-model
- [x] `combo.ts` — Marcar `semaphore.markRateLimited` para 502/503/504 (não só 429)
- [x] `combo.ts` — Implementar early exit quando todos os modelos têm breaker OPEN
### Testes
- [x] Criar `combo-circuit-breaker.test.mjs` — Combo skip breaker OPEN, early exit, semáforo 502
---
## Fase 3 — Anti-Thundering Herd & Auto Rate Limit
### Backend
- [x] `rateLimitManager.ts` — Auto-enable para `apikey` providers com limites elevados
- [x] `rateLimitManager.ts` — Criar limiter com defaults (100 RPM) quando não configurado
- [x] `auth.ts` — Adicionar mutex na `markAccountUnavailable` para evitar marcação paralela
### Testes
- [x] Criar `thundering-herd.test.mjs` — Mutex, auto-enable, limites não restritivos
---
## Fase 4 — Frontend Resilience UI
### Settings Page
- [x] `settings/page.tsx` — Adicionar tab "Resilience" (icon: `health_and_safety`) entre Routing e Pricing
### Novos Componentes
- [x] Criar `ResilienceTab.tsx` — Layout com 4 cards (Provider Profiles → Rate Limiting → Circuit Breakers → Policies)
- [x] Criar `ProviderProfilesCard.tsx` — Toggle OAuth/API Key, inputs para cooldowns
- [x] Criar `CircuitBreakerCard.tsx` — Status real-time per-provider, auto-refresh 5s, botão reset
- [x] Criar `RateLimitOverviewCard.tsx` — Tabela providers × accounts × cooldown — **agora editável com RPM, Min Gap, Max Concurrent**
### API Routes
- [x] Criar `api/resilience/route.ts` — GET (estado completo + defaults mesclados) + PATCH (salvar perfis + defaults)
- [x] Criar `api/resilience/reset/route.ts` — POST (resetar breakers + cooldowns)
### Migração
- [x] `PoliciesPanel.tsx` movido de Security para Resilience tab
---
## Fase 5 — Settings Page Restructure (v0.9.0)
### Tab Reorganization
- [x] **Security** — Simplificado para Login/Password + IP Access Control
- [x] **Routing** — Expandido para 6 estratégias globais com descrições
- [x] **Resilience** — Reordenado: Provider Profiles → Rate Limiting (editável) → Circuit Breakers → Policies
- [x] **AI** — Thinking Budget + System Prompt + Prompt Cache (movido do Advanced)
- [x] **Advanced** — Simplificado para apenas Global Proxy
### Backend Routing Strategies
- [x] `auth.ts` — Implementar `random` (Fisher-Yates shuffle)
- [x] `auth.ts` — Implementar `least-used` (sorted by lastUsedAt)
- [x] `auth.ts` — Implementar `cost-optimized` (sorted by priority)
- [x] `auth.ts` — Corrigir `p2c` (power-of-two-choices com health scoring)
- [x] `settings.ts` — Expandir tipo `fallbackStrategy` para 6 valores
---
## Verificação Final
- [x] Rodar todos os testes unitários: `node --test tests/unit/*.test.mjs`
- [x] Build do Next.js: `npm run build`
- [x] Verificar aba Resilience no browser
- [x] Testar persistência dos perfis (salvar → reload)
- [x] Testar Reset All Breakers
- [x] Verificar todas as 5 tabs reestruturadas
-215
View File
@@ -1,215 +0,0 @@
# Troubleshooting
Common problems and solutions for OmniRoute.
---
## Quick Fixes
| Problem | Solution |
| ----------------------------- | ------------------------------------------------------------------ |
| First login not working | Check `INITIAL_PASSWORD` in `.env` (default: `123456`) |
| Dashboard opens on wrong port | Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` |
| No request logs under `logs/` | Set `ENABLE_REQUEST_LOGS=true` |
---
## Provider Issues
### "Language model did not provide messages"
**Cause:** Provider quota exhausted.
**Fix:**
1. Check dashboard quota tracker
2. Use a combo with fallback tiers
3. Switch to cheaper/free tier
### Rate Limiting
**Cause:** Subscription quota exhausted.
**Fix:**
- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
- Use GLM/MiniMax as cheap backup
### OAuth Token Expired
OmniRoute auto-refreshes tokens. If issues persist:
1. Dashboard → Provider → Reconnect
2. Delete and re-add the provider connection
---
## Cloud Issues
### Cloud Sync Errors
1. Verify `BASE_URL` points to your running instance (e.g., `http://localhost:20128`)
2. Verify `CLOUD_URL` points to your cloud endpoint (e.g., `https://omniroute.dev`)
3. Keep `NEXT_PUBLIC_*` values aligned with server-side values
### Cloud `stream=false` Returns 500
**Symptom:** `Unexpected token 'd'...` on cloud endpoint for non-streaming calls.
**Cause:** Upstream returns SSE payload while client expects JSON.
**Workaround:** Use `stream=true` for cloud direct calls. Local runtime includes SSE→JSON fallback.
### Cloud Says Connected but "Invalid API key"
1. Create a fresh key from local dashboard (`/api/keys`)
2. Run cloud sync: Enable Cloud → Sync Now
3. Old/non-synced keys can still return `401` on cloud
---
## Docker Issues
### CLI Tool Shows Not Installed
1. Check runtime fields: `curl http://localhost:20128/api/cli-tools/runtime/codex | jq`
2. For portable mode: use image target `runner-cli` (bundled CLIs)
3. For host mount mode: set `CLI_EXTRA_PATHS` and mount host bin directory as read-only
4. If `installed=true` and `runnable=false`: binary was found but failed healthcheck
### Quick Runtime Validation
```bash
curl -s http://localhost:20128/api/cli-tools/codex-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
curl -s http://localhost:20128/api/cli-tools/claude-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
curl -s http://localhost:20128/api/cli-tools/openclaw-settings | jq '{installed,runnable,commandPath,runtimeMode,reason}'
```
---
## Cost Issues
### High Costs
1. Check usage stats in Dashboard → Usage
2. Switch primary model to GLM/MiniMax
3. Use free tier (Gemini CLI, iFlow) for non-critical tasks
4. Set cost budgets per API key: Dashboard → API Keys → Budget
---
## Debugging
### Enable Request Logs
Set `ENABLE_REQUEST_LOGS=true` in your `.env` file. Logs appear under `logs/` directory.
### Check Provider Health
```bash
# Health dashboard
http://localhost:20128/dashboard/health
# API health check
curl http://localhost:20128/api/monitoring/health
```
### Runtime Storage
- Main state: `${DATA_DIR}/db.json` (providers, combos, aliases, keys, settings)
- Usage: `${DATA_DIR}/usage.json`, `${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/`
- Request logs: `<repo>/logs/...` (when `ENABLE_REQUEST_LOGS=true`)
---
## Circuit Breaker Issues
### Provider stuck in OPEN state
When a provider's circuit breaker is OPEN, requests are blocked until the cooldown expires.
**Fix:**
1. Go to **Dashboard → Settings → Resilience**
2. Check the circuit breaker card for the affected provider
3. Click **Reset All** to clear all breakers, or wait for the cooldown to expire
4. Verify the provider is actually available before resetting
### Provider keeps tripping the circuit breaker
If a provider repeatedly enters OPEN state:
1. Check **Dashboard → Health → Provider Health** for the failure pattern
2. Go to **Settings → Resilience → Provider Profiles** and increase the failure threshold
3. Check if the provider has changed API limits or requires re-authentication
4. Review latency telemetry — high latency may cause timeout-based failures
---
## Audio Transcription Issues
### "Unsupported model" error
- Ensure you're using the correct prefix: `deepgram/nova-3` or `assemblyai/best`
- Verify the provider is connected in **Dashboard → Providers**
### Transcription returns empty or fails
- Check supported audio formats: `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`
- Verify file size is within provider limits (typically < 25MB)
- Check provider API key validity in the provider card
---
## Translator Debugging
Use **Dashboard → Translator** to debug format translation issues:
| Mode | When to Use |
| ---------------- | -------------------------------------------------------------------------------------------- |
| **Playground** | Compare input/output formats side by side — paste a failing request to see how it translates |
| **Chat Tester** | Send live messages and inspect the full request/response payload including headers |
| **Test Bench** | Run batch tests across format combinations to find which translations are broken |
| **Live Monitor** | Watch real-time request flow to catch intermittent translation issues |
### Common format issues
- **Thinking tags not appearing** — Check if the target provider supports thinking and the thinking budget setting
- **Tool calls dropping** — Some format translations may strip unsupported fields; verify in Playground mode
- **System prompt missing** — Claude and Gemini handle system prompts differently; check translation output
- **SDK returns raw string instead of object** — Fixed in v1.1.0: response sanitizer now strips non-standard fields (`x_groq`, `usage_breakdown`, etc.) that cause OpenAI SDK Pydantic validation failures
- **GLM/ERNIE rejects `system` role** — Fixed in v1.1.0: role normalizer automatically merges system messages into user messages for incompatible models
- **`developer` role not recognized** — Fixed in v1.1.0: automatically converted to `system` for non-OpenAI providers
- **`json_schema` not working with Gemini** — Fixed in v1.1.0: `response_format` is now converted to Gemini's `responseMimeType` + `responseSchema`
---
## Resilience Settings
### Auto rate-limit not triggering
- Auto rate-limit only applies to API key providers (not OAuth/subscription)
- Verify **Settings → Resilience → Provider Profiles** has auto-rate-limit enabled
- Check if the provider returns `429` status codes or `Retry-After` headers
### Tuning exponential backoff
Provider profiles support these settings:
- **Base delay** — Initial wait time after first failure (default: 1s)
- **Max delay** — Maximum wait time cap (default: 30s)
- **Multiplier** — How much to increase delay per consecutive failure (default: 2x)
### Anti-thundering herd
When many concurrent requests hit a rate-limited provider, OmniRoute uses mutex + auto rate-limiting to serialize requests and prevent cascading failures. This is automatic for API key providers.
---
## Still Stuck?
- **GitHub Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
- **Architecture**: See [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) for internal details
- **API Reference**: See [`docs/API_REFERENCE.md`](API_REFERENCE.md) for all endpoints
- **Health Dashboard**: Check **Dashboard → Health** for real-time system status
- **Translator**: Use **Dashboard → Translator** to debug format issues
-696
View File
@@ -1,696 +0,0 @@
# User Guide
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
---
## Table of Contents
- [Pricing at a Glance](#-pricing-at-a-glance)
- [Use Cases](#-use-cases)
- [Provider Setup](#-provider-setup)
- [CLI Integration](#-cli-integration)
- [Deployment](#-deployment)
- [Available Models](#-available-models)
- [Advanced Features](#-advanced-features)
---
## 💰 Pricing at a Glance
| Tier | Provider | Cost | Quota Reset | Best For |
| ------------------- | ----------------- | ----------- | ---------------- | -------------------- |
| **💳 SUBSCRIPTION** | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed |
| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users |
| | Gemini CLI | **FREE** | 180K/mo + 1K/day | Everyone! |
| | GitHub Copilot | $10-19/mo | Monthly | GitHub users |
| **🔑 API KEY** | DeepSeek | Pay per use | None | Cheap reasoning |
| | Groq | Pay per use | None | Ultra-fast inference |
| | xAI (Grok) | Pay per use | None | Grok 4 reasoning |
| | Mistral | Pay per use | None | EU-hosted models |
| | Perplexity | Pay per use | None | Search-augmented |
| | Together AI | Pay per use | None | Open-source models |
| | Fireworks AI | Pay per use | None | Fast FLUX images |
| | Cerebras | Pay per use | None | Wafer-scale speed |
| | Cohere | Pay per use | None | Command R+ RAG |
| | NVIDIA NIM | Pay per use | None | Enterprise models |
| **💰 CHEAP** | GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup |
| | MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option |
| | Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost |
| **🆓 FREE** | iFlow | $0 | Unlimited | 8 models free |
| | Qwen | $0 | Unlimited | 3 models free |
| | Kiro | $0 | Unlimited | Claude free |
**💡 Pro Tip:** Start with Gemini CLI (180K free/month) + iFlow (unlimited free) combo = $0 cost!
---
## 🎯 Use Cases
### Case 1: "I have Claude Pro subscription"
**Problem:** Quota expires unused, rate limits during heavy coding
```
Combo: "maximize-claude"
1. cc/claude-opus-4-6 (use subscription fully)
2. glm/glm-4.7 (cheap backup when quota out)
3. if/kimi-k2-thinking (free emergency fallback)
Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total
vs. $20 + hitting limits = frustration
```
### Case 2: "I want zero cost"
**Problem:** Can't afford subscriptions, need reliable AI coding
```
Combo: "free-forever"
1. gc/gemini-3-flash (180K free/month)
2. if/kimi-k2-thinking (unlimited free)
3. qw/qwen3-coder-plus (unlimited free)
Monthly cost: $0
Quality: Production-ready models
```
### Case 3: "I need 24/7 coding, no interruptions"
**Problem:** Deadlines, can't afford downtime
```
Combo: "always-on"
1. cc/claude-opus-4-6 (best quality)
2. cx/gpt-5.2-codex (second subscription)
3. glm/glm-4.7 (cheap, resets daily)
4. minimax/MiniMax-M2.1 (cheapest, 5h reset)
5. if/kimi-k2-thinking (free unlimited)
Result: 5 layers of fallback = zero downtime
Monthly cost: $20-200 (subscriptions) + $10-20 (backup)
```
### Case 4: "I want FREE AI in OpenClaw"
**Problem:** Need AI assistant in messaging apps, completely free
```
Combo: "openclaw-free"
1. if/glm-4.7 (unlimited free)
2. if/minimax-m2.1 (unlimited free)
3. if/kimi-k2-thinking (unlimited free)
Monthly cost: $0
Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
```
---
## 📖 Provider Setup
### 🔐 Subscription Providers
#### Claude Code (Pro/Max)
```bash
Dashboard → Providers → Connect Claude Code
→ OAuth login → Auto token refresh
→ 5-hour + weekly quota tracking
Models:
cc/claude-opus-4-6
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
```
**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. OmniRoute tracks quota per model!
#### OpenAI Codex (Plus/Pro)
```bash
Dashboard → Providers → Connect Codex
→ OAuth login (port 1455)
→ 5-hour + weekly reset
Models:
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
```
#### Gemini CLI (FREE 180K/month!)
```bash
Dashboard → Providers → Connect Gemini CLI
→ Google OAuth
→ 180K completions/month + 1K/day
Models:
gc/gemini-3-flash-preview
gc/gemini-2.5-pro
```
**Best Value:** Huge free tier! Use this before paid tiers.
#### GitHub Copilot
```bash
Dashboard → Providers → Connect GitHub
→ OAuth via GitHub
→ Monthly reset (1st of month)
Models:
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3-pro
```
### 💰 Cheap Providers
#### GLM-4.7 (Daily reset, $0.6/1M)
1. Sign up: [Zhipu AI](https://open.bigmodel.cn/)
2. Get API key from Coding Plan
3. Dashboard → Add API Key: Provider: `glm`, API Key: `your-key`
**Use:** `glm/glm-4.7`**Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM.
#### MiniMax M2.1 (5h reset, $0.20/1M)
1. Sign up: [MiniMax](https://www.minimax.io/)
2. Get API key → Dashboard → Add API Key
**Use:** `minimax/MiniMax-M2.1`**Pro Tip:** Cheapest option for long context (1M tokens)!
#### Kimi K2 ($9/month flat)
1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/)
2. Get API key → Dashboard → Add API Key
**Use:** `kimi/kimi-latest`**Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost!
### 🆓 FREE Providers
#### iFlow (8 FREE models)
```bash
Dashboard → Connect iFlow → OAuth login → Unlimited usage
Models: if/kimi-k2-thinking, if/qwen3-coder-plus, if/glm-4.7, if/minimax-m2, if/deepseek-r1
```
#### Qwen (3 FREE models)
```bash
Dashboard → Connect Qwen → Device code auth → Unlimited usage
Models: qw/qwen3-coder-plus, qw/qwen3-coder-flash
```
#### Kiro (Claude FREE)
```bash
Dashboard → Connect Kiro → AWS Builder ID or Google/GitHub → Unlimited
Models: kr/claude-sonnet-4.5, kr/claude-haiku-4.5
```
---
## 🎨 Combos
### Example 1: Maximize Subscription → Cheap Backup
```
Dashboard → Combos → Create New
Name: premium-coding
Models:
1. cc/claude-opus-4-6 (Subscription primary)
2. glm/glm-4.7 (Cheap backup, $0.6/1M)
3. minimax/MiniMax-M2.1 (Cheapest fallback, $0.20/1M)
Use in CLI: premium-coding
```
### Example 2: Free-Only (Zero Cost)
```
Name: free-combo
Models:
1. gc/gemini-3-flash-preview (180K free/month)
2. if/kimi-k2-thinking (unlimited)
3. qw/qwen3-coder-plus (unlimited)
Cost: $0 forever!
```
---
## 🔧 CLI Integration
### Cursor IDE
```
Settings → Models → Advanced:
OpenAI API Base URL: http://localhost:20128/v1
OpenAI API Key: [from omniroute dashboard]
Model: cc/claude-opus-4-6
```
### Claude Code
Edit `~/.claude/config.json`:
```json
{
"anthropic_api_base": "http://localhost:20128/v1",
"anthropic_api_key": "your-omniroute-api-key"
}
```
### Codex CLI
```bash
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
```
### OpenClaw
Edit `~/.openclaw/openclaw.json`:
```json
{
"agents": {
"defaults": {
"model": { "primary": "omniroute/if/glm-4.7" }
}
},
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://localhost:20128/v1",
"apiKey": "your-omniroute-api-key",
"api": "openai-completions",
"models": [{ "id": "if/glm-4.7", "name": "glm-4.7" }]
}
}
}
}
```
**Or use Dashboard:** CLI Tools → OpenClaw → Auto-config
### Cline / Continue / RooCode
```
Provider: OpenAI Compatible
Base URL: http://localhost:20128/v1
API Key: [from dashboard]
Model: cc/claude-opus-4-6
```
---
## 🚀 Deployment
### VPS Deployment
```bash
git clone https://github.com/diegosouzapw/OmniRoute.git
cd OmniRoute && npm install && npm run build
export JWT_SECRET="your-secure-secret-change-this"
export INITIAL_PASSWORD="your-password"
export DATA_DIR="/var/lib/omniroute"
export PORT="20128"
export HOSTNAME="0.0.0.0"
export NODE_ENV="production"
export NEXT_PUBLIC_BASE_URL="http://localhost:20128"
export API_KEY_SECRET="endpoint-proxy-api-key-secret"
npm run start
# Or: pm2 start npm --name omniroute -- start
```
### Docker
```bash
# Build image (default = runner-cli with codex/claude/droid preinstalled)
docker build -t omniroute:cli .
# Portable mode (recommended)
docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-data:/app/data omniroute:cli
```
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
### Environment Variables
| Variable | Default | Description |
| --------------------- | ------------------------------------ | ------------------------------------------------------- |
| `JWT_SECRET` | `omniroute-default-secret-change-me` | JWT signing secret (**change in production**) |
| `INITIAL_PASSWORD` | `123456` | First login password |
| `DATA_DIR` | `~/.omniroute` | Data directory (db, usage, logs) |
| `PORT` | framework default | Service port (`20128` in examples) |
| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) |
| `NODE_ENV` | runtime default | Set `production` for deploy |
| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL |
| `CLOUD_URL` | `https://omniroute.dev` | Cloud sync endpoint base URL |
| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys |
| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` |
| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs |
| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) |
For the full environment variable reference, see the [README](../README.md).
---
## 📊 Available Models
<details>
<summary><b>View all available models</b></summary>
**Claude Code (`cc/`)** — Pro/Max: `cc/claude-opus-4-6`, `cc/claude-sonnet-4-5-20250929`, `cc/claude-haiku-4-5-20251001`
**Codex (`cx/`)** — Plus/Pro: `cx/gpt-5.2-codex`, `cx/gpt-5.1-codex-max`
**Gemini CLI (`gc/`)** — FREE: `gc/gemini-3-flash-preview`, `gc/gemini-2.5-pro`
**GitHub Copilot (`gh/`)**: `gh/gpt-5`, `gh/claude-4.5-sonnet`
**GLM (`glm/`)** — $0.6/1M: `glm/glm-4.7`
**MiniMax (`minimax/`)** — $0.2/1M: `minimax/MiniMax-M2.1`
**iFlow (`if/`)** — FREE: `if/kimi-k2-thinking`, `if/qwen3-coder-plus`, `if/deepseek-r1`
**Qwen (`qw/`)** — FREE: `qw/qwen3-coder-plus`, `qw/qwen3-coder-flash`
**Kiro (`kr/`)** — FREE: `kr/claude-sonnet-4.5`, `kr/claude-haiku-4.5`
**DeepSeek (`ds/`)**: `ds/deepseek-chat`, `ds/deepseek-reasoner`
**Groq (`groq/`)**: `groq/llama-3.3-70b-versatile`, `groq/llama-4-maverick-17b-128e-instruct`
**xAI (`xai/`)**: `xai/grok-4`, `xai/grok-4-0709-fast-reasoning`, `xai/grok-code-mini`
**Mistral (`mistral/`)**: `mistral/mistral-large-2501`, `mistral/codestral-2501`
**Perplexity (`pplx/`)**: `pplx/sonar-pro`, `pplx/sonar`
**Together AI (`together/`)**: `together/meta-llama/Llama-3.3-70B-Instruct-Turbo`
**Fireworks AI (`fireworks/`)**: `fireworks/accounts/fireworks/models/deepseek-v3p1`
**Cerebras (`cerebras/`)**: `cerebras/llama-3.3-70b`
**Cohere (`cohere/`)**: `cohere/command-r-plus-08-2024`
**NVIDIA NIM (`nvidia/`)**: `nvidia/nvidia/llama-3.3-70b-instruct`
</details>
---
## 🧩 Advanced Features
### Custom Models
Add any model ID to any provider without waiting for an app update:
```bash
# Via API
curl -X POST http://localhost:20128/api/provider-models \
-H "Content-Type: application/json" \
-d '{"provider": "openai", "modelId": "gpt-4.5-preview", "modelName": "GPT-4.5 Preview"}'
# List: curl http://localhost:20128/api/provider-models?provider=openai
# Remove: curl -X DELETE "http://localhost:20128/api/provider-models?provider=openai&model=gpt-4.5-preview"
```
Or use Dashboard: **Providers → [Provider] → Custom Models**.
### Dedicated Provider Routes
Route requests directly to a specific provider with model validation:
```bash
POST http://localhost:20128/v1/providers/openai/chat/completions
POST http://localhost:20128/v1/providers/openai/embeddings
POST http://localhost:20128/v1/providers/fireworks/images/generations
```
The provider prefix is auto-added if missing. Mismatched models return `400`.
### Network Proxy Configuration
```bash
# Set global proxy
curl -X PUT http://localhost:20128/api/settings/proxy \
-d '{"global": {"type":"http","host":"proxy.example.com","port":"8080"}}'
# Per-provider proxy
curl -X PUT http://localhost:20128/api/settings/proxy \
-d '{"providers": {"openai": {"type":"socks5","host":"proxy.example.com","port":"1080"}}}'
# Test proxy
curl -X POST http://localhost:20128/api/settings/proxy/test \
-d '{"proxy":{"type":"socks5","host":"proxy.example.com","port":"1080"}}'
```
**Precedence:** Key-specific → Combo-specific → Provider-specific → Global → Environment.
### Model Catalog API
```bash
curl http://localhost:20128/api/models/catalog
```
Returns models grouped by provider with types (`chat`, `embedding`, `image`).
### Cloud Sync
- Sync providers, combos, and settings across devices
- Automatic background sync with timeout + fail-fast
- Prefer server-side `BASE_URL`/`CLOUD_URL` in production
### LLM Gateway Intelligence (Phase 9)
- **Semantic Cache** — Auto-caches non-streaming, temperature=0 responses (bypass with `X-OmniRoute-No-Cache: true`)
- **Request Idempotency** — Deduplicates requests within 5s via `Idempotency-Key` or `X-Request-Id` header
- **Progress Tracking** — Opt-in SSE `event: progress` events via `X-OmniRoute-Progress: true` header
---
### Translator Playground
Access via **Dashboard → Translator**. Debug and visualize how OmniRoute translates API requests between providers.
| Mode | Purpose |
| ---------------- | -------------------------------------------------------------------------------------- |
| **Playground** | Select source/target formats, paste a request, and see the translated output instantly |
| **Chat Tester** | Send live chat messages through the proxy and inspect the full request/response cycle |
| **Test Bench** | Run batch tests across multiple format combinations to verify translation correctness |
| **Live Monitor** | Watch real-time translations as requests flow through the proxy |
**Use cases:**
- Debug why a specific client/provider combination fails
- Verify that thinking tags, tool calls, and system prompts translate correctly
- Compare format differences between OpenAI, Claude, Gemini, and Responses API formats
---
### Routing Strategies
Configure via **Dashboard → Settings → Routing**.
| Strategy | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------ |
| **Fill First** | Uses accounts in priority order — primary account handles all requests until unavailable |
| **Round Robin** | Cycles through all accounts with a configurable sticky limit (default: 3 calls per account) |
| **P2C (Power of Two Choices)** | Picks 2 random accounts and routes to the healthier one — balances load with awareness of health |
| **Random** | Randomly selects an account for each request using Fisher-Yates shuffle |
| **Least Used** | Routes to the account with the oldest `lastUsedAt` timestamp, distributing traffic evenly |
| **Cost Optimized** | Routes to the account with the lowest priority value, optimizing for lowest-cost providers |
#### Wildcard Model Aliases
Create wildcard patterns to remap model names:
```
Pattern: claude-sonnet-* → Target: cc/claude-sonnet-4-5-20250929
Pattern: gpt-* → Target: gh/gpt-5.1-codex
```
Wildcards support `*` (any characters) and `?` (single character).
#### Fallback Chains
Define global fallback chains that apply across all requests:
```
Chain: production-fallback
1. cc/claude-opus-4-6
2. gh/gpt-5.1-codex
3. glm/glm-4.7
```
---
### Resilience & Circuit Breakers
Configure via **Dashboard → Settings → Resilience**.
OmniRoute implements provider-level resilience with four components:
1. **Provider Profiles** — Per-provider configuration for:
- Failure threshold (how many failures before opening)
- Cooldown duration
- Rate limit detection sensitivity
- Exponential backoff parameters
2. **Editable Rate Limits** — System-level defaults configurable in the dashboard:
- **Requests Per Minute (RPM)** — Maximum requests per minute per account
- **Min Time Between Requests** — Minimum gap in milliseconds between requests
- **Max Concurrent Requests** — Maximum simultaneous requests per account
- Click **Edit** to modify, then **Save** or **Cancel**. Values persist via the resilience API.
3. **Circuit Breaker** — Tracks failures per provider and automatically opens the circuit when a threshold is reached:
- **CLOSED** (Healthy) — Requests flow normally
- **OPEN** — Provider is temporarily blocked after repeated failures
- **HALF_OPEN** — Testing if provider has recovered
4. **Policies & Locked Identifiers** — Shows circuit breaker status and locked identifiers with force-unlock capability.
5. **Rate Limit Auto-Detection** — Monitors `429` and `Retry-After` headers to proactively avoid hitting provider rate limits.
**Pro Tip:** Use **Reset All** button to clear all circuit breakers and cooldowns when a provider recovers from an outage.
---
### Database Export / Import
Manage database backups in **Dashboard → Settings → System & Storage**.
| Action | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| **Export Database** | Downloads the current SQLite database as a `.sqlite` file |
| **Export All (.tar.gz)** | Downloads a full backup archive including: database, settings, combos, provider connections (no credentials), API key metadata |
| **Import Database** | Upload a `.sqlite` file to replace the current database. A pre-import backup is automatically created |
```bash
# API: Export database
curl -o backup.sqlite http://localhost:20128/api/db-backups/export
# API: Export all (full archive)
curl -o backup.tar.gz http://localhost:20128/api/db-backups/exportAll
# API: Import database
curl -X POST http://localhost:20128/api/db-backups/import \
-F "file=@backup.sqlite"
```
**Import Validation:** The imported file is validated for integrity (SQLite pragma check), required tables (`provider_connections`, `provider_nodes`, `combos`, `api_keys`), and size (max 100MB).
**Use Cases:**
- Migrate OmniRoute between machines
- Create external backups for disaster recovery
- Share configurations between team members (export all → share archive)
---
### Settings Dashboard
The settings page is organized into 5 tabs for easy navigation:
| Tab | Contents |
| -------------- | ---------------------------------------------------------------------------------------------- |
| **Security** | Login/Password settings, IP Access Control, API auth for `/models`, and Provider Blocking |
| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults |
| **Resilience** | Provider profiles, editable rate limits, circuit breaker status, policies & locked identifiers |
| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats |
| **Advanced** | Global proxy configuration (HTTP/SOCKS5) |
---
### Costs & Budget Management
Access via **Dashboard → Costs**.
| Tab | Purpose |
| ----------- | ---------------------------------------------------------------------------------------- |
| **Budget** | Set spending limits per API key with daily/weekly/monthly budgets and real-time tracking |
| **Pricing** | View and edit model pricing entries — cost per 1K input/output tokens per provider |
```bash
# API: Set a budget
curl -X POST http://localhost:20128/api/usage/budget \
-H "Content-Type: application/json" \
-d '{"keyId": "key-123", "limit": 50.00, "period": "monthly"}'
# API: Get current budget status
curl http://localhost:20128/api/usage/budget
```
**Cost Tracking:** Every request logs token usage and calculates cost using the pricing table. View breakdowns in **Dashboard → Usage** by provider, model, and API key.
---
### Audio Transcription
OmniRoute supports audio transcription via the OpenAI-compatible endpoint:
```bash
POST /v1/audio/transcriptions
Authorization: Bearer your-api-key
Content-Type: multipart/form-data
# Example with curl
curl -X POST http://localhost:20128/v1/audio/transcriptions \
-H "Authorization: Bearer your-api-key" \
-F "file=@audio.mp3" \
-F "model=deepgram/nova-3"
```
Available providers: **Deepgram** (`deepgram/`), **AssemblyAI** (`assemblyai/`).
Supported audio formats: `mp3`, `wav`, `m4a`, `flac`, `ogg`, `webm`.
---
### Combo Balancing Strategies
Configure per-combo balancing in **Dashboard → Combos → Create/Edit → Strategy**.
| Strategy | Description |
| ------------------ | ------------------------------------------------------------------------ |
| **Round-Robin** | Rotates through models sequentially |
| **Priority** | Always tries the first model; falls back only on error |
| **Random** | Picks a random model from the combo for each request |
| **Weighted** | Routes proportionally based on assigned weights per model |
| **Least-Used** | Routes to the model with the fewest recent requests (uses combo metrics) |
| **Cost-Optimized** | Routes to the cheapest available model (uses pricing table) |
Global combo defaults can be set in **Dashboard → Settings → Routing → Combo Defaults**.
---
### Health Dashboard
Access via **Dashboard → Health**. Real-time system health overview with 6 cards:
| Card | What It Shows |
| --------------------- | ----------------------------------------------------------- |
| **System Status** | Uptime, version, memory usage, data directory |
| **Provider Health** | Per-provider circuit breaker state (Closed/Open/Half-Open) |
| **Rate Limits** | Active rate limit cooldowns per account with remaining time |
| **Active Lockouts** | Providers temporarily blocked by the lockout policy |
| **Signature Cache** | Deduplication cache stats (active keys, hit rate) |
| **Latency Telemetry** | p50/p95/p99 latency aggregation per provider |
**Pro Tip:** The Health page auto-refreshes every 10 seconds. Use the circuit breaker card to identify which providers are experiencing issues.
-399
View File
@@ -1,399 +0,0 @@
# OmniRoute — Guia de Deploy em VM com Cloudflare
Guia completo para instalar e configurar o OmniRoute em uma VM (VPS) com domínio gerenciado via Cloudflare.
---
## Pré-Requisitos
| Item | Mínimo | Recomendado |
| ----------- | ------------------------ | ---------------- |
| **CPU** | 1 vCPU | 2 vCPU |
| **RAM** | 1 GB | 2 GB |
| **Disco** | 10 GB SSD | 25 GB SSD |
| **SO** | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| **Domínio** | Registrado no Cloudflare | — |
| **Docker** | Docker Engine 24+ | Docker 27+ |
**Providers testados**: Akamai (Linode), DigitalOcean, Vultr, Hetzner, AWS Lightsail.
---
## 1. Configurar a VM
### 1.1 Criar a instância
No seu provider de VPS preferido:
- Escolha Ubuntu 24.04 LTS
- Selecione o plano mínimo (1 vCPU / 1 GB RAM)
- Defina uma senha forte para root ou configure SSH key
- Anote o **IP público** (ex: `203.0.113.10`)
### 1.2 Conectar via SSH
```bash
ssh root@203.0.113.10
```
### 1.3 Atualizar o sistema
```bash
apt update && apt upgrade -y
```
### 1.4 Instalar Docker
```bash
# Instalar dependências
apt install -y ca-certificates curl gnupg
# Adicionar repositório oficial do Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
```
### 1.5 Instalar nginx
```bash
apt install -y nginx
```
### 1.6 Configurar Firewall (UFW)
```bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP (redirect)
ufw allow 443/tcp # HTTPS
ufw enable
```
> **Dica**: Para segurança máxima, restrinja as portas 80 e 443 apenas para IPs da Cloudflare. Veja a seção [Segurança Avançada](#segurança-avançada).
---
## 2. Instalar o OmniRoute
### 2.1 Criar diretório de configuração
```bash
mkdir -p /opt/omniroute
```
### 2.2 Criar arquivo de variáveis de ambiente
```bash
cat > /opt/omniroute/.env << 'EOF'
# === Segurança ===
JWT_SECRET=ALTERE-PARA-CHAVE-SECRETA-UNICA-64-CHARS
INITIAL_PASSWORD=SuaSenhaSegura123!
API_KEY_SECRET=ALTERE-PARA-OUTRA-CHAVE-SECRETA
STORAGE_ENCRYPTION_KEY=ALTERE-PARA-TERCEIRA-CHAVE-SECRETA
STORAGE_ENCRYPTION_KEY_VERSION=v1
MACHINE_ID_SALT=ALTERE-PARA-SALT-UNICO
# === App ===
PORT=20128
NODE_ENV=production
HOSTNAME=0.0.0.0
DATA_DIR=/app/data
STORAGE_DRIVER=sqlite
ENABLE_REQUEST_LOGS=true
AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false
# === Domain (altere para seu domínio) ===
BASE_URL=https://llms.seudominio.com
NEXT_PUBLIC_BASE_URL=https://llms.seudominio.com
# === Cloud Sync (opcional) ===
# CLOUD_URL=https://cloud.omniroute.online
# NEXT_PUBLIC_CLOUD_URL=https://cloud.omniroute.online
EOF
```
> ⚠️ **IMPORTANTE**: Gere chaves secretas únicas! Use `openssl rand -hex 32` para cada chave.
### 2.3 Iniciar o container
```bash
docker pull diegosouzapw/omniroute:latest
docker run -d \
--name omniroute \
--restart unless-stopped \
--env-file /opt/omniroute/.env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
### 2.4 Verificar se está rodando
```bash
docker ps | grep omniroute
docker logs omniroute --tail 20
```
Deve exibir: `[DB] SQLite database ready` e `listening on port 20128`.
---
## 3. Configurar nginx (Reverse Proxy)
### 3.1 Gerar certificado SSL (Cloudflare Origin)
No painel da Cloudflare:
1. Vá em **SSL/TLS → Origin Server**
2. Clique **Create Certificate**
3. Deixe os padrões (15 anos, \*.seudominio.com)
4. Copie o **Origin Certificate** e a **Private Key**
```bash
mkdir -p /etc/nginx/ssl
# Colar o certificado
nano /etc/nginx/ssl/origin.crt
# Colar a chave privada
nano /etc/nginx/ssl/origin.key
chmod 600 /etc/nginx/ssl/origin.key
```
### 3.2 Configuração do nginx
```bash
cat > /etc/nginx/sites-available/omniroute << 'NGINX'
# Default server — bloqueia acesso direto por IP
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/nginx/ssl/origin.crt;
ssl_certificate_key /etc/nginx/ssl/origin.key;
server_name _;
return 444;
}
# OmniRoute — HTTPS
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name llms.seudominio.com; # Altere para seu domínio
ssl_certificate /etc/nginx/ssl/origin.crt;
ssl_certificate_key /etc/nginx/ssl/origin.key;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:20128;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# SSE (Server-Sent Events) — streaming AI responses
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
# HTTP → HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name llms.seudominio.com;
return 301 https://$server_name$request_uri;
}
NGINX
```
### 3.3 Ativar e testar
```bash
# Remover config padrão
rm -f /etc/nginx/sites-enabled/default
# Ativar OmniRoute
ln -sf /etc/nginx/sites-available/omniroute /etc/nginx/sites-enabled/omniroute
# Testar e recarregar
nginx -t && systemctl reload nginx
```
---
## 4. Configurar Cloudflare DNS
### 4.1 Adicionar registro DNS
No painel da Cloudflare → DNS:
| Type | Name | Content | Proxy |
| ---- | ------ | ------------------------- | ---------- |
| A | `llms` | `203.0.113.10` (IP da VM) | ✅ Proxied |
### 4.2 Configurar SSL
Em **SSL/TLS → Overview**:
- Modo: **Full (Strict)**
Em **SSL/TLS → Edge Certificates**:
- Always Use HTTPS: ✅ On
- Minimum TLS Version: TLS 1.2
- Automatic HTTPS Rewrites: ✅ On
### 4.3 Testar
```bash
curl -sI https://llms.seudominio.com/health
# Deve retornar HTTP/2 200
```
---
## 5. Operações e Manutenção
### Atualizar para nova versão
```bash
docker pull diegosouzapw/omniroute:latest
docker stop omniroute && docker rm omniroute
docker run -d --name omniroute --restart unless-stopped \
--env-file /opt/omniroute/.env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
```
### Ver logs
```bash
docker logs -f omniroute # Stream em tempo real
docker logs omniroute --tail 50 # Últimas 50 linhas
```
### Backup manual do banco
```bash
# Copiar dados do volume para o host
docker cp omniroute:/app/data ./backup-$(date +%F)
# Ou comprimir todo o volume
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
alpine tar czf /backup/omniroute-data-$(date +%F).tar.gz /data
```
### Restaurar de backup
```bash
docker stop omniroute
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
alpine sh -c "rm -rf /data/* && tar xzf /backup/omniroute-data-YYYY-MM-DD.tar.gz -C /"
docker start omniroute
```
---
## 6. Segurança Avançada
### Restringir nginx para Cloudflare IPs
```bash
cat > /etc/nginx/cloudflare-ips.conf << 'CF'
# Cloudflare IPv4 ranges — atualizar periodicamente
# https://www.cloudflare.com/ips-v4/
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
CF
```
Adicionar no `nginx.conf` dentro do bloco `http {}`:
```nginx
include /etc/nginx/cloudflare-ips.conf;
```
### Install fail2ban
```bash
apt install -y fail2ban
systemctl enable fail2ban
systemctl start fail2ban
# Verificar status
fail2ban-client status sshd
```
### Bloquear acesso direto na porta do Docker
```bash
# Impedir acesso externo direto à porta 20128
iptables -I DOCKER-USER -p tcp --dport 20128 -j DROP
iptables -I DOCKER-USER -i lo -p tcp --dport 20128 -j ACCEPT
# Persistir as regras
apt install -y iptables-persistent
netfilter-persistent save
```
---
## 7. Deploy do Cloud Worker (Opcional)
Para acesso remoto via Cloudflare Workers (sem expor a VM diretamente):
```bash
# No repositório local
cd omnirouteCloud
npm install
npx wrangler login
npx wrangler deploy
```
Ver documentação completa em [omnirouteCloud/README.md](../omnirouteCloud/README.md).
---
## Resumo de Portas
| Porta | Serviço | Acesso |
| ----- | ----------- | ----------------------------- |
| 22 | SSH | Público (com fail2ban) |
| 80 | nginx HTTP | Redirect → HTTPS |
| 443 | nginx HTTPS | Via Cloudflare Proxy |
| 20128 | OmniRoute | Somente localhost (via nginx) |
-37
View File
@@ -1,37 +0,0 @@
# ADR-001: Next.js as the Foundation for an AI Gateway
## Status: Accepted
## Context
OmniRoute is an AI routing gateway that translates, forwards, and manages requests across 20+ LLM providers. We needed a framework that could serve both the API proxy layer and a management dashboard from a single codebase.
**Alternatives considered:**
- **Express.js only** — Simpler proxy, but requires separate frontend tooling
- **Fastify** — Fast, but no built-in SSR/dashboard support
- **Next.js** — Unified full-stack framework with API routes, SSR, and static pages
## Decision
We chose Next.js because:
1. **Single deployment** — API routes (`/api/*`) and dashboard UI in one process
2. **Middleware layer** — Native request interception for auth guards and request tracing
3. **File-based routing** — Easy to map provider endpoints to handlers
4. **Built-in TypeScript** — Type safety across the entire codebase
## Consequences
**Positive:**
- One `npm run build` produces both API and UI
- Middleware provides centralized auth and request tracing
- Dashboard gets automatic code splitting and optimization
**Negative:**
- Next.js middleware has limitations (no heavy imports, edge runtime constraints)
- Serverless deployment model doesn't align with persistent WebSocket/SSE connections
- Build times are longer than Express-only setups
- The SSE proxy layer (`open-sse/`) operates outside Next.js conventions
-37
View File
@@ -1,37 +0,0 @@
# ADR-002: Hub-and-Spoke Translation with OpenAI as Intermediate Format
## Status: Accepted
## Context
OmniRoute routes requests across 20+ providers, each with its own API format (OpenAI, Anthropic Messages, Google Gemini, AWS Bedrock, etc.). Direct provider-to-provider translation would require O(n²) translators.
**Alternatives considered:**
- **Direct translation** — Each pair needs a dedicated translator (n² complexity)
- **Common intermediate format** — Translate to/from a canonical format (2n complexity)
- **Protocol buffers** — Strong typing but heavy overhead for a proxy
## Decision
We use the **OpenAI Chat Completions format** as the canonical intermediate representation. All incoming requests are normalized to OpenAI format, processed, then translated to the target provider's format.
```
Client → [any format] → OpenAI canonical → [target format] → Provider
Provider → [response] → OpenAI canonical → [original format] → Client
```
## Consequences
**Positive:**
- Only 2 translators per provider (inbound + outbound) instead of n² pairs
- OpenAI format is the de facto standard — most clients already use it
- Adding a new provider requires only implementing one translator pair
- Streaming (SSE) works consistently through the canonical format
**Negative:**
- Some provider-specific features may be lost in translation
- The double translation adds latency (typically < 5ms)
- OpenAI format changes require updating the canonical representation
-39
View File
@@ -1,39 +0,0 @@
# ADR-003: Dual Storage — SQLite Primary with JSON Migration Path
## Status: Accepted
## Context
OmniRoute originally used LowDB (JSON file) for all persistence. As the project grew, JSON-based storage became a bottleneck for concurrent access, querying, and data integrity.
**Alternatives considered:**
- **LowDB only** — Simple but no concurrent access, no ACID, no querying
- **SQLite only** — Fast, ACID-compliant, but breaks existing deployments
- **PostgreSQL** — Production-grade but requires external dependency
- **Dual storage with migration** — SQLite primary + automatic JSON migration
## Decision
We migrated to **SQLite as the primary store** with an automatic one-time migration from `db.json`:
1. On startup, if `db.json` exists and SQLite is empty, auto-migrate all data
2. All new reads/writes go through SQLite
3. The `db.json` file is preserved but no longer written to
Settings remain in a hybrid model where LowDB handles simple key-value configuration for backward compatibility.
## Consequences
**Positive:**
- ACID transactions for provider connections, API keys, and usage data
- Proper SQL queries for analytics and log filtering
- Concurrent read/write safety via WAL mode
- Zero-downtime migration from JSON — users upgrade transparently
**Negative:**
- Two storage engines to maintain (SQLite + LowDB for settings)
- Migration code must handle edge cases and partial data
- SQLite binary dependency needed in deployment environments
+5 -979
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

+11 -36
View File
@@ -1,41 +1,16 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import tseslint from "typescript-eslint";
/** @type {import("eslint").Linter.Config[]} */
const eslintConfig = [
const eslintConfig = defineConfig([
...nextVitals,
// FASE-02: Security rules (strict everywhere)
{
rules: {
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
},
},
// Relaxed rules for open-sse and tests (incremental adoption)
{
files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@next/next/no-assign-module-variable": "off",
"react-hooks/rules-of-hooks": "off",
},
},
// Global ignores (open-sse and tests REMOVED — now linted)
{
ignores: [
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
"scripts/**",
"bin/**",
"node_modules/**",
],
},
];
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
-134
View File
@@ -1,134 +0,0 @@
# OmniRoute
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 36+ AI providers — all through a single OpenAI-compatible endpoint.
## Overview
OmniRoute solves the problem of managing multiple AI provider subscriptions, quotas, and rate limits. It sits between your AI-powered tools (IDE agents, CLI tools) and AI providers, routing requests intelligently through a 4-tier fallback system: Subscription → API Key → Cheap → Free.
**Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost.
## Tech Stack
- **Runtime:** Node.js >= 18
- **Framework:** Next.js 16 (App Router) with TypeScript
- **Database:** SQLite via better-sqlite3 (local, zero-config)
- **State management:** Zustand (client), lowdb (server JSON persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
- **Background jobs:** Custom token health check scheduler
- **Streaming:** Server-Sent Events (SSE) for real-time proxy responses
- **Proxy engine:** Custom pipeline with format translation, circuit breaker, rate limiting
- **Package:** Published on npm (`omniroute`) and Docker Hub (`diegosouzapw/omniroute`)
## Project Structure
```
/
├── src/ # Main application source
│ ├── app/ # Next.js App Router pages and API routes
│ │ ├── (dashboard)/ # Dashboard UI pages (providers, combos, analytics, logs, etc.)
│ │ ├── api/ # REST API endpoints
│ │ │ ├── v1/ # OpenAI-compatible API (chat, models, embeddings, images, audio)
│ │ │ ├── oauth/ # OAuth flows per provider (authorize, exchange, callback)
│ │ │ ├── providers/ # Provider CRUD and batch testing
│ │ │ ├── models/ # Dashboard model listing and aliases
│ │ │ ├── combos/ # Combo CRUD (multi-model fallback chains)
│ │ │ └── ... # Other endpoints (usage, logs, health, settings, etc.)
│ │ └── login/ # Login page
│ ├── domain/ # Domain types and business logic interfaces
│ ├── lib/ # Core libraries
│ │ ├── db/ # SQLite database layer (providers, combos, prompts, logs)
│ │ ├── oauth/ # OAuth providers, services, and utilities
│ │ │ ├── providers/ # Provider-specific OAuth configs (GitHub, Google, Claude, etc.)
│ │ │ ├── services/ # Provider-specific token exchange logic
│ │ │ └── utils/ # PKCE, callback server, token helpers
│ │ ├── cloudSync.ts # Cloud sync via Cloudflare Workers
│ │ ├── tokenHealthCheck.ts # Background OAuth token refresh scheduler
│ │ └── localDb.ts # Unified database access layer
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, etc.)
│ │ ├── constants/ # Provider definitions, model lists, pricing
│ │ └── utils/ # Helpers (auth, CORS, error codes, machine ID)
│ ├── sse/ # SSE proxy pipeline
│ │ ├── services/ # Auth resolution, format translation, response handling
│ │ └── middleware/ # Rate limiting, circuit breaker, caching, idempotency
│ ├── store/ # Zustand client-side stores
│ ├── types/ # TypeScript type definitions
│ ├── proxy.ts # Main proxy request handler
│ └── server-init.ts # Server initialization (DB, health checks)
├── open-sse/ # Standalone SSE server (npm workspace)
│ ├── config/ # Model registries (embedding, image, audio, rerank, moderation)
│ ├── handlers/ # Request handlers per API type
│ └── translators/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses)
├── tests/ # Test suites
│ ├── unit/ # Unit tests (32+ test files)
│ └── integration/ # Integration tests
├── docs/ # Documentation and screenshots
├── bin/ # CLI entry points (omniroute, reset-password)
└── .env.example # Environment variable template
```
## Key Architectural Decisions
1. **OpenAI-compatible API surface:** All incoming requests follow the OpenAI API format (`/v1/chat/completions`, `/v1/models`, etc.). This makes OmniRoute a drop-in replacement for any tool that supports custom OpenAI endpoints.
2. **Provider abstraction via format translators:** Each AI provider (Claude, Gemini, etc.) has a translator in `open-sse/translators/` that converts between the OpenAI format and the provider's native format. This happens transparently.
3. **Connection-based provider model:** Providers are stored as "connections" in SQLite. Each connection has an `id`, `provider`, `authType` (oauth/apikey/free), `isActive` flag, and credentials. Multiple connections per provider are supported for multi-account rotation.
4. **Combo system for fallback:** Users create "combos" — ordered lists of `provider/model` pairs. The proxy tries each in order until one succeeds. Supports 6 strategies: fill-first, round-robin, power-of-two-choices, random, least-used, cost-optimized.
5. **SSE proxy pipeline (`src/sse/`):** The proxy pipeline is middleware-based: request → auth resolution → rate limiting → circuit breaker → format translation → upstream call → response translation → SSE streaming back to client.
6. **SQLite for persistence:** All state (providers, combos, logs, settings) is stored in a single SQLite database file at `data/omniroute.db`. This keeps the app self-contained and zero-config.
7. **OAuth with PKCE:** OAuth flows use PKCE for security. A local callback server (`src/lib/oauth/utils/server.ts`) handles the redirect. Token refresh is handled by a background job (`tokenHealthCheck.ts`).
## Main Flows
### Proxy Request Flow
1. Client sends OpenAI-format request to `/v1/chat/completions`
2. API key validation (`src/shared/utils/apiAuth.ts`)
3. Model resolution: direct model or combo lookup
4. For combos: iterate through models in fallback order
5. Auth resolution: get credentials for the target provider
6. Format translation: OpenAI → provider native format
7. Upstream request with circuit breaker and rate limiting
8. Response translation: provider → OpenAI format
9. SSE streaming back to client
### OAuth Flow
1. Dashboard initiates `/api/oauth/[provider]/authorize`
2. User completes OAuth login in browser
3. Callback hits `/api/oauth/[provider]/exchange`
4. Tokens stored as a provider connection in SQLite
5. Background job refreshes tokens before expiry
### Model Listing
- `/api/models` — Dashboard endpoint, lists all defined models with aliases
- `/v1/models` — OpenAI-compatible endpoint, lists only models from active providers
## Important Notes for LLMs
1. **Two model endpoints exist:** `/api/models` (dashboard, all models) and `/v1/models` (OpenAI-compatible, active only). Don't confuse them.
2. **Provider IDs vs aliases:** Providers have both an ID (`claude`, `github`) and a short alias (`cc`, `gh`). Models are referenced as `alias/model-name` (e.g., `cc/claude-opus-4-6`).
3. **The `open-sse/` directory is a separate npm workspace** with its own config, handlers, and translators. It handles the actual SSE streaming and format translation.
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database migrations:** SQLite schema is managed inline in `src/lib/db/core.ts` and `src/lib/db/providers.ts`. No migration framework — schema changes are applied on startup.
6. **Tests use Node.js built-in test runner:** Run `npm test` or `node --test tests/unit/*.test.mjs`. Playwright is used for E2E tests.
7. **The proxy pipeline is in `src/sse/`**, not in `src/app/api/v1/`. The API routes in `src/app/api/v1/` delegate to the SSE server running on a separate Express instance.
## Links
- Repository: https://github.com/diegosouzapw/OmniRoute
- Website: https://omniroute.online
- npm: https://www.npmjs.com/package/omniroute
- Docker Hub: https://hub.docker.com/r/diegosouzapw/omniroute
- Documentation: See `/docs/` directory
-7
View File
@@ -1,18 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {},
output: "standalone",
serverExternalPackages: ["better-sqlite3"],
transpilePackages: ["@omniroute/open-sse"],
allowedDevOrigins: ["192.168.*"],
typescript: {
// All TS errors resolved — strict checking enforced
ignoreBuildErrors: false,
},
images: {
unoptimized: true,
},
// NEXT_PUBLIC_CLOUD_URL is set in .env — do NOT hardcode here (it overrides .env)
webpack: (config, { isServer }) => {
// Ignore fs/path modules in browser bundle
@@ -6,22 +6,7 @@
* - /v1/audio/speech (TTS API)
*/
interface AudioModel {
id: string;
name: string;
}
interface AudioProvider {
id: string;
baseUrl: string;
authType: string;
authHeader: string;
format?: string;
async?: boolean;
models: AudioModel[];
}
export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
export const AUDIO_TRANSCRIPTION_PROVIDERS = {
openai: {
id: "openai",
baseUrl: "https://api.openai.com/v1/audio/transcriptions",
@@ -72,7 +57,7 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
},
};
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
export const AUDIO_SPEECH_PROVIDERS = {
openai: {
id: "openai",
baseUrl: "https://api.openai.com/v1/audio/speech",
@@ -111,21 +96,21 @@ export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
/**
* Get transcription provider config by ID
*/
export function getTranscriptionProvider(providerId: string): AudioProvider | null {
export function getTranscriptionProvider(providerId) {
return AUDIO_TRANSCRIPTION_PROVIDERS[providerId] || null;
}
/**
* Get speech provider config by ID
*/
export function getSpeechProvider(providerId: string): AudioProvider | null {
export function getSpeechProvider(providerId) {
return AUDIO_SPEECH_PROVIDERS[providerId] || null;
}
/**
* Parse audio model string (format: "provider/model" or just "model")
*/
function parseAudioModel(modelStr: string | null, registry: Record<string, AudioProvider>): { provider: string | null; model: string | null } {
function parseAudioModel(modelStr, registry) {
if (!modelStr) return { provider: null, model: null };
for (const [providerId, config] of Object.entries(registry)) {
@@ -143,11 +128,11 @@ function parseAudioModel(modelStr: string | null, registry: Record<string, Audio
return { provider: null, model: modelStr };
}
export function parseTranscriptionModel(modelStr: string | null) {
export function parseTranscriptionModel(modelStr) {
return parseAudioModel(modelStr, AUDIO_TRANSCRIPTION_PROVIDERS);
}
export function parseSpeechModel(modelStr: string | null) {
export function parseSpeechModel(modelStr) {
return parseAudioModel(modelStr, AUDIO_SPEECH_PROVIDERS);
}
@@ -1,4 +1,4 @@
import { loadProviderCredentials } from "./credentialLoader.ts";
import { loadProviderCredentials } from "./credentialLoader.js";
// Timeout for non-streaming fetch requests (ms). Prevents stalled connections.
export const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "120000", 10);
@@ -9,7 +9,7 @@ export const STREAM_IDLE_TIMEOUT_MS = parseInt(process.env.STREAM_IDLE_TIMEOUT_M
// Provider configurations
// OAuth credentials read from env vars with hardcoded fallbacks for backward compatibility.
// Use provider-credentials.json or env vars to override in production.
import { generateLegacyProviders } from "./providerRegistry.ts";
import { generateLegacyProviders } from "./providerRegistry.js";
export const PROVIDERS = generateLegacyProviders();
@@ -113,29 +113,12 @@ export const BACKOFF_CONFIG = {
maxLevel: 15, // Cap backoff level
};
// Configurable backoff steps for rate limits (Phase 1 — enhanced rate limiting)
// Used for per-model lockouts with increasing severity
export const BACKOFF_STEPS_MS = [60_000, 120_000, 300_000, 600_000, 1_200_000];
// 1min → 2min → 5min → 10min → 20min
// Structured error classification for rate limiting decisions
export const RateLimitReason = {
QUOTA_EXHAUSTED: "quota_exhausted", // Daily/monthly quota depleted
RATE_LIMIT_EXCEEDED: "rate_limit_exceeded", // RPM/RPD limits hit
MODEL_CAPACITY: "model_capacity", // Model overloaded (529, 503)
SERVER_ERROR: "server_error", // 5xx errors
AUTH_ERROR: "auth_error", // 401, 403
UNKNOWN: "unknown",
};
// Error-based cooldown times (aligned with CLIProxyAPI)
export const COOLDOWN_MS = {
unauthorized: 2 * 60 * 1000, // 401 → 2 min
paymentRequired: 2 * 60 * 1000, // 402/403 → 2 min
unauthorized: 2 * 60 * 1000, // 401 → 30 min
paymentRequired: 2 * 60 * 1000, // 402/403 → 30 min
notFound: 2 * 60 * 1000, // 404 → 2 minutes
transientInitial: 5 * 1000, // 408/500/502/503/504 first hit → 5s (backoff from here)
transientMax: 60 * 1000, // 502/503/504 backoff ceiling → 60s
transient: 5 * 1000, // Legacy alias → points to transientInitial
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
// Legacy aliases for backward compatibility
rateLimit: 2 * 60 * 1000,
@@ -143,33 +126,5 @@ export const COOLDOWN_MS = {
authExpired: 2 * 60 * 1000,
};
// ─── Provider Resilience Profiles ───────────────────────────────────────────
// Separate behavior for OAuth (low-limit, session-based) vs API Key (high-limit, metered)
export const PROVIDER_PROFILES = {
oauth: {
transientCooldown: 5000, // 5s (session tokens — short recovery)
rateLimitCooldown: 60000, // 60s default when no retry-after header
maxBackoffLevel: 8, // Higher ceiling (sessions may stay bad longer)
circuitBreakerThreshold: 3, // Opens fast (low limit providers)
circuitBreakerReset: 60000, // 1min reset
},
apikey: {
transientCooldown: 3000, // 3s (API providers recover faster)
rateLimitCooldown: 0, // 0 = respect retry-after header from provider
maxBackoffLevel: 5, // Lower ceiling (API quotas reset at known intervals)
circuitBreakerThreshold: 5, // More tolerant (occasional 502 is normal)
circuitBreakerReset: 30000, // 30s reset
},
};
// Default rate limit values for API Key providers (auto-enabled safety net)
// These are intentionally HIGH — they won't restrict normal usage.
// Real limits are learned from provider response headers.
export const DEFAULT_API_LIMITS = {
requestsPerMinute: 100, // 100 RPM (most APIs allow 60-600 RPM)
minTimeBetweenRequests: 200, // 200ms minimum gap
concurrentRequests: 10, // Max 10 parallel per provider
};
// Skip patterns - requests containing these texts will bypass provider
export const SKIP_PATTERNS = ["Please write a 5-10 word title for the following conversation:"];
@@ -1,4 +1,4 @@
import { generateModels, generateAliasMap } from "./providerRegistry.ts";
import { generateModels, generateAliasMap } from "./providerRegistry.js";
// Provider models - Generated from providerRegistry.js (single source of truth)
export const PROVIDER_MODELS = generateModels();
@@ -6,51 +6,9 @@
* is auto-generated from this registry.
*/
// ── Types ─────────────────────────────────────────────────────────────────
export interface RegistryModel {
id: string;
name: string;
targetFormat?: string;
}
export interface RegistryOAuth {
clientIdEnv?: string;
clientIdDefault?: string;
clientSecretEnv?: string;
clientSecretDefault?: string;
tokenUrl?: string;
refreshUrl?: string;
authUrl?: string;
initiateUrl?: string;
pollUrlBase?: string;
}
export interface RegistryEntry {
id: string;
alias: string;
format: string;
executor: string;
baseUrl?: string;
baseUrls?: string[];
responsesBaseUrl?: string;
urlSuffix?: string;
urlBuilder?: (base: string, model: string, stream: boolean) => string;
authType: string;
authHeader: string;
authPrefix?: string;
headers?: Record<string, string>;
extraHeaders?: Record<string, string>;
oauth?: RegistryOAuth;
models: RegistryModel[];
chatPath?: string;
clientVersion?: string;
passthroughModels?: boolean;
}
// ── Registry ──────────────────────────────────────────────────────────────
export const REGISTRY: Record<string, RegistryEntry> = {
export const REGISTRY = {
// ─── OAuth Providers ───────────────────────────────────────────────────
claude: {
id: "claude",
@@ -802,10 +760,10 @@ export const REGISTRY: Record<string, RegistryEntry> = {
// ── Generator Functions ───────────────────────────────────────────────────
/** Generate legacy PROVIDERS object shape for constants.js backward compatibility */
export function generateLegacyProviders(): Record<string, any> {
const providers: Record<string, any> = {};
export function generateLegacyProviders() {
const providers = {};
for (const [id, entry] of Object.entries(REGISTRY)) {
const p: Record<string, any> = { format: entry.format };
const p = { format: entry.format };
// URL(s)
if (entry.baseUrls) {
@@ -850,8 +808,8 @@ export function generateLegacyProviders(): Record<string, any> {
}
/** Generate PROVIDER_MODELS map (alias → model list) */
export function generateModels(): Record<string, RegistryModel[]> {
const models: Record<string, RegistryModel[]> = {};
export function generateModels() {
const models = {};
for (const entry of Object.values(REGISTRY)) {
if (entry.models && entry.models.length > 0) {
const key = entry.alias || entry.id;
@@ -865,8 +823,8 @@ export function generateModels(): Record<string, RegistryModel[]> {
}
/** Generate PROVIDER_ID_TO_ALIAS map */
export function generateAliasMap(): Record<string, string> {
const map: Record<string, string> = {};
export function generateAliasMap() {
const map = {};
for (const entry of Object.values(REGISTRY)) {
map[entry.id] = entry.alias || entry.id;
}
@@ -883,23 +841,11 @@ for (const entry of Object.values(REGISTRY)) {
}
/** Get registry entry by provider ID or alias */
export function getRegistryEntry(provider: string): RegistryEntry | null {
export function getRegistryEntry(provider) {
return REGISTRY[provider] || _byAlias.get(provider) || null;
}
/** Get all registered provider IDs */
export function getRegisteredProviders(): string[] {
export function getRegisteredProviders() {
return Object.keys(REGISTRY);
}
/**
* Get provider category: "oauth" or "apikey"
* Used by the resilience layer to apply different cooldown/backoff profiles.
* @param {string} provider - Provider ID or alias
* @returns {"oauth"|"apikey"}
*/
export function getProviderCategory(provider: string): "oauth" | "apikey" {
const entry = getRegistryEntry(provider);
if (!entry) return "apikey"; // Safe default for unknown providers
return entry.authType === "apikey" ? "apikey" : "oauth";
}
@@ -1,6 +1,6 @@
import crypto from "crypto";
import { BaseExecutor } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.js";
const MAX_RETRY_AFTER_MS = 10000;
@@ -1,4 +1,4 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.js";
/**
* BaseExecutor - Base class for provider executors.
@@ -6,10 +6,7 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
* (buildUrl, buildHeaders, transformRequest, etc.) for each provider.
*/
export class BaseExecutor {
provider: any;
config: any;
constructor(provider: any, config: any) {
constructor(provider, config) {
this.provider = provider;
this.config = config;
}
@@ -99,7 +96,7 @@ export class BaseExecutor {
? AbortSignal.any([signal, timeoutSignal])
: signal || timeoutSignal;
const fetchOptions: Record<string, any> = {
const fetchOptions = {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
@@ -1,6 +1,6 @@
import { BaseExecutor } from "./base.ts";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.ts";
import { PROVIDERS } from "../config/constants.ts";
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/constants.js";
/**
* Codex Executor - handles OpenAI Codex API (Responses API format)
@@ -1,4 +1,3 @@
declare var EdgeRuntime: any;
/**
* CursorExecutor Handles communication with the Cursor IDE API.
*
@@ -20,16 +19,16 @@ declare var EdgeRuntime: any;
* @see cursorProtobuf.js for Protobuf encoding/decoding utilities
*/
import { BaseExecutor } from "./base.ts";
import { PROVIDERS, HTTP_STATUS } from "../config/constants.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, HTTP_STATUS } from "../config/constants.js";
import {
generateCursorBody,
parseConnectRPCFrame,
extractTextFromResponse,
} from "../utils/cursorProtobuf.ts";
import { estimateUsage } from "../utils/usageTracking.ts";
import { FORMATS } from "../translator/formats.ts";
import { buildCursorRequest } from "../translator/request/openai-to-cursor.ts";
} from "../utils/cursorProtobuf.js";
import { estimateUsage } from "../utils/usageTracking.js";
import { FORMATS } from "../translator/formats.js";
import { buildCursorRequest } from "../translator/request/openai-to-cursor.js";
import crypto from "crypto";
import { v5 as uuidv5 } from "uuid";
import zlib from "zlib";
@@ -227,7 +226,7 @@ export class CursorExecutor extends BaseExecutor {
return {
status: response.status,
headers: Object.fromEntries((response.headers as any).entries()),
headers: Object.fromEntries(response.headers.entries()),
body: Buffer.from(await response.arrayBuffer()),
};
}
@@ -291,7 +290,7 @@ export class CursorExecutor extends BaseExecutor {
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {
const response: any = http2
const response = http2
? await this.makeHttp2Request(url, headers, transformedBody, signal)
: await this.makeFetchRequest(url, headers, transformedBody, signal);
@@ -459,7 +458,8 @@ export class CursorExecutor extends BaseExecutor {
console.log(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
const message: Record<string, any> = { role: "assistant",
const message = {
role: "assistant",
content: totalContent || null,
};
@@ -1,6 +1,6 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
import { getAccessToken } from "../services/tokenRefresh.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
import { getAccessToken } from "../services/tokenRefresh.js";
export class DefaultExecutor extends BaseExecutor {
constructor(provider) {
@@ -1,5 +1,5 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
export class GeminiCLIExecutor extends BaseExecutor {
constructor() {
@@ -1,6 +1,6 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
import { getModelTargetFormat } from "../config/providerModels.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
import { getModelTargetFormat } from "../config/providerModels.js";
export class GithubExecutor extends BaseExecutor {
constructor() {
+38
View File
@@ -0,0 +1,38 @@
import { AntigravityExecutor } from "./antigravity.js";
import { GeminiCLIExecutor } from "./gemini-cli.js";
import { GithubExecutor } from "./github.js";
import { KiroExecutor } from "./kiro.js";
import { CodexExecutor } from "./codex.js";
import { CursorExecutor } from "./cursor.js";
import { DefaultExecutor } from "./default.js";
const executors = {
antigravity: new AntigravityExecutor(),
"gemini-cli": new GeminiCLIExecutor(),
github: new GithubExecutor(),
kiro: new KiroExecutor(),
codex: new CodexExecutor(),
cursor: new CursorExecutor(),
cu: new CursorExecutor(), // Alias for cursor
};
const defaultCache = new Map();
export function getExecutor(provider) {
if (executors[provider]) return executors[provider];
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
return defaultCache.get(provider);
}
export function hasSpecializedExecutor(provider) {
return !!executors[provider];
}
export { BaseExecutor } from "./base.js";
export { AntigravityExecutor } from "./antigravity.js";
export { GeminiCLIExecutor } from "./gemini-cli.js";
export { GithubExecutor } from "./github.js";
export { KiroExecutor } from "./kiro.js";
export { CodexExecutor } from "./codex.js";
export { CursorExecutor } from "./cursor.js";
export { DefaultExecutor } from "./default.js";
-38
View File
@@ -1,38 +0,0 @@
import { AntigravityExecutor } from "./antigravity.ts";
import { GeminiCLIExecutor } from "./gemini-cli.ts";
import { GithubExecutor } from "./github.ts";
import { KiroExecutor } from "./kiro.ts";
import { CodexExecutor } from "./codex.ts";
import { CursorExecutor } from "./cursor.ts";
import { DefaultExecutor } from "./default.ts";
const executors = {
antigravity: new AntigravityExecutor(),
"gemini-cli": new GeminiCLIExecutor(),
github: new GithubExecutor(),
kiro: new KiroExecutor(),
codex: new CodexExecutor(),
cursor: new CursorExecutor(),
cu: new CursorExecutor(), // Alias for cursor
};
const defaultCache = new Map();
export function getExecutor(provider) {
if (executors[provider]) return executors[provider];
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
return defaultCache.get(provider);
}
export function hasSpecializedExecutor(provider) {
return !!executors[provider];
}
export { BaseExecutor } from "./base.ts";
export { AntigravityExecutor } from "./antigravity.ts";
export { GeminiCLIExecutor } from "./gemini-cli.ts";
export { GithubExecutor } from "./github.ts";
export { KiroExecutor } from "./kiro.ts";
export { CodexExecutor } from "./codex.ts";
export { CursorExecutor } from "./cursor.ts";
export { DefaultExecutor } from "./default.ts";
@@ -1,7 +1,7 @@
import { BaseExecutor } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/constants.js";
import { v4 as uuidv4 } from "uuid";
import { refreshKiroToken } from "../services/tokenRefresh.ts";
import { refreshKiroToken } from "../services/tokenRefresh.js";
// ── CRC32 lookup table (IEEE polynomial, no dependency) ──
const CRC32_TABLE = new Uint32Array(256);
@@ -83,7 +83,8 @@ export class KiroExecutor extends BaseExecutor {
let chunkIndex = 0;
const responseId = `chatcmpl-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const state: Record<string, any> = { endDetected: false,
const state = {
endDetected: false,
finishEmitted: false,
hasToolCalls: false,
toolCallIndex: 0,
@@ -125,7 +126,7 @@ export class KiroExecutor extends BaseExecutor {
const content = event.payload.content;
state.totalContentLength += content.length;
const chunk: Record<string, any> = {
const chunk = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -144,7 +145,7 @@ export class KiroExecutor extends BaseExecutor {
// Handle codeEvent
if (eventType === "codeEvent" && event.payload?.content) {
const chunk: Record<string, any> = {
const chunk = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -256,7 +257,7 @@ export class KiroExecutor extends BaseExecutor {
// Handle messageStopEvent
if (eventType === "messageStopEvent") {
const chunk: Record<string, any> = {
const chunk = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -329,7 +330,7 @@ export class KiroExecutor extends BaseExecutor {
};
}
const finishChunk: Record<string, any> = {
const finishChunk = {
id: responseId,
object: "chat.completion.chunk",
created,
@@ -1,4 +1,3 @@
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Audio Speech Handler (TTS)
*
@@ -11,8 +10,8 @@ import { getCorsOrigin } from "../utils/cors.ts";
* - Deepgram: POST { text } with model via query param, Token auth
*/
import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts";
import { errorResponse } from "../utils/error.ts";
import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.js";
import { errorResponse } from "../utils/error.js";
/**
* Build auth header for a speech provider
@@ -41,10 +40,7 @@ async function handleHyperbolicSpeech(providerConfig, body, token) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -56,7 +52,7 @@ async function handleHyperbolicSpeech(providerConfig, body, token) {
status: 200,
headers: {
"Content-Type": "audio/mpeg",
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
},
});
}
@@ -81,10 +77,7 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -93,7 +86,7 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
"Transfer-Encoding": "chunked",
},
});
@@ -107,13 +100,12 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
export async function handleAudioSpeech({ body, credentials }) {
if (!body.model) {
return errorResponse(400, "model is required");
return errorResponse("model is required", 400);
}
if (!body.input) {
return errorResponse(400, "input is required");
return errorResponse("input is required", 400);
}
const { provider: providerId, model: modelId } = parseSpeechModel(body.model);
@@ -121,14 +113,14 @@ export async function handleAudioSpeech({ body, credentials }) {
if (!providerConfig) {
return errorResponse(
400,
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram`
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram`,
400
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for speech provider: ${providerId}`);
return errorResponse(`No credentials for speech provider: ${providerId}`, 401);
}
try {
@@ -161,10 +153,7 @@ export async function handleAudioSpeech({ body, credentials }) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -174,11 +163,11 @@ export async function handleAudioSpeech({ body, credentials }) {
status: 200,
headers: {
"Content-Type": contentType,
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
"Transfer-Encoding": "chunked",
},
});
} catch (err) {
return errorResponse(500, `Speech request failed: ${err.message}`);
return errorResponse(`Speech request failed: ${err.message}`, 500);
}
}
@@ -1,4 +1,3 @@
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Audio Transcription Handler
*
@@ -11,8 +10,8 @@ import { getCorsOrigin } from "../utils/cors.ts";
* - AssemblyAI: async workflow (upload submit poll)
*/
import { getTranscriptionProvider, parseTranscriptionModel } from "../config/audioRegistry.ts";
import { errorResponse } from "../utils/error.ts";
import { getTranscriptionProvider, parseTranscriptionModel } from "../config/audioRegistry.js";
import { errorResponse } from "../utils/error.js";
/**
* Build auth header for a transcription provider
@@ -47,10 +46,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -58,7 +54,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
// Transform Deepgram response to OpenAI Whisper format
const text = data.results?.channels?.[0]?.alternatives?.[0]?.transcript || "";
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } });
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": "*" } });
}
/**
@@ -82,10 +78,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
const errText = await uploadRes.text();
return new Response(errText, {
status: uploadRes.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -109,10 +102,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
const errText = await submitRes.text();
return new Response(errText, {
status: submitRes.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -134,16 +124,16 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
if (result.status === "completed") {
return Response.json(
{ text: result.text || "" },
{ headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }
{ headers: { "Access-Control-Allow-Origin": "*" } }
);
}
if (result.status === "error") {
return errorResponse(500, result.error || "AssemblyAI transcription failed");
return errorResponse(result.error || "AssemblyAI transcription failed", 500);
}
}
return errorResponse(504, "AssemblyAI transcription timed out after 120s");
return errorResponse("AssemblyAI transcription timed out after 120s", 504);
}
/**
@@ -154,16 +144,15 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
export async function handleAudioTranscription({ formData, credentials }) {
const model = formData.get("model");
if (!model) {
return errorResponse(400, "model is required");
return errorResponse("model is required", 400);
}
const file = formData.get("file");
if (!file) {
return errorResponse(400, "file is required");
return errorResponse("file is required", 400);
}
const { provider: providerId, model: modelId } = parseTranscriptionModel(model);
@@ -171,14 +160,14 @@ export async function handleAudioTranscription({ formData, credentials }) {
if (!providerConfig) {
return errorResponse(
400,
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai`
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai`,
400
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for transcription provider: ${providerId}`);
return errorResponse(`No credentials for transcription provider: ${providerId}`, 401);
}
// Route to provider-specific handler
@@ -192,11 +181,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
// Default: OpenAI/Groq-compatible multipart proxy
const upstreamForm = new FormData();
upstreamForm.append(
"file",
/** @type {Blob} */ file,
/** @type {any} */ file.name || "audio.wav"
);
upstreamForm.append("file", file, file.name || "audio.wav");
upstreamForm.append("model", modelId);
// Forward optional parameters
@@ -209,7 +194,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
]) {
const val = formData.get(key);
if (val !== null && val !== undefined) {
upstreamForm.append(key, /** @type {string} */ val);
upstreamForm.append(key, val);
}
}
@@ -224,10 +209,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
@@ -236,9 +218,9 @@ export async function handleAudioTranscription({ formData, credentials }) {
return new Response(data, {
status: 200,
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": getCorsOrigin() },
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": "*" },
});
} catch (err) {
return errorResponse(500, `Transcription request failed: ${err.message}`);
return errorResponse(`Transcription request failed: ${err.message}`, 500);
}
}
@@ -1,44 +1,34 @@
import { getCorsOrigin } from "../utils/cors.ts";
import { detectFormat, getTargetFormat } from "../services/provider.ts";
import { translateRequest, needsTranslation } from "../translator/index.ts";
import { FORMATS } from "../translator/formats.ts";
import { detectFormat, getTargetFormat } from "../services/provider.js";
import { translateRequest, needsTranslation } from "../translator/index.js";
import { FORMATS } from "../translator/formats.js";
import {
createSSETransformStreamWithLogger,
createPassthroughStreamWithLogger,
COLORS,
} from "../utils/stream.ts";
import { createStreamController, pipeWithDisconnect } from "../utils/streamHandler.ts";
import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/usageTracking.ts";
import { refreshWithRetry } from "../services/tokenRefresh.ts";
import { createRequestLogger } from "../utils/requestLogger.ts";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.ts";
import { HTTP_STATUS } from "../config/constants.ts";
import { handleBypassRequest } from "../utils/bypassHandler.ts";
} from "../utils/stream.js";
import { createStreamController, pipeWithDisconnect } from "../utils/streamHandler.js";
import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/usageTracking.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { createRequestLogger } from "../utils/requestLogger.js";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS } from "../config/constants.js";
import { handleBypassRequest } from "../utils/bypassHandler.js";
import {
saveRequestUsage,
trackPendingRequest,
appendRequestLog,
saveCallLog,
} from "@/lib/usageDb";
import { getExecutor } from "../executors/index.ts";
import { translateNonStreamingResponse } from "./responseTranslator.ts";
import { extractUsageFromResponse } from "./usageExtractor.ts";
import { parseSSEToOpenAIResponse, parseSSEToResponsesOutput } from "./sseParser.ts";
import { sanitizeOpenAIResponse } from "./responseSanitizer.ts";
} from "@/lib/usageDb.js";
import { getExecutor } from "../executors/index.js";
import { translateNonStreamingResponse } from "./responseTranslator.js";
import { extractUsageFromResponse } from "./usageExtractor.js";
import { parseSSEToOpenAIResponse, parseSSEToResponsesOutput } from "./sseParser.js";
import {
withRateLimit,
updateFromHeaders,
initializeRateLimits,
} from "../services/rateLimitManager.ts";
import {
generateSignature,
getCachedResponse,
setCachedResponse,
isCacheable,
} from "@/lib/semanticCache";
import { getIdempotencyKey, checkIdempotency, saveIdempotency } from "@/lib/idempotencyLayer";
import { createProgressTransform, wantsProgress } from "../utils/progressTracker.ts";
} from "../services/rateLimitManager.js";
/**
* Core chat handler - shared between SSE and Worker
@@ -54,7 +44,6 @@ import { createProgressTransform, wantsProgress } from "../utils/progressTracker
* @param {string} options.connectionId - Connection ID for usage tracking
* @param {object} options.apiKeyInfo - API key metadata for usage attribution
*/
/** @param {any} options */
export async function handleChatCore({
body,
modelInfo,
@@ -72,24 +61,6 @@ export async function handleChatCore({
const { provider, model } = modelInfo;
const startTime = Date.now();
// ── Phase 9.2: Idempotency check ──
const idempotencyKey = getIdempotencyKey(clientRawRequest?.headers);
const cachedIdemp = checkIdempotency(idempotencyKey);
if (cachedIdemp) {
log?.debug?.("IDEMPOTENCY", `Hit for key=${idempotencyKey?.slice(0, 12)}...`);
return {
success: true,
response: new Response(JSON.stringify(cachedIdemp.response), {
status: cachedIdemp.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
"X-OmniRoute-Idempotent": "true",
},
}),
};
}
// Initialize rate limit settings from persisted DB (once, lazy)
await initializeRateLimits();
@@ -110,27 +81,8 @@ export async function handleChatCore({
const modelTargetFormat = getModelTargetFormat(alias, model);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
// Default to false unless client explicitly sets stream: true (OpenAI spec compliant)
const stream = body.stream === true;
// ── Phase 9.1: Semantic cache check (non-streaming, temp=0 only) ──
if (isCacheable(body, clientRawRequest?.headers)) {
const signature = generateSignature(model, body.messages, body.temperature, body.top_p);
const cached = getCachedResponse(signature);
if (cached) {
log?.debug?.("CACHE", `Semantic cache HIT for ${model}`);
return {
success: true,
response: new Response(JSON.stringify(cached), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
"X-OmniRoute-Cache": "HIT",
},
}),
};
}
}
// Default to streaming unless client explicitly sets stream: false
const stream = body.stream !== false;
// Create request logger for this session: sourceFormat_targetFormat_model
const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model);
@@ -190,7 +142,7 @@ export async function handleChatCore({
status: statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
},
}
),
@@ -473,17 +425,10 @@ export async function handleChatCore({
}
// Translate response to client's expected format (usually OpenAI)
let translatedResponse = needsTranslation(targetFormat, sourceFormat)
const translatedResponse = needsTranslation(targetFormat, sourceFormat)
? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat)
: responseBody;
// Sanitize response for OpenAI SDK compatibility
// Strips non-standard fields (x_groq, usage_breakdown, service_tier, etc.)
// Extracts <think> tags into reasoning_content
if (sourceFormat === FORMATS.OPENAI) {
translatedResponse = sanitizeOpenAIResponse(translatedResponse);
}
// Add buffer and filter usage for client (to prevent CLI context errors)
if (translatedResponse?.usage) {
const buffered = addBufferToUsage(translatedResponse.usage);
@@ -499,24 +444,12 @@ export async function handleChatCore({
}
}
// ── Phase 9.1: Cache store (non-streaming, temp=0) ──
if (isCacheable(body, clientRawRequest?.headers)) {
const signature = generateSignature(model, body.messages, body.temperature, body.top_p);
const tokensSaved = usage?.prompt_tokens + usage?.completion_tokens || 0;
setCachedResponse(signature, model, translatedResponse, tokensSaved);
log?.debug?.("CACHE", `Stored response for ${model} (${tokensSaved} tokens)`);
}
// ── Phase 9.2: Save for idempotency ──
saveIdempotency(idempotencyKey, translatedResponse, 200);
return {
success: true,
response: new Response(JSON.stringify(translatedResponse), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
"X-OmniRoute-Cache": "MISS",
"Access-Control-Allow-Origin": "*",
},
}),
};
@@ -533,7 +466,7 @@ export async function handleChatCore({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
};
// Create transform stream with logger for streaming response
@@ -613,22 +546,12 @@ export async function handleChatCore({
);
}
// ── Phase 9.3: Progress tracking (opt-in) ──
const progressEnabled = wantsProgress(clientRawRequest?.headers);
let finalStream;
if (progressEnabled) {
const progressTransform = createProgressTransform({ signal: streamController.signal });
// Chain: provider → transform → progress → client
const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController);
finalStream = transformedBody.pipeThrough(progressTransform);
responseHeaders["X-OmniRoute-Progress"] = "enabled";
} else {
finalStream = pipeWithDisconnect(providerResponse, transformStream, streamController);
}
// Pipe response through transform with disconnect detection
const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController);
return {
success: true,
response: new Response(finalStream, {
response: new Response(transformedBody, {
headers: responseHeaders,
}),
};
@@ -13,8 +13,8 @@
* }
*/
import { getEmbeddingProvider, parseEmbeddingModel } from "../config/embeddingRegistry.ts";
import { saveCallLog } from "@/lib/usageDb";
import { getEmbeddingProvider, parseEmbeddingModel } from "../config/embeddingRegistry.js";
import { saveCallLog } from "@/lib/usageDb.js";
/**
* Handle embedding request
@@ -52,7 +52,7 @@ export async function handleEmbedding({ body, credentials, log }) {
}
// Build upstream request
const upstreamBody: Record<string, any> = {
const upstreamBody = {
model: model,
input: body.input,
};
@@ -15,8 +15,8 @@
* }
*/
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
import { saveCallLog } from "@/lib/usageDb";
import { getImageProvider, parseImageModel } from "../config/imageRegistry.js";
import { saveCallLog } from "@/lib/usageDb.js";
/**
* Handle image generation request
@@ -232,7 +232,7 @@ async function handleOpenAIImageGeneration({
};
// Build upstream request (OpenAI-compatible format)
const upstreamBody: Record<string, any> = {
const upstreamBody = {
model: model,
prompt: body.prompt,
};
@@ -1,12 +1,11 @@
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Moderation Handler
*
* Handles POST /v1/moderations (OpenAI Moderations API format).
*/
import { getModerationProvider, parseModerationModel } from "../config/moderationRegistry.ts";
import { errorResponse } from "../utils/error.ts";
import { getModerationProvider, parseModerationModel } from "../config/moderationRegistry.js";
import { errorResponse } from "../utils/error.js";
/**
* Handle moderation request
@@ -16,10 +15,9 @@ import { errorResponse } from "../utils/error.ts";
* @param {Object} options.credentials - Provider credentials { apiKey }
* @returns {Response}
*/
/** @returns {Promise<any>} */
export async function handleModeration({ body, credentials }) {
if (!body.input) {
return errorResponse(400, "input is required");
return errorResponse("input is required", 400);
}
// Default to latest moderation model
@@ -29,14 +27,14 @@ export async function handleModeration({ body, credentials }) {
if (!providerConfig) {
return errorResponse(
400,
`No moderation provider found for model "${model}". Available: openai`
`No moderation provider found for model "${model}". Available: openai`,
400
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for moderation provider: ${providerId}`);
return errorResponse(`No credentials for moderation provider: ${providerId}`, 401);
}
try {
@@ -56,18 +54,15 @@ export async function handleModeration({ body, credentials }) {
const errText = await res.text();
return new Response(errText, {
status: res.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": getCorsOrigin(),
},
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
});
}
const data = await res.json();
return Response.json(data, {
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
headers: { "Access-Control-Allow-Origin": "*" },
});
} catch (err) {
return errorResponse(500, `Moderation request failed: ${err.message}`);
return errorResponse(`Moderation request failed: ${err.message}`, 500);
}
}
@@ -1,4 +1,3 @@
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Rerank Handler
*
@@ -6,8 +5,8 @@ import { getCorsOrigin } from "../utils/cors.ts";
* Routes to the appropriate provider based on the model prefix or lookup.
*/
import { getRerankProvider, parseRerankModel } from "../config/rerankRegistry.ts";
import { errorResponse } from "../utils/error.ts";
import { getRerankProvider, parseRerankModel } from "../config/rerankRegistry.js";
import { errorResponse } from "../utils/error.js";
/**
* Build authorization header for a rerank provider
@@ -70,7 +69,6 @@ function transformResponseFromProvider(providerConfig, data) {
* @param {Object} options.credentials - Provider credentials { apiKey, accessToken }
* @returns {Response}
*/
/** @returns {Promise<any>} */
export async function handleRerank({
model,
query,
@@ -79,10 +77,10 @@ export async function handleRerank({
return_documents,
credentials,
}) {
if (!model) return errorResponse(400, "model is required");
if (!query) return errorResponse(400, "query is required");
if (!model) return errorResponse("model is required", 400);
if (!query) return errorResponse("query is required", 400);
if (!documents || !Array.isArray(documents) || documents.length === 0) {
return errorResponse(400, "documents must be a non-empty array");
return errorResponse("documents must be a non-empty array", 400);
}
const { provider: providerId, model: modelId } = parseRerankModel(model);
@@ -90,14 +88,14 @@ export async function handleRerank({
if (!providerConfig) {
return errorResponse(
400,
`No rerank provider found for model "${model}". Available: cohere, together, nvidia, fireworks`
`No rerank provider found for model "${model}". Available: cohere, together, nvidia, fireworks`,
400
);
}
const token = credentials?.apiKey || credentials?.accessToken;
if (!token) {
return errorResponse(401, `No credentials for rerank provider: ${providerId}`);
return errorResponse(`No credentials for rerank provider: ${providerId}`, 401);
}
const requestBody = transformRequestForProvider(providerConfig, {
@@ -121,8 +119,8 @@ export async function handleRerank({
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
return errorResponse(
res.status,
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`,
res.status
);
}
@@ -130,9 +128,9 @@ export async function handleRerank({
const result = transformResponseFromProvider(providerConfig, data);
return Response.json(result, {
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
headers: { "Access-Control-Allow-Origin": "*" },
});
} catch (err) {
return errorResponse(500, `Rerank request failed: ${err.message}`);
return errorResponse(`Rerank request failed: ${err.message}`, 500);
}
}
-269
View File
@@ -1,269 +0,0 @@
/**
* Response Sanitizer Normalizes LLM responses to strict OpenAI SDK format.
*
* Fixes Issues:
* 1. Strips non-standard fields (x_groq, usage_breakdown, service_tier) that
* break OpenAI Python SDK v1.83+ Pydantic validation (returns str instead of object)
* 2. Extracts <think> tags from thinking models into reasoning_content
* 3. Normalizes response id, object, and usage fields
* 4. Converts developer role system for non-OpenAI providers
*/
// ── Standard OpenAI ChatCompletion fields ──────────────────────────────────
const ALLOWED_TOP_LEVEL_FIELDS = new Set([
"id",
"object",
"created",
"model",
"choices",
"usage",
"system_fingerprint",
]);
const ALLOWED_USAGE_FIELDS = new Set([
"prompt_tokens",
"completion_tokens",
"total_tokens",
"prompt_tokens_details",
"completion_tokens_details",
]);
const ALLOWED_MESSAGE_FIELDS = new Set([
"role",
"content",
"tool_calls",
"function_call",
"refusal",
"reasoning_content",
]);
const ALLOWED_CHOICE_FIELDS = new Set(["index", "message", "delta", "finish_reason", "logprobs"]);
// ── Think tag regex ────────────────────────────────────────────────────────
// Matches <think>...</think> blocks (greedy, dotAll)
const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/gi;
/**
* Extract <think> blocks from text content and return separated parts.
* @returns {{ content: string, thinking: string | null }}
*/
export function extractThinkingFromContent(text: string): {
content: string;
thinking: string | null;
} {
if (!text || typeof text !== "string") {
return { content: text || "", thinking: null };
}
const thinkingParts: string[] = [];
let hasThinkTags = false;
const cleaned = text.replace(THINK_TAG_REGEX, (_, thinkContent) => {
hasThinkTags = true;
const trimmed = thinkContent.trim();
if (trimmed) {
thinkingParts.push(trimmed);
}
return "";
});
if (!hasThinkTags) {
return { content: text, thinking: null };
}
return {
content: cleaned.trim(),
thinking: thinkingParts.length > 0 ? thinkingParts.join("\n\n") : null,
};
}
/**
* Sanitize a non-streaming OpenAI ChatCompletion response.
* Strips non-standard fields and normalizes required fields.
*/
export function sanitizeOpenAIResponse(body: any): any {
if (!body || typeof body !== "object") return body;
// Build sanitized response with only allowed top-level fields
const sanitized: Record<string, any> = {};
// Ensure required fields exist
sanitized.id = normalizeResponseId(body.id);
sanitized.object = body.object || "chat.completion";
sanitized.created = body.created || Math.floor(Date.now() / 1000);
sanitized.model = body.model || "unknown";
// Sanitize choices
if (Array.isArray(body.choices)) {
sanitized.choices = body.choices.map((choice: any, idx: number) => sanitizeChoice(choice, idx));
} else {
sanitized.choices = [];
}
// Sanitize usage
if (body.usage && typeof body.usage === "object") {
sanitized.usage = sanitizeUsage(body.usage);
}
// Keep system_fingerprint if present (it's a valid OpenAI field)
if (body.system_fingerprint) {
sanitized.system_fingerprint = body.system_fingerprint;
}
return sanitized;
}
/**
* Sanitize a single choice object.
*/
function sanitizeChoice(choice: any, defaultIndex: number): any {
const sanitized: Record<string, any> = {
index: choice.index ?? defaultIndex,
finish_reason: choice.finish_reason || null,
};
// Sanitize message (non-streaming) or delta (streaming)
if (choice.message) {
sanitized.message = sanitizeMessage(choice.message);
}
if (choice.delta) {
sanitized.delta = sanitizeMessage(choice.delta);
}
// Keep logprobs if present
if (choice.logprobs !== undefined) {
sanitized.logprobs = choice.logprobs;
}
return sanitized;
}
/**
* Sanitize a message object, extracting <think> tags if present.
*/
function sanitizeMessage(msg: any): any {
if (!msg || typeof msg !== "object") return msg;
const sanitized: Record<string, any> = {};
// Copy only allowed fields
if (msg.role) sanitized.role = msg.role;
if (msg.refusal !== undefined) sanitized.refusal = msg.refusal;
// Handle content — extract <think> tags
if (typeof msg.content === "string") {
const { content, thinking } = extractThinkingFromContent(msg.content);
sanitized.content = content;
// Set reasoning_content from <think> tags (if not already set)
if (thinking && !msg.reasoning_content) {
sanitized.reasoning_content = thinking;
}
} else if (msg.content !== undefined) {
sanitized.content = msg.content;
}
// Preserve existing reasoning_content (from providers that natively support it)
if (msg.reasoning_content && !sanitized.reasoning_content) {
sanitized.reasoning_content = msg.reasoning_content;
}
// Preserve tool_calls
if (msg.tool_calls) {
sanitized.tool_calls = msg.tool_calls;
}
// Preserve function_call (legacy)
if (msg.function_call) {
sanitized.function_call = msg.function_call;
}
return sanitized;
}
/**
* Sanitize usage object keep only standard fields.
*/
function sanitizeUsage(usage: any): any {
if (!usage || typeof usage !== "object") return usage;
const sanitized: Record<string, any> = {};
for (const key of ALLOWED_USAGE_FIELDS) {
if (usage[key] !== undefined) {
sanitized[key] = usage[key];
}
}
// Ensure required fields
if (sanitized.prompt_tokens === undefined) sanitized.prompt_tokens = 0;
if (sanitized.completion_tokens === undefined) sanitized.completion_tokens = 0;
if (sanitized.total_tokens === undefined) {
sanitized.total_tokens = sanitized.prompt_tokens + sanitized.completion_tokens;
}
return sanitized;
}
/**
* Normalize response ID to use chatcmpl- prefix.
*/
function normalizeResponseId(id: any): string {
if (!id || typeof id !== "string") {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
// Already correct format
if (id.startsWith("chatcmpl-")) return id;
// Keep custom IDs but don't break them
return id;
}
/**
* Sanitize a streaming SSE chunk for passthrough mode.
* Lighter than full sanitization only strips problematic extra fields.
*/
export function sanitizeStreamingChunk(parsed: any): any {
if (!parsed || typeof parsed !== "object") return parsed;
// Build sanitized chunk
const sanitized: Record<string, any> = {};
// Keep only standard fields
if (parsed.id !== undefined) sanitized.id = parsed.id;
sanitized.object = parsed.object || "chat.completion.chunk";
if (parsed.created !== undefined) sanitized.created = parsed.created;
if (parsed.model !== undefined) sanitized.model = parsed.model;
// Sanitize choices with delta
if (Array.isArray(parsed.choices)) {
sanitized.choices = parsed.choices.map((choice: any) => {
const c: Record<string, any> = {
index: choice.index ?? 0,
};
if (choice.delta !== undefined) {
c.delta = {};
const delta = choice.delta;
if (delta.role !== undefined) c.delta.role = delta.role;
if (delta.content !== undefined) c.delta.content = delta.content;
if (delta.reasoning_content !== undefined)
c.delta.reasoning_content = delta.reasoning_content;
if (delta.tool_calls !== undefined) c.delta.tool_calls = delta.tool_calls;
if (delta.function_call !== undefined) c.delta.function_call = delta.function_call;
}
if (choice.finish_reason !== undefined) c.finish_reason = choice.finish_reason;
if (choice.logprobs !== undefined) c.logprobs = choice.logprobs;
return c;
});
}
// Sanitize usage if present
if (parsed.usage && typeof parsed.usage === "object") {
sanitized.usage = sanitizeUsage(parsed.usage);
}
// Keep system_fingerprint if present
if (parsed.system_fingerprint) {
sanitized.system_fingerprint = parsed.system_fingerprint;
}
return sanitized;
}
@@ -1,4 +1,4 @@
import { FORMATS } from "../translator/formats.ts";
import { FORMATS } from "../translator/formats.js";
/**
* Translate non-streaming response to OpenAI format
@@ -56,7 +56,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
}
}
const message: Record<string, any> = { role: "assistant" };
const message = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -74,7 +74,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
const model = response?.model || responseBody?.model || "openai-responses";
const finishReason = toolCalls.length > 0 ? "tool_calls" : "stop";
const result: Record<string, any> = {
const result = {
id: `chatcmpl-${response?.id || Date.now()}`,
object: "chat.completion",
created: createdAt,
@@ -162,7 +162,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
}
// Build OpenAI format message
const message: Record<string, any> = { role: "assistant" };
const message = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -183,7 +183,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
finishReason = "tool_calls";
}
const result: Record<string, any> = {
const result = {
id: `chatcmpl-${response.responseId || Date.now()}`,
object: "chat.completion",
created: Math.floor(new Date(response.createTime || Date.now()).getTime() / 1000),
@@ -241,7 +241,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
}
}
const message: Record<string, any> = { role: "assistant" };
const message = { role: "assistant" };
if (textContent) {
message.content = textContent;
}
@@ -259,7 +259,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
if (finishReason === "end_turn") finishReason = "stop";
if (finishReason === "tool_use") finishReason = "tool_calls";
const result: Record<string, any> = {
const result = {
id: `chatcmpl-${responseBody.id || Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
@@ -1,12 +1,11 @@
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Responses API Handler for Workers
* Converts Chat Completions to Codex Responses API format
*/
import { handleChatCore } from "./chatCore.ts";
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.ts";
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.ts";
import { handleChatCore } from "./chatCore.js";
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js";
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js";
/**
* Handle /v1/responses request
@@ -46,11 +45,8 @@ export async function handleResponsesCore({
onCredentialsRefreshed,
onRequestSuccess,
onDisconnect,
clientRawRequest: null,
connectionId,
userAgent: null,
comboName: null,
} as any);
});
if (!result.success || !result.response) {
return result;
@@ -76,7 +72,7 @@ export async function handleResponsesCore({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": getCorsOrigin(),
"Access-Control-Allow-Origin": "*",
},
}),
};
@@ -44,14 +44,15 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
}
}
const message: Record<string, any> = { role: "assistant",
const message = {
role: "assistant",
content: contentParts.join(""),
};
if (reasoningParts.length > 0) {
message.reasoning_content = reasoningParts.join("");
}
const result: Record<string, any> = {
const result = {
id: first.id || `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: first.created || Math.floor(Date.now() / 1000),
+26 -26
View File
@@ -1,5 +1,5 @@
// Patch global fetch with proxy support (must be first)
import "./utils/proxyFetch.ts";
import "./utils/proxyFetch.js";
// Config
export {
@@ -10,7 +10,7 @@ export {
CLAUDE_SYSTEM_PROMPT,
COOLDOWN_MS,
BACKOFF_CONFIG,
} from "./config/constants.ts";
} from "./config/constants.js";
export {
PROVIDER_MODELS,
getProviderModels,
@@ -20,10 +20,10 @@ export {
getModelTargetFormat,
PROVIDER_ID_TO_ALIAS,
getModelsByProviderId,
} from "./config/providerModels.ts";
} from "./config/providerModels.js";
// Translator
export { FORMATS } from "./translator/formats.ts";
export { FORMATS } from "./translator/formats.js";
export {
register,
translateRequest,
@@ -31,7 +31,7 @@ export {
needsTranslation,
initState,
initTranslators,
} from "./translator/index.ts";
} from "./translator/index.js";
// Services
export {
@@ -40,16 +40,16 @@ export {
buildProviderUrl,
buildProviderHeaders,
getTargetFormat,
} from "./services/provider.ts";
} from "./services/provider.js";
export { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "./services/model.ts";
export { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "./services/model.js";
export {
checkFallbackError,
isAccountUnavailable,
getUnavailableUntil,
filterAvailableAccounts,
} from "./services/accountFallback.ts";
} from "./services/accountFallback.js";
export {
TOKEN_EXPIRY_BUFFER_MS,
@@ -63,43 +63,43 @@ export {
refreshCopilotToken,
getAccessToken,
refreshTokenByProvider,
} from "./services/tokenRefresh.ts";
} from "./services/tokenRefresh.js";
// Handlers
export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.ts";
export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.js";
export {
createStreamController,
pipeWithDisconnect,
createDisconnectAwareStream,
} from "./utils/streamHandler.ts";
} from "./utils/streamHandler.js";
// Executors
export { getExecutor, hasSpecializedExecutor } from "./executors/index.ts";
export { getExecutor, hasSpecializedExecutor } from "./executors/index.js";
// Utils
export { errorResponse, formatProviderError } from "./utils/error.ts";
export { errorResponse, formatProviderError } from "./utils/error.js";
export {
createSSETransformStreamWithLogger,
createPassthroughStreamWithLogger,
} from "./utils/stream.ts";
} from "./utils/stream.js";
// Embeddings
export { handleEmbedding } from "./handlers/embeddings.ts";
export { handleEmbedding } from "./handlers/embeddings.js";
export {
EMBEDDING_PROVIDERS,
getEmbeddingProvider,
parseEmbeddingModel,
getAllEmbeddingModels,
} from "./config/embeddingRegistry.ts";
} from "./config/embeddingRegistry.js";
// Image Generation
export { handleImageGeneration } from "./handlers/imageGeneration.ts";
export { handleImageGeneration } from "./handlers/imageGeneration.js";
export {
IMAGE_PROVIDERS,
getImageProvider,
parseImageModel,
getAllImageModels,
} from "./config/imageRegistry.ts";
} from "./config/imageRegistry.js";
// Think Tag Parser
export {
@@ -107,20 +107,20 @@ export {
extractThinkTags,
processStreamingThinkDelta,
flushThinkBuffer,
} from "./utils/thinkTagParser.ts";
} from "./utils/thinkTagParser.js";
// Rerank
export { handleRerank } from "./handlers/rerank.ts";
export { handleRerank } from "./handlers/rerank.js";
export {
RERANK_PROVIDERS,
getRerankProvider,
parseRerankModel,
getAllRerankModels,
} from "./config/rerankRegistry.ts";
} from "./config/rerankRegistry.js";
// Audio (Transcription + Speech)
export { handleAudioTranscription } from "./handlers/audioTranscription.ts";
export { handleAudioSpeech } from "./handlers/audioSpeech.ts";
export { handleAudioTranscription } from "./handlers/audioTranscription.js";
export { handleAudioSpeech } from "./handlers/audioSpeech.js";
export {
AUDIO_TRANSCRIPTION_PROVIDERS,
AUDIO_SPEECH_PROVIDERS,
@@ -129,13 +129,13 @@ export {
parseTranscriptionModel,
parseSpeechModel,
getAllAudioModels,
} from "./config/audioRegistry.ts";
} from "./config/audioRegistry.js";
// Moderations
export { handleModeration } from "./handlers/moderations.ts";
export { handleModeration } from "./handlers/moderations.js";
export {
MODERATION_PROVIDERS,
getModerationProvider,
parseModerationModel,
getAllModerationModels,
} from "./config/moderationRegistry.ts";
} from "./config/moderationRegistry.js";
+201
View File
@@ -0,0 +1,201 @@
import { COOLDOWN_MS, BACKOFF_CONFIG, HTTP_STATUS } from "../config/constants.js";
/**
* Calculate exponential backoff cooldown for rate limits (429)
* Level 0: 1s, Level 1: 2s, Level 2: 4s... max 2 min
* @param {number} backoffLevel - Current backoff level
* @returns {number} Cooldown in milliseconds
*/
export function getQuotaCooldown(backoffLevel = 0) {
const cooldown = BACKOFF_CONFIG.base * Math.pow(2, backoffLevel);
return Math.min(cooldown, BACKOFF_CONFIG.max);
}
/**
* Check if error should trigger account fallback (switch to next account)
* @param {number} status - HTTP status code
* @param {string} errorText - Error message text
* @param {number} backoffLevel - Current backoff level for exponential backoff
* @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number }}
*/
export function checkFallbackError(status, errorText, backoffLevel = 0) {
// Check error message FIRST - specific patterns take priority over status codes
if (errorText) {
const errorStr = typeof errorText === "string" ? errorText : JSON.stringify(errorText);
const lowerError = errorStr.toLowerCase();
if (lowerError.includes("no credentials")) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound };
}
if (lowerError.includes("request not allowed")) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed };
}
// Rate limit keywords - exponential backoff
if (
lowerError.includes("rate limit") ||
lowerError.includes("too many requests") ||
lowerError.includes("quota exceeded") ||
lowerError.includes("capacity") ||
lowerError.includes("overloaded")
) {
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
return {
shouldFallback: true,
cooldownMs: getQuotaCooldown(backoffLevel),
newBackoffLevel: newLevel,
};
}
}
if (status === HTTP_STATUS.UNAUTHORIZED) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.unauthorized };
}
if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.paymentRequired };
}
if (status === HTTP_STATUS.NOT_FOUND) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.notFound };
}
// 429 - Rate limit with exponential backoff
if (status === HTTP_STATUS.RATE_LIMITED) {
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
return {
shouldFallback: true,
cooldownMs: getQuotaCooldown(backoffLevel),
newBackoffLevel: newLevel,
};
}
// Transient errors
const transientStatuses = [
HTTP_STATUS.NOT_ACCEPTABLE,
HTTP_STATUS.REQUEST_TIMEOUT,
HTTP_STATUS.SERVER_ERROR,
HTTP_STATUS.BAD_GATEWAY,
HTTP_STATUS.SERVICE_UNAVAILABLE,
HTTP_STATUS.GATEWAY_TIMEOUT,
];
if (transientStatuses.includes(status)) {
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient };
}
// 400 Bad Request - don't fallback (same request will fail on all accounts)
if (status === HTTP_STATUS.BAD_REQUEST) {
return { shouldFallback: false, cooldownMs: 0 };
}
// All other errors - fallback with transient cooldown
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.transient };
}
/**
* Check if account is currently unavailable (cooldown not expired)
*/
export function isAccountUnavailable(unavailableUntil) {
if (!unavailableUntil) return false;
return new Date(unavailableUntil).getTime() > Date.now();
}
/**
* Calculate unavailable until timestamp
*/
export function getUnavailableUntil(cooldownMs) {
return new Date(Date.now() + cooldownMs).toISOString();
}
/**
* Get the earliest rateLimitedUntil from a list of accounts
* @param {Array} accounts - Array of account objects with rateLimitedUntil
* @returns {string|null} Earliest rateLimitedUntil ISO string, or null
*/
export function getEarliestRateLimitedUntil(accounts) {
let earliest = null;
const now = Date.now();
for (const acc of accounts) {
if (!acc.rateLimitedUntil) continue;
const until = new Date(acc.rateLimitedUntil).getTime();
if (until <= now) continue;
if (!earliest || until < earliest) earliest = until;
}
if (!earliest) return null;
return new Date(earliest).toISOString();
}
/**
* Format rateLimitedUntil to human-readable "reset after Xm Ys"
* @param {string} rateLimitedUntil - ISO timestamp
* @returns {string} e.g. "reset after 2m 30s"
*/
export function formatRetryAfter(rateLimitedUntil) {
if (!rateLimitedUntil) return "";
const diffMs = new Date(rateLimitedUntil).getTime() - Date.now();
if (diffMs <= 0) return "reset after 0s";
const totalSec = Math.ceil(diffMs / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
const parts = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s > 0 || parts.length === 0) parts.push(`${s}s`);
return `reset after ${parts.join(" ")}`;
}
/**
* Filter available accounts (not in cooldown)
*/
export function filterAvailableAccounts(accounts, excludeId = null) {
const now = Date.now();
return accounts.filter((acc) => {
if (excludeId && acc.id === excludeId) return false;
if (acc.rateLimitedUntil) {
const until = new Date(acc.rateLimitedUntil).getTime();
if (until > now) return false;
}
return true;
});
}
/**
* Reset account state when request succeeds
* Clears cooldown and resets backoff level to 0
* @param {object} account - Account object
* @returns {object} Updated account with reset state
*/
export function resetAccountState(account) {
if (!account) return account;
return {
...account,
rateLimitedUntil: null,
backoffLevel: 0,
lastError: null,
status: "active",
};
}
/**
* Apply error state to account
* @param {object} account - Account object
* @param {number} status - HTTP status code
* @param {string} errorText - Error message
* @returns {object} Updated account with error state
*/
export function applyErrorState(account, status, errorText) {
if (!account) return account;
const backoffLevel = account.backoffLevel || 0;
const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel);
return {
...account,
rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null,
backoffLevel: newBackoffLevel ?? backoffLevel,
lastError: { status, message: errorText, timestamp: new Date().toISOString() },
status: "error",
};
}
-514
View File
@@ -1,514 +0,0 @@
import {
COOLDOWN_MS,
BACKOFF_CONFIG,
BACKOFF_STEPS_MS,
RateLimitReason,
HTTP_STATUS,
PROVIDER_PROFILES,
} from "../config/constants.ts";
import { getProviderCategory } from "../config/providerRegistry.ts";
// ─── Provider Profile Helper ────────────────────────────────────────────────
/**
* Get the resilience profile for a provider (oauth or apikey).
* @param {string} provider - Provider ID or alias
* @returns {import('../config/constants.js').PROVIDER_PROFILES['oauth']}
*/
export function getProviderProfile(provider) {
const category = getProviderCategory(provider);
return PROVIDER_PROFILES[category] ?? PROVIDER_PROFILES.apikey;
}
// ─── Per-Model Lockout Tracking ─────────────────────────────────────────────
// In-memory map: "provider:connectionId:model" → { reason, until, lockedAt }
const modelLockouts = new Map();
// Auto-cleanup expired lockouts every 15 seconds
const _cleanupTimer = setInterval(() => {
const now = Date.now();
for (const [key, entry] of modelLockouts) {
if (now > entry.until) modelLockouts.delete(key);
}
}, 15_000);
_cleanupTimer.unref(); // Don't prevent process exit
/**
* Lock a specific model on a specific account
* @param {string} provider
* @param {string} connectionId
* @param {string} model
* @param {string} reason - from RateLimitReason
* @param {number} cooldownMs
*/
export function lockModel(provider, connectionId, model, reason, cooldownMs) {
if (!model) return; // No model → skip model-level locking
const key = `${provider}:${connectionId}:${model}`;
modelLockouts.set(key, {
reason,
until: Date.now() + cooldownMs,
lockedAt: Date.now(),
});
}
/**
* Check if a specific model on a specific account is locked
* @returns {boolean}
*/
export function isModelLocked(provider, connectionId, model) {
if (!model) return false;
const key = `${provider}:${connectionId}:${model}`;
const entry = modelLockouts.get(key);
if (!entry) return false;
if (Date.now() > entry.until) {
modelLockouts.delete(key);
return false;
}
return true;
}
/**
* Get model lockout info (for debugging/dashboard)
*/
export function getModelLockoutInfo(provider, connectionId, model) {
if (!model) return null;
const key = `${provider}:${connectionId}:${model}`;
const entry = modelLockouts.get(key);
if (!entry || Date.now() > entry.until) return null;
return {
reason: entry.reason,
remainingMs: entry.until - Date.now(),
lockedAt: new Date(entry.lockedAt).toISOString(),
};
}
/**
* Get all active model lockouts (for dashboard)
*/
export function getAllModelLockouts() {
const now = Date.now();
const active = [];
for (const [key, entry] of modelLockouts) {
if (now <= entry.until) {
const [provider, connectionId, model] = key.split(":");
active.push({
provider,
connectionId,
model,
reason: entry.reason,
remainingMs: entry.until - now,
});
}
}
return active;
}
// ─── Retry-After Parsing ────────────────────────────────────────────────────
/**
* Parse retry-after information from JSON error response bodies.
* Providers embed retry info in different formats.
*
* @param {string|object} responseBody - Raw response body or parsed JSON
* @returns {{ retryAfterMs: number|null, reason: string }}
*/
export function parseRetryAfterFromBody(responseBody) {
let body;
try {
body = typeof responseBody === "string" ? JSON.parse(responseBody) : responseBody;
} catch {
return { retryAfterMs: null, reason: RateLimitReason.UNKNOWN };
}
if (!body) return { retryAfterMs: null, reason: RateLimitReason.UNKNOWN };
// Gemini: { error: { details: [{ retryDelay: "33s" }] } }
const details = body.error?.details || body.details || [];
for (const detail of Array.isArray(details) ? details : []) {
if (detail.retryDelay) {
return {
retryAfterMs: parseDelayString(detail.retryDelay),
reason: RateLimitReason.RATE_LIMIT_EXCEEDED,
};
}
}
// OpenAI: "Please retry after 20s" in message
const msg = body.error?.message || body.message || "";
const retryMatch = msg.match(/retry\s+after\s+(\d+)\s*s/i);
if (retryMatch) {
return {
retryAfterMs: parseInt(retryMatch[1], 10) * 1000,
reason: RateLimitReason.RATE_LIMIT_EXCEEDED,
};
}
// Anthropic: error type classification
const errorType = body.error?.type || body.type || "";
if (errorType === "rate_limit_error") {
return { retryAfterMs: null, reason: RateLimitReason.RATE_LIMIT_EXCEEDED };
}
// Classify by error message keywords
const reason = classifyErrorText(msg || errorType);
return { retryAfterMs: null, reason };
}
/**
* Parse delay strings like "33s", "2m", "1h", "1500ms"
*/
function parseDelayString(value) {
if (!value) return null;
const str = String(value).trim();
const msMatch = str.match(/^(\d+)\s*ms$/i);
if (msMatch) return parseInt(msMatch[1], 10);
const secMatch = str.match(/^(\d+)\s*s$/i);
if (secMatch) return parseInt(secMatch[1], 10) * 1000;
const minMatch = str.match(/^(\d+)\s*m$/i);
if (minMatch) return parseInt(minMatch[1], 10) * 60 * 1000;
const hrMatch = str.match(/^(\d+)\s*h$/i);
if (hrMatch) return parseInt(hrMatch[1], 10) * 3600 * 1000;
// Bare number → seconds
const num = parseInt(str, 10);
return isNaN(num) ? null : num * 1000;
}
// ─── Error Classification ───────────────────────────────────────────────────
/**
* Classify error text into RateLimitReason
*/
export function classifyErrorText(errorText) {
if (!errorText) return RateLimitReason.UNKNOWN;
const lower = String(errorText).toLowerCase();
if (
lower.includes("quota exceeded") ||
lower.includes("quota depleted") ||
lower.includes("billing")
) {
return RateLimitReason.QUOTA_EXHAUSTED;
}
if (
lower.includes("rate limit") ||
lower.includes("too many requests") ||
lower.includes("rate_limit")
) {
return RateLimitReason.RATE_LIMIT_EXCEEDED;
}
if (
lower.includes("capacity") ||
lower.includes("overloaded") ||
lower.includes("resource exhausted")
) {
return RateLimitReason.MODEL_CAPACITY;
}
if (
lower.includes("unauthorized") ||
lower.includes("invalid api key") ||
lower.includes("authentication")
) {
return RateLimitReason.AUTH_ERROR;
}
if (lower.includes("server error") || lower.includes("internal error")) {
return RateLimitReason.SERVER_ERROR;
}
return RateLimitReason.UNKNOWN;
}
/**
* Classify HTTP status + error text into RateLimitReason
*/
export function classifyError(status, errorText) {
// Text classification takes priority (more specific)
const textReason = classifyErrorText(errorText);
if (textReason !== RateLimitReason.UNKNOWN) return textReason;
// Fall back to status code
if (status === HTTP_STATUS.UNAUTHORIZED || status === HTTP_STATUS.FORBIDDEN) {
return RateLimitReason.AUTH_ERROR;
}
if (status === HTTP_STATUS.PAYMENT_REQUIRED) {
return RateLimitReason.QUOTA_EXHAUSTED;
}
if (status === HTTP_STATUS.RATE_LIMITED) {
return RateLimitReason.RATE_LIMIT_EXCEEDED;
}
if (status === HTTP_STATUS.SERVICE_UNAVAILABLE || status === 529) {
return RateLimitReason.MODEL_CAPACITY;
}
if (status >= 500) {
return RateLimitReason.SERVER_ERROR;
}
return RateLimitReason.UNKNOWN;
}
// ─── Configurable Backoff ───────────────────────────────────────────────────
/**
* Get backoff duration from configurable steps.
* @param {number} failureCount - Number of consecutive failures
* @returns {number} Duration in ms
*/
export function getBackoffDuration(failureCount) {
const idx = Math.min(failureCount, BACKOFF_STEPS_MS.length - 1);
return BACKOFF_STEPS_MS[idx];
}
// ─── Original API (Backward Compatible) ────────────────────────────────────
/**
* Calculate exponential backoff cooldown for rate limits (429)
* Level 0: 1s, Level 1: 2s, Level 2: 4s... max 2 min
* @param {number} backoffLevel - Current backoff level
* @returns {number} Cooldown in milliseconds
*/
export function getQuotaCooldown(backoffLevel = 0) {
const cooldown = BACKOFF_CONFIG.base * Math.pow(2, backoffLevel);
return Math.min(cooldown, BACKOFF_CONFIG.max);
}
/**
* Check if error should trigger account fallback (switch to next account)
* @param {number} status - HTTP status code
* @param {string} errorText - Error message text
* @param {number} backoffLevel - Current backoff level for exponential backoff
* @param {string} [model] - Optional model name for model-level lockout
* @param {string} [provider] - Provider ID for profile-aware cooldowns
* @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number, reason?: string }}
*/
export function checkFallbackError(
status,
errorText,
backoffLevel = 0,
model = null,
provider = null
) {
// Check error message FIRST - specific patterns take priority over status codes
if (errorText) {
const errorStr = typeof errorText === "string" ? errorText : JSON.stringify(errorText);
const lowerError = errorStr.toLowerCase();
if (lowerError.includes("no credentials")) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.notFound,
reason: RateLimitReason.AUTH_ERROR,
};
}
if (lowerError.includes("request not allowed")) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.requestNotAllowed,
reason: RateLimitReason.RATE_LIMIT_EXCEEDED,
};
}
// Rate limit keywords - exponential backoff
if (
lowerError.includes("rate limit") ||
lowerError.includes("too many requests") ||
lowerError.includes("quota exceeded") ||
lowerError.includes("capacity") ||
lowerError.includes("overloaded")
) {
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
const reason = classifyErrorText(errorStr);
return {
shouldFallback: true,
cooldownMs: getQuotaCooldown(backoffLevel),
newBackoffLevel: newLevel,
reason,
};
}
}
if (status === HTTP_STATUS.UNAUTHORIZED) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.unauthorized,
reason: RateLimitReason.AUTH_ERROR,
};
}
if (status === HTTP_STATUS.PAYMENT_REQUIRED || status === HTTP_STATUS.FORBIDDEN) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.paymentRequired,
reason: RateLimitReason.QUOTA_EXHAUSTED,
};
}
if (status === HTTP_STATUS.NOT_FOUND) {
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.notFound,
reason: RateLimitReason.UNKNOWN,
};
}
// 429 - Rate limit with exponential backoff
if (status === HTTP_STATUS.RATE_LIMITED) {
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
return {
shouldFallback: true,
cooldownMs: getQuotaCooldown(backoffLevel),
newBackoffLevel: newLevel,
reason: RateLimitReason.RATE_LIMIT_EXCEEDED,
};
}
// Transient / server errors — exponential backoff with provider profile
const transientStatuses = [
HTTP_STATUS.NOT_ACCEPTABLE,
HTTP_STATUS.REQUEST_TIMEOUT,
HTTP_STATUS.SERVER_ERROR,
HTTP_STATUS.BAD_GATEWAY,
HTTP_STATUS.SERVICE_UNAVAILABLE,
HTTP_STATUS.GATEWAY_TIMEOUT,
];
if (transientStatuses.includes(status)) {
const profile = provider ? getProviderProfile(provider) : null;
const baseCooldown = profile?.transientCooldown ?? COOLDOWN_MS.transientInitial;
const maxLevel = profile?.maxBackoffLevel ?? BACKOFF_CONFIG.maxLevel;
const cooldownMs = Math.min(baseCooldown * Math.pow(2, backoffLevel), COOLDOWN_MS.transientMax);
const newLevel = Math.min(backoffLevel + 1, maxLevel);
return {
shouldFallback: true,
cooldownMs,
newBackoffLevel: newLevel,
reason: RateLimitReason.SERVER_ERROR,
};
}
// 400 Bad Request - don't fallback (same request will fail on all accounts)
if (status === HTTP_STATUS.BAD_REQUEST) {
return { shouldFallback: false, cooldownMs: 0, reason: RateLimitReason.UNKNOWN };
}
// All other errors - fallback with transient cooldown
return {
shouldFallback: true,
cooldownMs: COOLDOWN_MS.transient,
reason: RateLimitReason.UNKNOWN,
};
}
// ─── Account State Management ───────────────────────────────────────────────
/**
* Check if account is currently unavailable (cooldown not expired)
*/
export function isAccountUnavailable(unavailableUntil) {
if (!unavailableUntil) return false;
return new Date(unavailableUntil).getTime() > Date.now();
}
/**
* Calculate unavailable until timestamp
*/
export function getUnavailableUntil(cooldownMs) {
return new Date(Date.now() + cooldownMs).toISOString();
}
/**
* Get the earliest rateLimitedUntil from a list of accounts
*/
export function getEarliestRateLimitedUntil(accounts) {
let earliest = null;
const now = Date.now();
for (const acc of accounts) {
if (!acc.rateLimitedUntil) continue;
const until = new Date(acc.rateLimitedUntil).getTime();
if (until <= now) continue;
if (!earliest || until < earliest) earliest = until;
}
if (!earliest) return null;
return new Date(earliest).toISOString();
}
/**
* Format rateLimitedUntil to human-readable "reset after Xm Ys"
*/
export function formatRetryAfter(rateLimitedUntil) {
if (!rateLimitedUntil) return "";
const diffMs = new Date(rateLimitedUntil).getTime() - Date.now();
if (diffMs <= 0) return "reset after 0s";
const totalSec = Math.ceil(diffMs / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
const parts = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s > 0 || parts.length === 0) parts.push(`${s}s`);
return `reset after ${parts.join(" ")}`;
}
/**
* Filter available accounts (not in cooldown)
*/
export function filterAvailableAccounts(accounts, excludeId = null) {
const now = Date.now();
return accounts.filter((acc) => {
if (excludeId && acc.id === excludeId) return false;
if (acc.rateLimitedUntil) {
const until = new Date(acc.rateLimitedUntil).getTime();
if (until > now) return false;
}
return true;
});
}
/**
* Reset account state when request succeeds
*/
export function resetAccountState(account) {
if (!account) return account;
return {
...account,
rateLimitedUntil: null,
backoffLevel: 0,
lastError: null,
status: "active",
};
}
/**
* Apply error state to account
*/
export function applyErrorState(account, status, errorText, provider = null) {
if (!account) return account;
const backoffLevel = account.backoffLevel || 0;
const { cooldownMs, newBackoffLevel, reason } = checkFallbackError(
status,
errorText,
backoffLevel,
null,
provider
);
return {
...account,
rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null,
backoffLevel: newBackoffLevel ?? backoffLevel,
lastError: { status, message: errorText, timestamp: new Date().toISOString(), reason },
status: "error",
};
}
/**
* Get account health score (0-100) for P2C selection (Phase 9)
* @param {object} account
* @returns {number} score 0 = unhealthy, 100 = perfectly healthy
*/
export function getAccountHealth(account, model?: any) {
if (!account) return 0;
let score = 100;
score -= (account.backoffLevel || 0) * 10;
if (account.lastError) score -= 20;
if (account.rateLimitedUntil && isAccountUnavailable(account.rateLimitedUntil)) score -= 30;
return Math.max(0, score);
}
-74
View File
@@ -1,74 +0,0 @@
/**
* Quota-Aware Account Selection (P2C) Phase 9
*
* Power of Two Choices: pick 2 random accounts, select the healthier one.
* Uses account health scores from accountFallback.js.
*/
import { getAccountHealth } from "./accountFallback.ts";
/**
* P2C selection: pick 2 random candidates, return the healthier one.
* Falls back to random if only 1 candidate.
*
* @param {Array} accounts - Available account objects
* @param {string} [model] - Model name (for model-specific health check)
* @returns {object|null} Selected account
*/
export function selectAccountP2C(accounts, model = null) {
if (!accounts || accounts.length === 0) return null;
if (accounts.length === 1) return accounts[0];
// Pick 2 random distinct indices
const i = Math.floor(Math.random() * accounts.length);
let j = Math.floor(Math.random() * (accounts.length - 1));
if (j >= i) j++; // Ensure distinct
const a = accounts[i];
const b = accounts[j];
const healthA = getAccountHealth(a, model);
const healthB = getAccountHealth(b, model);
return healthA >= healthB ? a : b;
}
/**
* Select account with strategy support.
* Integrates P2C as a new strategy alongside existing fill-first and round-robin.
*
* @param {Array} accounts - Available accounts
* @param {string} strategy - "fill-first" | "round-robin" | "p2c" | "random"
* @param {object} [state] - Strategy state (e.g., lastIndex for round-robin)
* @param {string} [model] - Model name
* @returns {{ account: object|null, state: object }}
*/
export function selectAccount(accounts, strategy = "fill-first", state: any = {}, model = null) {
if (!accounts || accounts.length === 0) {
return { account: null, state };
}
switch (strategy) {
case "p2c":
return { account: selectAccountP2C(accounts, model), state };
case "random":
return {
account: accounts[Math.floor(Math.random() * accounts.length)],
state,
};
case "round-robin": {
const lastIndex = state.lastIndex ?? -1;
const nextIndex = (lastIndex + 1) % accounts.length;
return {
account: accounts[nextIndex],
state: { ...state, lastIndex: nextIndex },
};
}
case "fill-first":
default:
return { account: accounts[0], state };
}
}
@@ -1,18 +1,13 @@
/**
* Shared combo (model combo) handling with fallback support
* Supports: priority, weighted, round-robin, random, least-used, and cost-optimized strategies
* Supports: priority (sequential), weighted (probabilistic), and round-robin (circular) strategies
*/
import { checkFallbackError, formatRetryAfter, getProviderProfile } from "./accountFallback.ts";
import { unavailableResponse } from "../utils/error.ts";
import { recordComboRequest, getComboMetrics } from "./comboMetrics.ts";
import { resolveComboConfig, getDefaultComboConfig } from "./comboConfig.ts";
import * as semaphore from "./rateLimitSemaphore.ts";
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
import { parseModel } from "./model.ts";
// Status codes that should mark semaphore + record circuit breaker failures
const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504];
import { checkFallbackError, formatRetryAfter } from "./accountFallback.js";
import { unavailableResponse } from "../utils/error.js";
import { recordComboRequest } from "./comboMetrics.js";
import { resolveComboConfig, getDefaultComboConfig } from "./comboConfig.js";
import * as semaphore from "./rateLimitSemaphore.js";
const MAX_COMBO_DEPTH = 3;
@@ -150,69 +145,9 @@ function orderModelsForWeightedFallback(models, selectedModel) {
return [selected, ...rest].filter(Boolean).map((e) => e.model);
}
/**
* Fisher-Yates shuffle (in-place)
* @param {Array} arr
* @returns {Array} The shuffled array
*/
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
/**
* Sort models by pricing (cheapest first) for cost-optimized strategy
* @param {Array<string>} models - Model strings in "provider/model" format
* @returns {Promise<Array<string>>} Sorted model strings
*/
async function sortModelsByCost(models) {
try {
const { getPricingForModel } = await import("../../src/lib/localDb");
const withCost = await Promise.all(
models.map(async (modelStr) => {
const parsed = parseModel(modelStr);
const provider = parsed.provider || parsed.providerAlias || "unknown";
const model = parsed.model || modelStr;
try {
const pricing = await getPricingForModel(provider, model);
return { modelStr, cost: pricing?.input ?? Infinity };
} catch {
return { modelStr, cost: Infinity };
}
})
);
withCost.sort((a, b) => a.cost - b.cost);
return withCost.map((e) => e.modelStr);
} catch {
// If pricing lookup fails entirely, return original order
return models;
}
}
/**
* Sort models by usage count (least-used first) for least-used strategy
* @param {Array<string>} models - Model strings
* @param {string} comboName - Combo name for metrics lookup
* @returns {Array<string>} Sorted model strings
*/
function sortModelsByUsage(models, comboName) {
const metrics = getComboMetrics(comboName);
if (!metrics || !metrics.byModel) return models;
const withUsage = models.map((modelStr) => ({
modelStr,
requests: metrics.byModel[modelStr]?.requests ?? 0,
}));
withUsage.sort((a, b) => a.requests - b.requests);
return withUsage.map((e) => e.modelStr);
}
/**
* Handle combo chat with fallback
* Supports all 6 strategies: priority, weighted, round-robin, random, least-used, cost-optimized
* Supports priority (sequential) and weighted (probabilistic) strategies
* @param {Object} options
* @param {Object} options.body - Request body
* @param {Object} options.combo - Full combo object { name, models, strategy, config }
@@ -221,7 +156,6 @@ function sortModelsByUsage(models, comboName) {
* @param {Object} options.log - Logger object
* @returns {Promise<Response>}
*/
/** @param {any} options */
export async function handleComboChat({
body,
combo,
@@ -276,7 +210,7 @@ export async function handleComboChat({
);
} else {
orderedModels = flatModels;
log.info("COMBO", `${strategy} with nested resolution: ${orderedModels.length} total models`);
log.info("COMBO", `Priority with nested resolution: ${orderedModels.length} total models`);
}
} else if (strategy === "weighted") {
const selected = selectWeightedModel(models);
@@ -286,18 +220,6 @@ export async function handleComboChat({
orderedModels = models.map((m) => normalizeModelEntry(m).model);
}
// Apply strategy-specific ordering
if (strategy === "random") {
orderedModels = shuffleArray([...orderedModels]);
log.info("COMBO", `Random shuffle: ${orderedModels.length} models`);
} else if (strategy === "least-used") {
orderedModels = sortModelsByUsage(orderedModels, combo.name);
log.info("COMBO", `Least-used ordering: ${orderedModels[0]} has fewest requests`);
} else if (strategy === "cost-optimized") {
orderedModels = await sortModelsByCost(orderedModels);
log.info("COMBO", `Cost-optimized ordering: cheapest first (${orderedModels[0]})`);
}
let lastError = null;
let earliestRetryAfter = null;
let lastStatus = null;
@@ -307,20 +229,6 @@ export async function handleComboChat({
for (let i = 0; i < orderedModels.length; i++) {
const modelStr = orderedModels[i];
const parsed = parseModel(modelStr);
const provider = parsed.provider || parsed.providerAlias || "unknown";
const profile = getProviderProfile(provider);
const breaker = getCircuitBreaker(`combo:${provider}`, {
failureThreshold: profile.circuitBreakerThreshold,
resetTimeout: profile.circuitBreakerReset,
});
// Skip model if circuit breaker is OPEN
if (!breaker.canExecute()) {
log.info("COMBO", `Skipping ${modelStr}: circuit breaker OPEN for ${provider}`);
if (i > 0) fallbackCount++;
continue;
}
// Pre-check: skip models where all accounts are in cooldown
if (isModelAvailable) {
@@ -405,18 +313,7 @@ export async function handleComboChat({
}
}
const { shouldFallback, cooldownMs } = checkFallbackError(
result.status,
errorText,
0,
null,
provider
);
// Record failure in circuit breaker for transient errors
if (TRANSIENT_FOR_BREAKER.includes(result.status)) {
breaker._onFailure();
}
const { shouldFallback } = checkFallbackError(result.status, errorText);
if (!shouldFallback) {
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
@@ -438,24 +335,10 @@ export async function handleComboChat({
}
}
// Early exit: check if all models have breaker OPEN
const allBreakersOpen = orderedModels.every((m) => {
const p = parseModel(m).provider || parseModel(m).providerAlias || "unknown";
return !getCircuitBreaker(`combo:${p}`).canExecute();
});
// All models failed
const latencyMs = Date.now() - startTime;
recordComboRequest(combo.name, null, { success: false, latencyMs, fallbackCount, strategy });
if (allBreakersOpen) {
log.warn("COMBO", "All models have circuit breaker OPEN — aborting");
return unavailableResponse(
503,
"All providers temporarily unavailable (circuit breakers open)"
);
}
const status = lastStatus || 406;
const msg = lastError || "All combo models unavailable";
@@ -529,20 +412,6 @@ async function handleRoundRobinCombo({
for (let offset = 0; offset < modelCount; offset++) {
const modelIndex = (startIndex + offset) % modelCount;
const modelStr = orderedModels[modelIndex];
const parsed = parseModel(modelStr);
const provider = parsed.provider || parsed.providerAlias || "unknown";
const profile = getProviderProfile(provider);
const breaker = getCircuitBreaker(`combo:${provider}`, {
failureThreshold: profile.circuitBreakerThreshold,
resetTimeout: profile.circuitBreakerReset,
});
// Skip model if circuit breaker is OPEN
if (!breaker.canExecute()) {
log.info("COMBO-RR", `Skipping ${modelStr}: circuit breaker OPEN for ${provider}`);
if (offset > 0) fallbackCount++;
continue;
}
// Pre-check availability
if (isModelAvailable) {
@@ -641,22 +510,12 @@ async function handleRoundRobinCombo({
}
}
const { shouldFallback, cooldownMs } = checkFallbackError(
result.status,
errorText,
0,
null,
provider
);
const { shouldFallback, cooldownMs } = checkFallbackError(result.status, errorText);
// Transient errors → mark in semaphore AND record circuit breaker failure
if (TRANSIENT_FOR_BREAKER.includes(result.status) && cooldownMs > 0) {
// Rate-limited → mark in semaphore so queue pauses
if (result.status === 429 && cooldownMs > 0) {
semaphore.markRateLimited(modelStr, cooldownMs);
breaker._onFailure();
log.warn(
"COMBO-RR",
`${modelStr} error ${result.status}, cooldown ${cooldownMs}ms (breaker: ${breaker.getStatus().failureCount}/${profile.circuitBreakerThreshold})`
);
log.warn("COMBO-RR", `${modelStr} rate-limited, cooldown ${cooldownMs}ms`);
}
if (!shouldFallback) {
@@ -692,20 +551,6 @@ async function handleRoundRobinCombo({
strategy: "round-robin",
});
// Early exit: check if all models have breaker OPEN
const allBreakersOpen = orderedModels.every((m) => {
const p = parseModel(m).provider || parseModel(m).providerAlias || "unknown";
return !getCircuitBreaker(`combo:${p}`).canExecute();
});
if (allBreakersOpen) {
log.warn("COMBO-RR", "All models have circuit breaker OPEN — aborting");
return unavailableResponse(
503,
"All providers temporarily unavailable (circuit breakers open)"
);
}
const status = lastStatus || 406;
const msg = lastError || "All round-robin combo models unavailable";
@@ -27,7 +27,7 @@ const DEFAULT_COMBO_CONFIG = {
* @param {string} [provider] - Optional provider to apply provider-level overrides
* @returns {Object} Resolved config
*/
export function resolveComboConfig(combo, settings, provider?: any) {
export function resolveComboConfig(combo, settings, provider) {
const global = settings?.comboDefaults || {};
const providerOverride = provider ? settings?.providerOverrides?.[provider] || {} : {};
const comboConfig = combo?.config || {};
@@ -35,7 +35,7 @@ export function recordComboRequest(
});
}
const combo: any = metrics.get(comboName);
const combo = metrics.get(comboName);
combo.totalRequests++;
combo.totalLatencyMs += latencyMs;
combo.totalFallbacks += fallbackCount;
@@ -81,7 +81,7 @@ export function recordComboRequest(
* @returns {Object|null}
*/
export function getComboMetrics(comboName) {
const combo: any = metrics.get(comboName);
const combo = metrics.get(comboName);
if (!combo) return null;
return {
@@ -93,7 +93,7 @@ export function getComboMetrics(comboName) {
fallbackRate:
combo.totalRequests > 0 ? Math.round((combo.totalFallbacks / combo.totalRequests) * 100) : 0,
byModel: Object.fromEntries(
Object.entries(combo.byModel).map(([model, m]: [string, any]) => [
Object.entries(combo.byModel).map(([model, m]) => [
model,
{
...m,
@@ -110,7 +110,7 @@ export function getComboMetrics(comboName) {
* @returns {Object} Map of comboName metrics
*/
export function getAllComboMetrics() {
const result: Record<string, any> = {};
const result = {};
for (const [name] of metrics) {
result[name] = getComboMetrics(name);
}
-203
View File
@@ -1,203 +0,0 @@
/**
* Context Manager Phase 4
*
* Pre-flight context compression to prevent "prompt too long" errors.
* 3 layers: trim tool messages, compress thinking, aggressive purification.
*/
// Default token limits per provider (rough estimates based on model context windows)
const DEFAULT_LIMITS = {
claude: 200000,
openai: 128000,
gemini: 1000000,
default: 128000,
};
// Rough chars-per-token ratio for quick estimation
const CHARS_PER_TOKEN = 4;
/**
* Estimate token count from text length
*/
export function estimateTokens(text) {
if (!text) return 0;
const str = typeof text === "string" ? text : JSON.stringify(text);
return Math.ceil(str.length / CHARS_PER_TOKEN);
}
/**
* Get token limit for a provider/model combination
*/
export function getTokenLimit(provider, model = null) {
// Check if model has a known limit
if (model) {
const lower = model.toLowerCase();
if (lower.includes("claude")) return DEFAULT_LIMITS.claude;
if (lower.includes("gemini")) return DEFAULT_LIMITS.gemini;
if (lower.includes("gpt") || lower.includes("o1") || lower.includes("o3") || lower.includes("o4")) return DEFAULT_LIMITS.openai;
}
return DEFAULT_LIMITS[provider] || DEFAULT_LIMITS.default;
}
/**
* Apply context compression to request body.
* Operates in 3 layers of increasing aggressiveness:
*
* Layer 1: Trim tool_result messages (truncate long outputs)
* Layer 2: Compress thinking blocks (remove from history, keep last)
* Layer 3: Aggressive purification (drop old messages until fitting)
*
* @param {object} body - Request body with messages[]
* @param {object} options - { provider?, model?, maxTokens?, reserveTokens? }
* @returns {{ body: object, compressed: boolean, stats: object }}
*/
export function compressContext(body, options: any = {}) {
if (!body || !body.messages || !Array.isArray(body.messages)) {
return { body, compressed: false, stats: {} };
}
const provider = options.provider || "default";
const maxTokens = options.maxTokens || getTokenLimit(provider, body.model || options.model);
const reserveTokens = options.reserveTokens || 16000; // Reserve for response
const targetTokens = maxTokens - reserveTokens;
let messages = [...body.messages];
let currentTokens = estimateTokens(JSON.stringify(messages));
const stats = { original: currentTokens, layers: [] };
// Already fits
if (currentTokens <= targetTokens) {
return { body, compressed: false, stats: { original: currentTokens, final: currentTokens } };
}
// Layer 1: Trim tool_result/tool messages
messages = trimToolMessages(messages, 2000); // Max 2000 chars per tool result
currentTokens = estimateTokens(JSON.stringify(messages));
stats.layers.push({ name: "trim_tools", tokens: currentTokens });
if (currentTokens <= targetTokens) {
return {
body: { ...body, messages },
compressed: true,
stats: { ...stats, final: currentTokens },
};
}
// Layer 2: Compress thinking blocks (remove from non-last assistant messages)
messages = compressThinking(messages);
currentTokens = estimateTokens(JSON.stringify(messages));
stats.layers.push({ name: "compress_thinking", tokens: currentTokens });
if (currentTokens <= targetTokens) {
return {
body: { ...body, messages },
compressed: true,
stats: { ...stats, final: currentTokens },
};
}
// Layer 3: Aggressive purification — drop oldest messages keeping system + last N pairs
messages = purifyHistory(messages, targetTokens);
currentTokens = estimateTokens(JSON.stringify(messages));
stats.layers.push({ name: "purify_history", tokens: currentTokens });
return {
body: { ...body, messages },
compressed: true,
stats: { ...stats, final: currentTokens },
};
}
// ─── Layer 1: Trim Tool Messages ────────────────────────────────────────────
function trimToolMessages(messages, maxChars) {
return messages.map((msg) => {
if (msg.role === "tool" && typeof msg.content === "string" && msg.content.length > maxChars) {
return {
...msg,
content: msg.content.slice(0, maxChars) + "\n... [truncated]",
};
}
// Handle array content (Claude format with tool_result blocks)
if (msg.role === "user" && Array.isArray(msg.content)) {
return {
...msg,
content: msg.content.map((block) => {
if (block.type === "tool_result" && typeof block.content === "string" && block.content.length > maxChars) {
return { ...block, content: block.content.slice(0, maxChars) + "\n... [truncated]" };
}
return block;
}),
};
}
return msg;
});
}
// ─── Layer 2: Compress Thinking Blocks ──────────────────────────────────────
function compressThinking(messages) {
// Find last assistant message index
let lastAssistantIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "assistant") {
lastAssistantIdx = i;
break;
}
}
return messages.map((msg, i) => {
if (msg.role !== "assistant") return msg;
if (i === lastAssistantIdx) return msg; // Keep thinking in last assistant msg
// Remove thinking blocks from content array
if (Array.isArray(msg.content)) {
const filtered = msg.content.filter((block) => block.type !== "thinking");
if (filtered.length === 0) {
return { ...msg, content: "[thinking compressed]" };
}
return { ...msg, content: filtered };
}
// Remove thinking XML tags from string content
if (typeof msg.content === "string") {
const cleaned = msg.content
.replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
.replace(/<antThinking>[\s\S]*?<\/antThinking>/g, "")
.trim();
return { ...msg, content: cleaned || "[thinking compressed]" };
}
return msg;
});
}
// ─── Layer 3: Aggressive Purification ───────────────────────────────────────
function purifyHistory(messages, targetTokens) {
// Keep system message(s) and the last N message pairs
const system = messages.filter((m) => m.role === "system" || m.role === "developer");
const nonSystem = messages.filter((m) => m.role !== "system" && m.role !== "developer");
// Binary search for how many messages to keep from the end
let keep = nonSystem.length;
while (keep > 2) {
const candidate = [...system, ...nonSystem.slice(-keep)];
const tokens = estimateTokens(JSON.stringify(candidate));
if (tokens <= targetTokens) break;
keep = Math.max(2, Math.floor(keep * 0.7)); // Drop 30% each iteration
}
const result = [...system, ...nonSystem.slice(-keep)];
// Add summary of dropped messages
if (keep < nonSystem.length) {
const dropped = nonSystem.length - keep;
result.splice(system.length, 0, {
role: "system",
content: `[Context compressed: ${dropped} earlier messages removed to fit context window]`,
});
}
return result;
}
-249
View File
@@ -1,249 +0,0 @@
/**
* IP Filter Middleware Phase 6
*
* IP-based access control with blacklist, whitelist, priority modes, and temporary bans.
*/
// In-memory IP lists
let _config = {
enabled: false,
mode: "blacklist", // "blacklist" | "whitelist" | "whitelist-priority"
blacklist: new Set(),
whitelist: new Set(),
tempBans: new Map(), // IP → { until: timestamp, reason: string }
};
/**
* Configure the IP filter
*/
export function configureIPFilter(config) {
if (config.enabled !== undefined) _config.enabled = config.enabled;
if (config.mode) _config.mode = config.mode;
if (config.blacklist) _config.blacklist = new Set(config.blacklist);
if (config.whitelist) _config.whitelist = new Set(config.whitelist);
}
/**
* Get current IP filter config (for API)
*/
export function getIPFilterConfig() {
return {
enabled: _config.enabled,
mode: _config.mode,
blacklist: Array.from(_config.blacklist),
whitelist: Array.from(_config.whitelist),
tempBans: Array.from(_config.tempBans.entries()).map(([ip, info]) => ({
ip,
until: new Date(info.until).toISOString(),
reason: info.reason,
remainingMs: Math.max(0, info.until - Date.now()),
})),
};
}
/**
* Check if an IP is allowed
* @param {string} ip
* @returns {{ allowed: boolean, reason?: string }}
*/
export function checkIP(ip) {
if (!_config.enabled) return { allowed: true };
if (!ip) return { allowed: true };
const normalizedIP = normalizeIP(ip);
// Check temp bans first (highest priority)
const ban = _config.tempBans.get(normalizedIP);
if (ban) {
if (Date.now() < ban.until) {
return { allowed: false, reason: `Temporarily banned: ${ban.reason}` };
}
_config.tempBans.delete(normalizedIP); // Expired
}
switch (_config.mode) {
case "whitelist":
// Only whitelisted IPs allowed
if (!matchesAny(normalizedIP, _config.whitelist)) {
return { allowed: false, reason: "IP not in whitelist" };
}
return { allowed: true };
case "whitelist-priority":
// Whitelist overrides blacklist
if (matchesAny(normalizedIP, _config.whitelist)) {
return { allowed: true };
}
if (matchesAny(normalizedIP, _config.blacklist)) {
return { allowed: false, reason: "IP blacklisted" };
}
return { allowed: true };
case "blacklist":
default:
// Blacklisted IPs blocked
if (matchesAny(normalizedIP, _config.blacklist)) {
return { allowed: false, reason: "IP blacklisted" };
}
return { allowed: true };
}
}
/**
* Temporarily ban an IP
*/
export function tempBanIP(ip, durationMs, reason = "Automated ban") {
const normalizedIP = normalizeIP(ip);
_config.tempBans.set(normalizedIP, {
until: Date.now() + durationMs,
reason,
});
}
/**
* Remove a temporary ban
*/
export function removeTempBan(ip) {
_config.tempBans.delete(normalizeIP(ip));
}
/**
* Add IP to blacklist
*/
export function addToBlacklist(ip) {
_config.blacklist.add(normalizeIP(ip));
}
/**
* Remove IP from blacklist
*/
export function removeFromBlacklist(ip) {
_config.blacklist.delete(normalizeIP(ip));
}
/**
* Add IP to whitelist
*/
export function addToWhitelist(ip) {
_config.whitelist.add(normalizeIP(ip));
}
/**
* Remove IP from whitelist
*/
export function removeFromWhitelist(ip) {
_config.whitelist.delete(normalizeIP(ip));
}
/**
* Express/Next.js middleware factory
*/
export function createIPFilterMiddleware() {
return (req, res, next) => {
const ip = extractClientIP(req);
const { allowed, reason } = checkIP(ip);
if (!allowed) {
const statusCode = 403;
if (res.status) {
// Express
return res.status(statusCode).json({ error: reason || "Access denied" });
}
// Raw response
res.writeHead(statusCode, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: reason || "Access denied" }));
}
if (next) next();
};
}
/**
* For Next.js App Router check IP from request object
*/
export function checkRequestIP(request) {
const ip =
request.headers?.get?.("x-forwarded-for")?.split(",")[0].trim() ||
request.headers?.get?.("x-real-ip") ||
request.headers?.get?.("cf-connecting-ip") ||
request.ip ||
"unknown";
return checkIP(ip);
}
// ─── Internal Helpers ───────────────────────────────────────────────────────
function normalizeIP(ip) {
if (!ip) return "";
// Remove IPv6 prefix from IPv4-mapped addresses
return ip.replace(/^::ffff:/, "").trim();
}
function matchesAny(ip, ipSet) {
// Direct match
if (ipSet.has(ip)) return true;
// CIDR match
for (const entry of ipSet) {
if (entry.includes("/") && matchesCIDR(ip, entry)) return true;
// Wildcard match (e.g., "192.168.*.*")
if (entry.includes("*") && matchesWildcard(ip, entry)) return true;
}
return false;
}
function matchesCIDR(ip, cidr) {
try {
const [range, bits] = cidr.split("/");
const mask = parseInt(bits, 10);
if (isNaN(mask) || mask < 0 || mask > 32) return false;
const ipNum = ipToNum(ip);
const rangeNum = ipToNum(range);
if (ipNum === null || rangeNum === null) return false;
const maskBits = (-1 << (32 - mask)) >>> 0;
return (ipNum & maskBits) === (rangeNum & maskBits);
} catch {
return false;
}
}
function ipToNum(ip) {
const parts = ip.split(".");
if (parts.length !== 4) return null;
let num = 0;
for (const p of parts) {
const n = parseInt(p, 10);
if (isNaN(n) || n < 0 || n > 255) return null;
num = (num << 8) | n;
}
return num >>> 0;
}
function matchesWildcard(ip, pattern) {
const ipParts = ip.split(".");
const patParts = pattern.split(".");
if (ipParts.length !== 4 || patParts.length !== 4) return false;
return ipParts.every((part, i) => patParts[i] === "*" || part === patParts[i]);
}
function extractClientIP(req) {
return (
req.headers?.["x-forwarded-for"]?.split(",")[0].trim() ||
req.headers?.["x-real-ip"] ||
req.headers?.["cf-connecting-ip"] ||
req.socket?.remoteAddress ||
req.ip ||
"unknown"
);
}
/**
* Reset config (for testing)
*/
export function resetIPFilter() {
_config = {
enabled: false,
mode: "blacklist",
blacklist: new Set(),
whitelist: new Set(),
tempBans: new Map(),
};
}
@@ -1,4 +1,4 @@
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts";
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.js";
// Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS)
// This prevents the two maps from drifting out of sync
@@ -1,5 +1,5 @@
import { PROVIDERS } from "../config/constants.ts";
import { getRegistryEntry } from "../config/providerRegistry.ts";
import { PROVIDERS } from "../config/constants.js";
import { getRegistryEntry } from "../config/providerRegistry.js";
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
const OPENAI_COMPATIBLE_DEFAULTS = {
@@ -156,7 +156,7 @@ export function getProviderFallbackCount(provider) {
}
// Build provider URL
export function buildProviderUrl(provider, model, stream = true, options: any = {}) {
export function buildProviderUrl(provider, model, stream = true, options = {}) {
if (isOpenAICompatible(provider)) {
const apiType = getOpenAICompatibleType(provider);
const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl;
@@ -4,14 +4,10 @@
* Creates per-provider+connection limiters that auto-learn rate limits
* from API response headers (x-ratelimit-*, retry-after, anthropic-ratelimit-*).
*
* Default: ENABLED for API key providers (safety net), DISABLED for OAuth.
* Can be toggled per provider connection via dashboard.
* Default: DISABLED. Must be enabled per provider connection via dashboard toggle.
*/
import Bottleneck from "bottleneck";
import { parseRetryAfterFromBody, lockModel } from "./accountFallback.ts";
import { getProviderCategory } from "../config/providerRegistry.ts";
import { DEFAULT_API_LIMITS } from "../config/constants.ts";
// Store limiters keyed by "provider:connectionId" (and optionally ":model")
const limiters = new Map();
@@ -40,47 +36,17 @@ export async function initializeRateLimits() {
initialized = true;
try {
const { getProviderConnections } = await import("@/lib/localDb");
const { getProviderConnections } = await import("@/lib/localDb.js");
const connections = await getProviderConnections();
let explicitCount = 0;
let autoCount = 0;
let count = 0;
for (const conn of connections) {
if (conn.rateLimitProtection) {
// Explicitly enabled by user
enabledConnections.add(conn.id);
explicitCount++;
} else if (
conn.provider &&
getProviderCategory(conn.provider) === "apikey" &&
conn.isActive
) {
// Auto-enable for API key providers (safety net)
enabledConnections.add(conn.id);
autoCount++;
// Create a pre-configured limiter with conservative defaults
const key = `${conn.provider}:${conn.id}`;
if (!limiters.has(key)) {
limiters.set(
key,
new Bottleneck({
maxConcurrent: DEFAULT_API_LIMITS.concurrentRequests,
minTime: DEFAULT_API_LIMITS.minTimeBetweenRequests,
reservoir: DEFAULT_API_LIMITS.requestsPerMinute,
reservoirRefreshAmount: DEFAULT_API_LIMITS.requestsPerMinute,
reservoirRefreshInterval: 60 * 1000, // Refresh every minute
id: key,
})
);
}
count++;
}
}
if (explicitCount > 0 || autoCount > 0) {
console.log(
`🛡️ [RATE-LIMIT] Loaded ${explicitCount} explicit + ${autoCount} auto-enabled (API key) protection(s)`
);
if (count > 0) {
console.log(`🛡️ [RATE-LIMIT] Loaded ${count} connection(s) with rate limit protection`);
}
} catch (err) {
console.error("[RATE-LIMIT] Failed to load settings:", err.message);
@@ -293,7 +259,7 @@ export function updateFromHeaders(provider, connectionId, headers, status, model
// Calculate optimal minTime from RPM limit
const minTime = Math.max(0, Math.floor(60000 / limit) - 10); // Small buffer
const updates: Record<string, any> = { minTime };
const updates = { minTime };
// If remaining is low (< 10% of limit), set reservoir to throttle immediately
if (!isNaN(remaining)) {
@@ -348,7 +314,7 @@ export function getRateLimitStatus(provider, connectionId) {
* Get all active limiters status (for dashboard overview)
*/
export function getAllRateLimitStatus() {
const result: Record<string, any> = {};
const result = {};
for (const [key, limiter] of limiters) {
const counts = limiter.counts();
result[key] = {
@@ -359,38 +325,3 @@ export function getAllRateLimitStatus() {
}
return result;
}
/**
* Update rate limiter based on API response body (JSON error responses).
* Providers embed retry info in JSON payloads in different formats.
* Should be called alongside updateFromHeaders for 4xx/5xx responses.
*
* @param {string} provider - Provider ID
* @param {string} connectionId - Connection ID
* @param {string|object} responseBody - Response body (string or parsed JSON)
* @param {number} status - HTTP status code
* @param {string} model - Model name (for per-model lockouts)
*/
export function updateFromResponseBody(provider, connectionId, responseBody, status, model = null) {
if (!enabledConnections.has(connectionId)) return;
const { retryAfterMs, reason } = parseRetryAfterFromBody(responseBody);
if (retryAfterMs && retryAfterMs > 0) {
const limiter = getLimiter(provider, connectionId, null);
console.log(
`🚫 [RATE-LIMIT] ${provider}:${connectionId.slice(0, 8)} — body-parsed retry: ${Math.ceil(retryAfterMs / 1000)}s (${reason})`
);
limiter.updateSettings({
reservoir: 0,
reservoirRefreshAmount: 60,
reservoirRefreshInterval: retryAfterMs,
});
// Also apply model-level lockout if model is known
if (model) {
lockModel(provider, connectionId, model, reason, retryAfterMs);
}
}
}
@@ -117,7 +117,7 @@ export function acquire(modelStr, { maxConcurrency = 3, timeoutMs = 30000 } = {}
const idx = gate.queue.findIndex((item) => item.timer === timer);
if (idx !== -1) gate.queue.splice(idx, 1);
const err = new Error(`Semaphore timeout after ${timeoutMs}ms for ${modelStr}`);
(err as any).code = "SEMAPHORE_TIMEOUT";
err.code = "SEMAPHORE_TIMEOUT";
reject(err);
}, timeoutMs);
-169
View File
@@ -1,169 +0,0 @@
/**
* Role Normalizer Converts message roles for provider compatibility.
*
* Fixes Issues:
* 1. GLM/ZhipuAI rejects `system` role merged into first `user` message
* 2. OpenAI `developer` role not understood by non-OpenAI providers normalized to `system`
* 3. Some providers don't support `system` role at all prepended to user message
*
* Provider capability matrix is defined here rather than in the registry to
* avoid breaking changes to the existing RegistryEntry interface.
*/
// ── Provider capabilities ──────────────────────────────────────────────────
/**
* Providers that do NOT support the `system` role in messages.
* For these, system messages are merged into the first user message.
*
* Note: This applies only to OpenAI-format passthrough providers.
* Claude and Gemini have their own system message handling in dedicated translators.
*/
const PROVIDERS_WITHOUT_SYSTEM_ROLE = new Set([
// Known to reject system role (from troubleshooting report)
// GLM uses Claude format, so this is handled through claude translator
// But if accessed through OpenAI-format providers like nvidia, it needs this:
]);
/**
* Models that are known to reject the `system` role regardless of provider.
* Uses prefix matching (e.g., "glm-" matches "glm-4.7", "glm-4.5", etc.)
*/
const MODELS_WITHOUT_SYSTEM_ROLE = [
"glm-", // ZhipuAI GLM models
"ernie-", // Baidu ERNIE models
];
/**
* Check if a provider+model combo supports the system role.
*/
function supportsSystemRole(provider: string, model: string): boolean {
if (PROVIDERS_WITHOUT_SYSTEM_ROLE.has(provider)) return false;
const modelLower = (model || "").toLowerCase();
for (const prefix of MODELS_WITHOUT_SYSTEM_ROLE) {
if (modelLower.startsWith(prefix)) return false;
}
return true;
}
/**
* Normalize the `developer` role to `system` for non-OpenAI providers.
* OpenAI introduced `developer` as a replacement for `system` in newer models,
* but most other providers still expect `system`.
*
* @param messages - Array of messages
* @param targetFormat - The target format (e.g., "openai", "claude", "gemini")
* @returns Modified messages array
*/
export function normalizeDeveloperRole(messages: any[], targetFormat: string): any[] {
if (!Array.isArray(messages)) return messages;
// For OpenAI format, keep developer role as-is (it's valid)
// For all other formats, convert developer → system
if (targetFormat === "openai") return messages;
return messages.map((msg) => {
if (msg.role === "developer") {
return { ...msg, role: "system" };
}
return msg;
});
}
/**
* Convert `system` messages to user messages for providers that don't support
* the system role. The system content is prepended to the first user message
* with a clear delimiter.
*
* @param messages - Array of messages
* @param provider - Provider name
* @param model - Model name
* @returns Modified messages array
*/
export function normalizeSystemRole(messages: any[], provider: string, model: string): any[] {
if (!Array.isArray(messages) || messages.length === 0) return messages;
if (supportsSystemRole(provider, model)) return messages;
// Extract system messages
const systemMessages = messages.filter((m) => m.role === "system" || m.role === "developer");
if (systemMessages.length === 0) return messages;
// Build system content string
const systemContent = systemMessages
.map((m) => {
if (typeof m.content === "string") return m.content;
if (Array.isArray(m.content)) {
return m.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n");
}
return "";
})
.filter(Boolean)
.join("\n\n");
if (!systemContent) {
return messages.filter((m) => m.role !== "system" && m.role !== "developer");
}
// Remove system messages and merge into first user message
const nonSystemMessages = messages.filter((m) => m.role !== "system" && m.role !== "developer");
// Find first user message and prepend system content
const firstUserIdx = nonSystemMessages.findIndex((m) => m.role === "user");
if (firstUserIdx >= 0) {
const userMsg = nonSystemMessages[firstUserIdx];
const userContent =
typeof userMsg.content === "string"
? userMsg.content
: Array.isArray(userMsg.content)
? userMsg.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n")
: "";
nonSystemMessages[firstUserIdx] = {
...userMsg,
content: `[System Instructions]\n${systemContent}\n\n[User Message]\n${userContent}`,
};
} else {
// No user message found — insert as a user message at the beginning
nonSystemMessages.unshift({
role: "user",
content: `[System Instructions]\n${systemContent}`,
});
}
return nonSystemMessages;
}
/**
* Full role normalization pipeline.
* Call this before sending the request to the provider.
*
* @param messages - Array of messages
* @param provider - Provider name/id
* @param model - Model name
* @param targetFormat - Target API format
* @returns Normalized messages array
*/
export function normalizeRoles(
messages: any[],
provider: string,
model: string,
targetFormat: string
): any[] {
if (!Array.isArray(messages)) return messages;
// Step 1: Normalize developer → system (for non-OpenAI formats)
let result = normalizeDeveloperRole(messages, targetFormat);
// Step 2: Normalize system → user (for providers that don't support system role)
result = normalizeSystemRole(result, provider, model);
return result;
}
-179
View File
@@ -1,179 +0,0 @@
/**
* Session Fingerprinting Phase 5
*
* Generates stable session IDs for sticky routing,
* prompt caching, and per-session tracking.
*/
import { createHash } from "node:crypto";
// In-memory session store with metadata
// key: sessionId → { createdAt, lastActive, requestCount, connectionId? }
const sessions = new Map();
// Auto-cleanup sessions older than 30 minutes
const SESSION_TTL_MS = 30 * 60 * 1000;
const _cleanupTimer = setInterval(() => {
const now = Date.now();
for (const [key, entry] of sessions) {
if (now - entry.lastActive > SESSION_TTL_MS) sessions.delete(key);
}
}, 60_000);
_cleanupTimer.unref();
/**
* Generate a stable session fingerprint from request characteristics.
* Same client + same conversation same session ID.
*
* Fingerprint factors:
* - System prompt hash (stable per conversation/tool)
* - First user message hash (stable per conversation)
* - Model name
* - Provider (optional)
* - Tools signature (sorted tool names)
*
* @param {object} body - Request body
* @param {object} [options] - Extra context
* @returns {string} Session ID (hex hash)
*/
export function generateSessionId(body, options: any = {}) {
const parts = [];
// Model contributes to fingerprint
if (body.model) parts.push(`model:${body.model}`);
// Provider binding
if (options.provider) parts.push(`provider:${options.provider}`);
// System prompt hash (first 32 chars of system content)
const systemPrompt = extractSystemPrompt(body);
if (systemPrompt) {
parts.push(`sys:${hashShort(systemPrompt)}`);
}
// First user message hash (identifies the conversation)
const firstUser = extractFirstUserMessage(body);
if (firstUser) {
parts.push(`user0:${hashShort(firstUser)}`);
}
// Tools signature (sorted names)
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
const toolNames = body.tools
.map((t) => t.name || t.function?.name || "")
.filter(Boolean)
.sort()
.join(",");
if (toolNames) parts.push(`tools:${hashShort(toolNames)}`);
}
// Connection ID for sticky routing
if (options.connectionId) parts.push(`conn:${options.connectionId}`);
if (parts.length === 0) return null;
const fingerprint = parts.join("|");
return createHash("sha256").update(fingerprint).digest("hex").slice(0, 16);
}
/**
* Touch or create a session
*/
export function touchSession(sessionId, connectionId = null) {
if (!sessionId) return;
const existing = sessions.get(sessionId);
if (existing) {
existing.lastActive = Date.now();
existing.requestCount++;
if (connectionId) existing.connectionId = connectionId;
} else {
sessions.set(sessionId, {
createdAt: Date.now(),
lastActive: Date.now(),
requestCount: 1,
connectionId,
});
}
}
/**
* Get session info (for sticky routing decisions)
*/
export function getSessionInfo(sessionId) {
if (!sessionId) return null;
const entry = sessions.get(sessionId);
if (!entry) return null;
if (Date.now() - entry.lastActive > SESSION_TTL_MS) {
sessions.delete(sessionId);
return null;
}
return { ...entry };
}
/**
* Get the bound connection for a session (sticky routing)
*/
export function getSessionConnection(sessionId) {
const info = getSessionInfo(sessionId);
return info?.connectionId || null;
}
/**
* Get session count (for dashboard)
*/
export function getActiveSessionCount() {
return sessions.size;
}
/**
* Get all active sessions (for dashboard)
*/
export function getActiveSessions() {
const now = Date.now();
const result = [];
for (const [id, entry] of sessions) {
if (now - entry.lastActive <= SESSION_TTL_MS) {
result.push({ sessionId: id, ...entry, ageMs: now - entry.createdAt });
}
}
return result;
}
/**
* Clear all sessions (for testing)
*/
export function clearSessions() {
sessions.clear();
}
// ─── Internal Helpers ───────────────────────────────────────────────────────
function hashShort(text) {
return createHash("sha256").update(text).digest("hex").slice(0, 8);
}
function extractSystemPrompt(body) {
// Claude format: body.system
if (body.system) {
return typeof body.system === "string" ? body.system : JSON.stringify(body.system);
}
// OpenAI format: messages[0].role === "system"
if (body.messages && Array.isArray(body.messages)) {
const sys = body.messages.find((m) => m.role === "system" || m.role === "developer");
if (sys) {
return typeof sys.content === "string" ? sys.content : JSON.stringify(sys.content);
}
}
return null;
}
function extractFirstUserMessage(body) {
const messages = body.messages || body.input || [];
if (!Array.isArray(messages)) return null;
for (const msg of messages) {
if (msg.role === "user") {
return typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
}
}
return null;
}
-171
View File
@@ -1,171 +0,0 @@
/**
* Signature Cache Phase 3
*
* Dynamic 3-layer cache for thinking signatures (tool, model family, session).
* Replaces hardcoded thinking signature patterns with adaptive detection.
*/
// 3-layer cache: tool → model family → session
// Each layer stores patterns detected from responses
const layers = {
tool: new Map(), // e.g. "cursor" → Set of signature patterns
family: new Map(), // e.g. "claude-sonnet" → Set of signature patterns
session: new Map(), // e.g. sessionId → Set of signature patterns
};
// Known default signatures (bootstrap — will be supplemented by learning)
const DEFAULT_SIGNATURES = [
"<antThinking>",
"</antThinking>",
"<thinking>",
"</thinking>",
"<internal_thought>",
"</internal_thought>",
];
// Max entries per cache layer to prevent unbounded growth
const MAX_ENTRIES_PER_LAYER = 500;
const MAX_PATTERNS_PER_KEY = 50;
/**
* Get all matching signatures for a given context.
* Checks all 3 layers and merges results with defaults.
*
* @param {object} context - { tool?, modelFamily?, sessionId? }
* @returns {string[]} Array of unique signature patterns
*/
export function getSignatures(context: any = {}) {
const patterns = new Set(DEFAULT_SIGNATURES);
// Layer 1: Tool (e.g., "cursor", "cline", "antigravity")
if (context.tool && layers.tool.has(context.tool)) {
for (const p of layers.tool.get(context.tool)) patterns.add(p);
}
// Layer 2: Model family (e.g., "claude-sonnet", "claude-opus")
if (context.modelFamily && layers.family.has(context.modelFamily)) {
for (const p of layers.family.get(context.modelFamily)) patterns.add(p);
}
// Layer 3: Session-specific
if (context.sessionId && layers.session.has(context.sessionId)) {
for (const p of layers.session.get(context.sessionId)) patterns.add(p);
}
return Array.from(patterns);
}
/**
* Add a discovered signature pattern to the cache.
*
* @param {string} pattern - The signature pattern (e.g., "<antThinking>")
* @param {object} context - { tool?, modelFamily?, sessionId? }
*/
export function addSignature(pattern: any, context: any = {}) {
if (!pattern || typeof pattern !== "string") return;
const addToLayer = (layer, key) => {
if (!key) return;
if (!layer.has(key)) {
if (layer.size >= MAX_ENTRIES_PER_LAYER) {
// Evict oldest entry
const firstKey = layer.keys().next().value;
layer.delete(firstKey);
}
layer.set(key, new Set());
}
const set = layer.get(key);
if (set.size < MAX_PATTERNS_PER_KEY) {
set.add(pattern);
}
};
addToLayer(layers.tool, context.tool);
addToLayer(layers.family, context.modelFamily);
addToLayer(layers.session, context.sessionId);
}
/**
* Detect signatures in a text chunk from streaming response.
* Auto-learns new patterns and adds them to cache.
*
* @param {string} text - Streaming text to scan
* @param {object} context - { tool?, modelFamily?, sessionId? }
* @returns {{ found: string[], cleaned: string }} Detected tags and cleaned text
*/
export function detectAndLearn(text: any, context: any = {}) {
if (!text || typeof text !== "string") return { found: [], cleaned: text };
const found = [];
let cleaned = text;
// Check all known signatures
const known = getSignatures(context);
for (const sig of known) {
if (cleaned.includes(sig)) {
found.push(sig);
cleaned = cleaned.split(sig).join("");
}
}
// Auto-detect new XML-like thinking tags
const tagRegex = /<\/?([a-zA-Z_][a-zA-Z0-9_]*(?:Thinking|thinking|thought|Thought|internal_thought))>/g;
let match;
while ((match = tagRegex.exec(text)) !== null) {
const tag = match[0];
if (!known.includes(tag)) {
found.push(tag);
addSignature(tag, context);
cleaned = cleaned.split(tag).join("");
}
}
return { found, cleaned: cleaned.trim() || cleaned };
}
/**
* Extract model family from model name
* "claude-sonnet-4-20250514" "claude-sonnet"
* "gpt-4o-2024-08-06" "gpt-4o"
*/
export function getModelFamily(model) {
if (!model) return null;
// Remove date suffixes and version numbers
const cleaned = model
.replace(/-\d{4}-\d{2}-\d{2}$/, "") // Remove YYYY-MM-DD suffix
.replace(/-\d{8,}$/, "") // Remove YYYYMMDD suffix
.replace(/-\d+(\.\d+)*$/, "") // Remove version suffix like -4
.replace(/@.*$/, ""); // Remove @latest etc.
// Keep meaningful prefix
return cleaned || model;
}
/**
* Get cache stats (for dashboard)
*/
export function getCacheStats() {
return {
tool: {
entries: layers.tool.size,
patterns: Array.from(layers.tool.values()).reduce((sum, s) => sum + s.size, 0),
},
family: {
entries: layers.family.size,
patterns: Array.from(layers.family.values()).reduce((sum, s) => sum + s.size, 0),
},
session: {
entries: layers.session.size,
patterns: Array.from(layers.session.values()).reduce((sum, s) => sum + s.size, 0),
},
defaultCount: DEFAULT_SIGNATURES.length,
};
}
/**
* Clear all cache layers (for testing)
*/
export function clearCache() {
layers.tool.clear();
layers.family.clear();
layers.session.clear();
}
-66
View File
@@ -1,66 +0,0 @@
/**
* System Prompt Injection Phase 10
*
* Injects a global system prompt into all requests at proxy level.
*/
// In-memory config
let _config = {
enabled: false,
prompt: "",
};
/**
* Set system prompt config
*/
export function setSystemPromptConfig(config) {
_config = { ..._config, ...config };
}
/**
* Get system prompt config
*/
export function getSystemPromptConfig() {
return { ..._config };
}
/**
* Inject system prompt into request body.
*
* @param {object} body - Request body
* @param {string} [promptText] - Override prompt text
* @returns {object} Modified body
*/
export function injectSystemPrompt(body, promptText = null) {
const text = promptText || _config.prompt;
if (!text || !_config.enabled) return body;
if (!body || typeof body !== "object") return body;
if (body._skipSystemPrompt) return body;
const result = { ...body };
// OpenAI/Claude format (messages[])
if (result.messages && Array.isArray(result.messages)) {
const sysIdx = result.messages.findIndex((m) => m.role === "system" || m.role === "developer");
result.messages = [...result.messages];
if (sysIdx >= 0) {
// Prepend to existing system message
const msg = { ...result.messages[sysIdx] };
msg.content = text + "\n\n" + (msg.content || "");
result.messages[sysIdx] = msg;
} else {
result.messages = [{ role: "system", content: text }, ...result.messages];
}
}
// Claude format (system field)
if (result.system !== undefined) {
if (typeof result.system === "string") {
result.system = text + "\n\n" + result.system;
} else if (Array.isArray(result.system)) {
result.system = [{ type: "text", text }, ...result.system];
}
}
return result;
}

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