Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc077bc309 | |||
| 0fd634ef43 | |||
| d352b6b509 | |||
| fcc48cc738 | |||
| ec06a345cc | |||
| 7690b364e7 | |||
| b94c0c7d04 | |||
| 500bfdf588 | |||
| d23b19c466 | |||
| 3a5450039d | |||
| b582ddf090 | |||
| ef8b470e8b | |||
| 5a0841a994 | |||
| bd462c4e0b | |||
| f11ec4e142 | |||
| bf76da3222 | |||
| f171b7de96 | |||
| c0cbf00199 | |||
| 0cd6e59fb9 | |||
| 11a8adc71c | |||
| b9c7fd879f | |||
| 2fc4c7ea33 | |||
| c5003665c3 | |||
| 538028c150 | |||
| fb8d187f8d | |||
| 1a11301e1a | |||
| 4c6cdd5c23 | |||
| 30a64b0dd3 | |||
| 04de492019 | |||
| 07890df6cb | |||
| 2f23cfdf1c | |||
| 1832946d41 | |||
| 6ec8745d2e | |||
| b6bbfe063b | |||
| 48182edbd5 | |||
| 94a00cb6d6 | |||
| fc24361aa6 | |||
| cec833afc6 | |||
| f1cddba938 | |||
| a0acdfdcb9 | |||
| 6637f294df | |||
| ad8a444105 | |||
| 877cfa0071 | |||
| e6f0a780b7 | |||
| dd9de2efa9 | |||
| f6b0811f78 | |||
| eba9d854a9 | |||
| 437cf9bab0 | |||
| b84c915b23 |
@@ -40,6 +40,7 @@ MACHINE_ID_SALT=endpoint-proxy-salt
|
||||
ENABLE_REQUEST_LOGS=false
|
||||
AUTH_COOKIE_SECURE=false
|
||||
REQUIRE_API_KEY=false
|
||||
ALLOW_API_KEY_REVEAL=false
|
||||
|
||||
# Input Sanitizer (FASE-01 — prompt injection & PII protection)
|
||||
# INPUT_SANITIZER_ENABLED=true
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
name: Publish to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
@@ -12,6 +15,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
@@ -37,6 +41,13 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from release tag or input
|
||||
id: version
|
||||
run: |
|
||||
@@ -59,6 +70,8 @@ jobs:
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
ghcr.io/diegosouzapw/omniroute:${{ steps.version.outputs.version }}
|
||||
ghcr.io/diegosouzapw/omniroute:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
no-cache: false
|
||||
|
||||
@@ -35,6 +35,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -105,3 +106,21 @@ jobs:
|
||||
echo "✅ Published omniroute@$VERSION (tag: $TAG)"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: |
|
||||
VERSION="${{ steps.resolve.outputs.version }}"
|
||||
TAG="${{ steps.resolve.outputs.tag }}"
|
||||
|
||||
echo "Configuring for GitHub Packages..."
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc
|
||||
npm pkg set name="@diegosouzapw/omniroute"
|
||||
|
||||
if [ "$TAG" = "latest" ]; then
|
||||
npm publish --registry=https://npm.pkg.github.com || echo "⚠️ Version ${VERSION} might already be published on GitHub."
|
||||
else
|
||||
npm publish --registry=https://npm.pkg.github.com --tag "$TAG" || echo "⚠️ Version ${VERSION} might already be published on GitHub."
|
||||
fi
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -47,3 +47,12 @@ AGENTS.md
|
||||
# Build artifacts (pre-built goes inside app/)
|
||||
.next/
|
||||
node_modules/
|
||||
|
||||
# Ignore large binary files and other build directories
|
||||
*.tgz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.rpm
|
||||
electron/
|
||||
app/electron/
|
||||
app/vscode-extension/
|
||||
|
||||
+113
-6
@@ -4,6 +4,109 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.2.6] — 2026-03-29
|
||||
|
||||
### ✨ Enhancements & Refactoring
|
||||
|
||||
- **API Key Reveal (#740)** — Added a scoped API key copy flow in the Api Manager, protected by the `ALLOW_API_KEY_REVEAL` environment variable.
|
||||
- **Sidebar Visibility Controls (#739)** — Admins can now hide any sidebar navigation link via the Appearance settings to reduce visual clutter.
|
||||
- **Strict Combo Testing (#735)** — Hardened the combo health check endpoint to require live text responses from models instead of just soft reachability signals.
|
||||
- **Streamed Detailed Logs (#734)** — Switched detailed request logging for SSE streams to reconstruct the final payload, saving immense amounts of SQLite database size and significantly cleaning up the UI.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **OpenCode Go MiniMax Auth (#733)** — Corrected the authentication header logic for `minimax` models on OpenCode Go to use `x-api-key` instead of standard bearer tokens across the `/messages` protocol.
|
||||
|
||||
---
|
||||
|
||||
## [3.2.5] — 2026-03-29
|
||||
|
||||
### ✨ Enhancements & Refactoring
|
||||
|
||||
- **Void Linux Deployment Support (#732)** — Integrated `xbps-src` packaging template and instructions to natively compile and install OmniRoute with `better-sqlite3` bindings via cross-compilation target.
|
||||
|
||||
## [3.2.4] — 2026-03-29
|
||||
|
||||
### ✨ Enhancements & Refactoring
|
||||
|
||||
- **Qoder AI Migration (#660)** — Completely migrated the legacy `iFlow` core provider onto `Qoder AI` maintaining stable API routing capabilities.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Gemini Tools HTTP 400 Payload Invalid Argument (#731)** — Prevented `thoughtSignature` array injections inside standard Gemini `functionCall` sequences blocking agentic routing flows.
|
||||
|
||||
---
|
||||
|
||||
## [3.2.3] — 2026-03-29
|
||||
|
||||
### ✨ Enhancements & Refactoring
|
||||
|
||||
- **Provider Limits Quota UI (#728)** — Normalized quota limit logic and data labeling inside the Limits interface.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Core Routing Schemas & Leaks** — Expanded `comboStrategySchema` to natively support `fill-first` and `p2c` strategies to unblock complex combo editing natively.
|
||||
- **Thinking Tags Extraction (CLI)** — Restructured CLI token responses sanitizer RegEx capturing model reasoning structures inside streams avoiding broken `<thinking>` extractions breaking response text output format.
|
||||
- **Strict Format Enforcements** — Hardened pipeline sanitization execution making it universally apply to translation mode targets.
|
||||
|
||||
---
|
||||
|
||||
## [3.2.2] — 2026-03-29
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Four-Stage Request Log Pipeline (#705)** — Refactored log persistence to save comprehensive payloads at four distinct pipeline stages: Client Request, Translated Provider Request, Provider Response, and Translated Client Response. Introduced `streamPayloadCollector` for robust SSE stream truncation and payload serialization.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Mobile UI Fixes (#659)** — Prevented table components on the dashboard from breaking the layout on narrow viewports by adding proper horizontal scrolling and overflow containment to `DashboardLayout`.
|
||||
- **Claude Prompt Cache Fixes (#708)** — Ensured `cache_control` blocks in Claude-to-Claude fallback loops are faithfully preserved and passed safely back to Anthropic models.
|
||||
- **Gemini Tool Definitions (#725)** — Fixed schema translation errors when declaring simple `object` parameter types for Gemini function calling.
|
||||
|
||||
## [3.2.1] — 2026-03-29
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Global Fallback Provider (#689)** — When all combo models are exhausted (502/503), OmniRoute now attempts a configurable global fallback model before returning the error. Set `globalFallbackModel` in settings to enable.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix #721** — Fixed context pinning bypass during tool-call responses. Non-streaming tagging used wrong JSON path (`json.messages` → `json.choices[0].message`). Streaming injection now triggers on `finish_reason` chunks for tool-call-only streams. `injectModelTag()` now appends synthetic pin messages for non-string content.
|
||||
- **Fix #709** — Confirmed already fixed (v3.1.9) — `system-info.mjs` creates directories recursively. Closed.
|
||||
- **Fix #707** — Confirmed already fixed (v3.1.9) — empty tool name sanitization in `chatCore.ts`. Closed.
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- Added 6 unit tests for context pinning with tool-call responses (null content, array content, roundtrip, re-injection)
|
||||
|
||||
## [3.2.0] — 2026-03-28
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Cache Management UI** — Added a dedicated semantic caching dashboard at \`/dashboard/cache\` with targeted API invalidation and 31-language i18n support (PR #701 by @oyi77)
|
||||
- **GLM Quota Tracking** — Added real-time usage and session quota tracking for the GLM Coding (Z.AI) provider (PR #698 by @christopher-s)
|
||||
- **Detailed Log Payloads** — Wired full four-stage pipeline payload capturing (original, translated, provider-response, streamed-deltas) directly into the UI (PR #705 by @rdself)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix #708** — Prevented token bleeding for Claude Code users routing through OmniRoute by correctly preserving native \`cache_control\` headers during Claude-to-Claude passthrough (PR #708 by @tombii)
|
||||
- **Fix #719** — Setup internal auth boundaries for \`ModelSyncScheduler\` to prevent unauthenticated daemon failures on startup (PR #719 by @rdself)
|
||||
- **Fix #718** — Rebuilt badge rendering in Provider Limits UI preventing bad quota boundaries overlap (PR #718 by @rdself)
|
||||
- **Fix #704** — Fixed Combo Fallbacks breaking on HTTP 400 content-policy errors preventing model-rotation dead-routing (PR #704 by @rdself)
|
||||
|
||||
### 🔒 Security & Dependencies
|
||||
|
||||
- Bumped \`path-to-regexp\` to \`8.4.0\` resolving dependabot vulnerabilities (PR #715)
|
||||
|
||||
## [3.1.10] — 2026-03-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix #706** — Fixed icon fallback rendering caused by Tailwind V4 `font-sans` override by applying `!important` to `.material-symbols-outlined`.
|
||||
- **Fix #703** — Fixed GitHub Copilot broken streams by enabling `responses` to `openai` format translation for any custom models leveraging `apiFormat: "responses"`.
|
||||
- **Fix #702** — Replaced flat-rate usage tracking with accurate DB pricing calculations for both streaming and non-streaming responses.
|
||||
- **Fix #716** — Cleaned up Claude tool-call translation state, correctly parsing streaming arguments and preventing OpenAI `tool_calls` chunks from repeating the `id` field.
|
||||
|
||||
## [3.1.9] — 2026-03-28
|
||||
|
||||
### ✨ New Features
|
||||
@@ -31,12 +134,16 @@
|
||||
|
||||
### 📁 New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `open-sse/translator/helpers/schemaCoercion.ts` | Schema coercion and tool description sanitization utilities |
|
||||
| `tests/unit/schema-coercion.test.mjs` | Unit tests for schema coercion |
|
||||
| `tests/unit/t40-opencode-cli-tools-integration.test.mjs` | CLI tool integration tests |
|
||||
| `COVERAGE_PLAN.md` | Test coverage planning document |
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `open-sse/translator/helpers/schemaCoercion.ts` | Schema coercion and tool description sanitization utilities |
|
||||
| `tests/unit/schema-coercion.test.mjs` | Unit tests for schema coercion |
|
||||
| `tests/unit/t40-opencode-cli-tools-integration.test.mjs` | CLI tool integration tests |
|
||||
| `COVERAGE_PLAN.md` | Test coverage planning document |
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Claude Prompt Caching Passthrough** — Fixed cache_control markers being stripped in Claude passthrough mode (Claude → OmniRoute → Claude), which caused Claude Code users to deplete their Anthropic API quota 5-10x faster than direct connections. OmniRoute now preserves client's cache_control markers when sourceFormat and targetFormat are both Claude, ensuring prompt caching works correctly and dramatically reducing token consumption.
|
||||
|
||||
## [3.1.8] - 2026-03-27
|
||||
|
||||
|
||||
+12
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/ARCHITECTURE.md) | 🇪🇸 [Español](i18n/es/ARCHITECTURE.md) | 🇫🇷 [Français](i18n/fr/ARCHITECTURE.md) | 🇮🇹 [Italiano](i18n/it/ARCHITECTURE.md) | 🇷🇺 [Русский](i18n/ru/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/ARCHITECTURE.md) | 🇩🇪 [Deutsch](i18n/de/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](i18n/in/ARCHITECTURE.md) | 🇹🇭 [ไทย](i18n/th/ARCHITECTURE.md) | 🇺🇦 [Українська](i18n/uk-UA/ARCHITECTURE.md) | 🇸🇦 [العربية](i18n/ar/ARCHITECTURE.md) | 🇯🇵 [日本語](i18n/ja/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/ARCHITECTURE.md) | 🇧🇬 [Български](i18n/bg/ARCHITECTURE.md) | 🇩🇰 [Dansk](i18n/da/ARCHITECTURE.md) | 🇫🇮 [Suomi](i18n/fi/ARCHITECTURE.md) | 🇮🇱 [עברית](i18n/he/ARCHITECTURE.md) | 🇭🇺 [Magyar](i18n/hu/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/ARCHITECTURE.md) | 🇰🇷 [한국어](i18n/ko/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/ARCHITECTURE.md) | 🇳🇱 [Nederlands](i18n/nl/ARCHITECTURE.md) | 🇳🇴 [Norsk](i18n/no/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/ARCHITECTURE.md) | 🇷🇴 [Română](i18n/ro/ARCHITECTURE.md) | 🇵🇱 [Polski](i18n/pl/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](i18n/sk/ARCHITECTURE.md) | 🇸🇪 [Svenska](i18n/sv/ARCHITECTURE.md) | 🇵🇭 [Filipino](i18n/phi/ARCHITECTURE.md) | 🇨🇿 [Čeština](i18n/cs/ARCHITECTURE.md)
|
||||
|
||||
_Last updated: 2026-03-24_
|
||||
_Last updated: 2026-03-28_
|
||||
|
||||
## Executive Summary
|
||||
|
||||
@@ -274,8 +274,9 @@ Domain State DB (SQLite):
|
||||
|
||||
## 5) Cloud Sync
|
||||
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`, `src/shared/services/modelSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/cloudSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/modelSyncScheduler.ts`
|
||||
- Control route: `src/app/api/sync/cloud/route.ts`
|
||||
|
||||
## Request Lifecycle (`/v1/chat/completions`)
|
||||
@@ -355,7 +356,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.ts` using status codes and error-message heuristics. Combo routing adds one extra guard: provider-scoped 400s such as upstream content-block and role-validation failures are treated as model-local failures so later combo targets can still run.
|
||||
|
||||
## OAuth Onboarding and Token Refresh Lifecycle
|
||||
|
||||
@@ -755,10 +756,18 @@ Runtime visibility sources:
|
||||
|
||||
- console logs from `src/sse/utils/logger.ts`
|
||||
- per-request usage aggregates in SQLite (`usage_history`, `call_logs`, `proxy_logs`)
|
||||
- four-stage detailed payload captures in SQLite (`request_detail_logs`) when `settings.detailed_logs_enabled=true`
|
||||
- textual request status log in `log.txt` (optional/compat)
|
||||
- optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true`
|
||||
- dashboard usage endpoints (`/api/usage/*`) for UI consumption
|
||||
|
||||
Detailed request payload capture stores up to four JSON payload stages per routed call:
|
||||
|
||||
- raw request received from the client
|
||||
- translated request actually sent upstream
|
||||
- provider response reconstructed as JSON; streamed responses are compacted to the final summary plus stream metadata
|
||||
- final client response returned by OmniRoute; streamed responses are stored in the same compact summary form
|
||||
|
||||
## Security-Sensitive Boundaries
|
||||
|
||||
- JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing
|
||||
|
||||
+103
-1
@@ -405,6 +405,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -419,6 +519,7 @@ For host-integrated mode with CLI binaries, see the Docker section in the main d
|
||||
| `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/*` |
|
||||
| `ALLOW_API_KEY_REVEAL` | `false` | Allow Api Manager to copy full API keys on demand |
|
||||
| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs |
|
||||
| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (behind HTTPS reverse proxy) |
|
||||
| `OMNIROUTE_MEMORY_MB` | `512` | Node.js heap limit in MB |
|
||||
@@ -683,10 +784,11 @@ curl -X POST http://localhost:20128/api/db-backups/import \
|
||||
|
||||
### Settings Dashboard
|
||||
|
||||
The settings page is organized into 5 tabs for easy navigation:
|
||||
The settings page is organized into 6 tabs for easy navigation:
|
||||
|
||||
| Tab | Contents |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| **General** | System storage tools, appearance settings, theme controls, and per-item sidebar visibility |
|
||||
| **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 |
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (العربية)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### تثبيت
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Български)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Инсталиране
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Dansk)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installer
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Deutsch)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installieren
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Español)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Instalar
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Suomi)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Asenna
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Français)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installer
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (עברית)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### התקנה
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Magyar)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Telepítés
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Bahasa Indonesia)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Instal
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (हिन्दी)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### स्थापित करें
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Italiano)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installare
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (日本語)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### インストール
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (한국어)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### 설치
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Bahasa Melayu)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Pasang
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Nederlands)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installeren
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Norsk)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installer
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Filipino)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### I-install
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Polski)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Zainstaluj
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Usuários do Void Linux podem empacotar e instalar o OmniRoute nativamente usando o framework de compilação cruzada `xbps-src`. Isso automatiza a compilação do bundle standalone do Node.js juntamente com os bindings nativos necessários do `better-sqlite3`.
|
||||
|
||||
<details>
|
||||
<summary><b>Ver template do xbps-src</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Português (Portugal))
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Instalar
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Română)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Instalare
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Русский)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Установить
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Slovenčina)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Inštalácia
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Svenska)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Installera
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (ไทย)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### ติดตั้ง
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Українська)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Встановити
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+122
-6
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (Tiếng Việt)
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### Cài đặt
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
🌐 **Languages:** 🇺🇸 [English](../../README.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
# User Guide (中文(简体))
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](../../USER_GUIDE.md) · 🇧🇷 [pt-BR](../pt-BR/USER_GUIDE.md) · 🇪🇸 [es](../es/USER_GUIDE.md) · 🇫🇷 [fr](../fr/USER_GUIDE.md) · 🇩🇪 [de](../de/USER_GUIDE.md) · 🇮🇹 [it](../it/USER_GUIDE.md) · 🇷🇺 [ru](../ru/USER_GUIDE.md) · 🇨🇳 [zh-CN](../zh-CN/USER_GUIDE.md) · 🇯🇵 [ja](../ja/USER_GUIDE.md) · 🇰🇷 [ko](../ko/USER_GUIDE.md) · 🇸🇦 [ar](../ar/USER_GUIDE.md) · 🇮🇳 [in](../in/USER_GUIDE.md) · 🇹🇭 [th](../th/USER_GUIDE.md) · 🇻🇳 [vi](../vi/USER_GUIDE.md) · 🇮🇩 [id](../id/USER_GUIDE.md) · 🇲🇾 [ms](../ms/USER_GUIDE.md) · 🇳🇱 [nl](../nl/USER_GUIDE.md) · 🇵🇱 [pl](../pl/USER_GUIDE.md) · 🇸🇪 [sv](../sv/USER_GUIDE.md) · 🇳🇴 [no](../no/USER_GUIDE.md) · 🇩🇰 [da](../da/USER_GUIDE.md) · 🇫🇮 [fi](../fi/USER_GUIDE.md) · 🇵🇹 [pt](../pt/USER_GUIDE.md) · 🇷🇴 [ro](../ro/USER_GUIDE.md) · 🇭🇺 [hu](../hu/USER_GUIDE.md) · 🇧🇬 [bg](../bg/USER_GUIDE.md) · 🇸🇰 [sk](../sk/USER_GUIDE.md) · 🇺🇦 [uk-UA](../uk-UA/USER_GUIDE.md) · 🇮🇱 [he](../he/USER_GUIDE.md) · 🇵🇭 [phi](../phi/USER_GUIDE.md)
|
||||
|
||||
> 🇺🇸 [English](../../USER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](USER_GUIDE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/USER_GUIDE.md) | 🇪🇸 [Español](i18n/es/USER_GUIDE.md) | 🇫🇷 [Français](i18n/fr/USER_GUIDE.md) | 🇮🇹 [Italiano](i18n/it/USER_GUIDE.md) | 🇷🇺 [Русский](i18n/ru/USER_GUIDE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/USER_GUIDE.md) | 🇩🇪 [Deutsch](i18n/de/USER_GUIDE.md) | 🇮🇳 [हिन्दी](i18n/in/USER_GUIDE.md) | 🇹🇭 [ไทย](i18n/th/USER_GUIDE.md) | 🇺🇦 [Українська](i18n/uk-UA/USER_GUIDE.md) | 🇸🇦 [العربية](i18n/ar/USER_GUIDE.md) | 🇯🇵 [日本語](i18n/ja/USER_GUIDE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/USER_GUIDE.md) | 🇧🇬 [Български](i18n/bg/USER_GUIDE.md) | 🇩🇰 [Dansk](i18n/da/USER_GUIDE.md) | 🇫🇮 [Suomi](i18n/fi/USER_GUIDE.md) | 🇮🇱 [עברית](i18n/he/USER_GUIDE.md) | 🇭🇺 [Magyar](i18n/hu/USER_GUIDE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/USER_GUIDE.md) | 🇰🇷 [한국어](i18n/ko/USER_GUIDE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/USER_GUIDE.md) | 🇳🇱 [Nederlands](i18n/nl/USER_GUIDE.md) | 🇳🇴 [Norsk](i18n/no/USER_GUIDE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/USER_GUIDE.md) | 🇷🇴 [Română](i18n/ro/USER_GUIDE.md) | 🇵🇱 [Polski](i18n/pl/USER_GUIDE.md) | 🇸🇰 [Slovenčina](i18n/sk/USER_GUIDE.md) | 🇸🇪 [Svenska](i18n/sv/USER_GUIDE.md) | 🇵🇭 [Filipino](i18n/phi/USER_GUIDE.md)
|
||||
|
||||
Complete guide for configuring providers, creating combos, integrating CLI tools, and deploying OmniRoute.
|
||||
|
||||
---
|
||||
@@ -409,6 +409,106 @@ docker run -d --name omniroute -p 20128:20128 --env-file ./.env -v omniroute-dat
|
||||
|
||||
For host-integrated mode with CLI binaries, see the Docker section in the main docs.
|
||||
|
||||
### Void Linux (xbps-src)
|
||||
|
||||
Void Linux users can package and install OmniRoute natively using the `xbps-src` cross-compilation framework. This automates the Node.js standalone build along with the required `better-sqlite3` native bindings.
|
||||
|
||||
<details>
|
||||
<summary><b>View xbps-src template</b></summary>
|
||||
|
||||
```bash
|
||||
# Template file for 'omniroute'
|
||||
pkgname=omniroute
|
||||
version=3.2.4
|
||||
revision=1
|
||||
hostmakedepends="nodejs python3 make"
|
||||
depends="openssl"
|
||||
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
|
||||
maintainer="zenobit <zenobit@disroot.org>"
|
||||
license="MIT"
|
||||
homepage="https://github.com/diegosouzapw/OmniRoute"
|
||||
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
|
||||
system_accounts="_omniroute"
|
||||
omniroute_homedir="/var/lib/omniroute"
|
||||
export NODE_ENV=production
|
||||
export npm_config_engine_strict=false
|
||||
export npm_config_loglevel=error
|
||||
export npm_config_fund=false
|
||||
export npm_config_audit=false
|
||||
|
||||
do_build() {
|
||||
# Determine target CPU arch for node-gyp
|
||||
local _gyp_arch
|
||||
case "$XBPS_TARGET_MACHINE" in
|
||||
aarch64*) _gyp_arch=arm64 ;;
|
||||
armv7*|armv6*) _gyp_arch=arm ;;
|
||||
i686*) _gyp_arch=ia32 ;;
|
||||
*) _gyp_arch=x64 ;;
|
||||
esac
|
||||
|
||||
# 1) Install all deps – skip scripts
|
||||
NODE_ENV=development npm ci --ignore-scripts
|
||||
|
||||
# 2) Build the Next.js standalone bundle
|
||||
npm run build
|
||||
|
||||
# 3) Copy static assets into standalone
|
||||
cp -r .next/static .next/standalone/.next/static
|
||||
[ -d public ] && cp -r public .next/standalone/public || true
|
||||
|
||||
# 4) Compile better-sqlite3 native binding
|
||||
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
|
||||
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
|
||||
|
||||
# 5) Place the compiled binding into the standalone bundle
|
||||
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
|
||||
mkdir -p "$_bs3_release"
|
||||
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
|
||||
|
||||
# 6) Remove arch-specific sharp bundles
|
||||
rm -rf .next/standalone/node_modules/@img
|
||||
|
||||
# 7) Copy pino runtime deps omitted by Next.js static analysis:
|
||||
for _mod in pino-abstract-transport split2 process-warning; do
|
||||
cp -r "node_modules/$_mod" .next/standalone/node_modules/
|
||||
done
|
||||
}
|
||||
|
||||
do_check() {
|
||||
npm run test:unit
|
||||
}
|
||||
|
||||
do_install() {
|
||||
vmkdir usr/lib/omniroute/.next
|
||||
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
|
||||
|
||||
# Prevent removal of empty Next.js app router dirs by the post-install hook
|
||||
for _d in \
|
||||
.next/standalone/.next/server/app/dashboard \
|
||||
.next/standalone/.next/server/app/dashboard/settings \
|
||||
.next/standalone/.next/server/app/dashboard/providers; do
|
||||
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
|
||||
done
|
||||
|
||||
cat > "${WRKDIR}/omniroute" <<'EOF'
|
||||
#!/bin/sh
|
||||
export PORT="${PORT:-20128}"
|
||||
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
|
||||
export LOG_TO_FILE="${LOG_TO_FILE:-false}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
|
||||
EOF
|
||||
vbin "${WRKDIR}/omniroute"
|
||||
}
|
||||
|
||||
post_install() {
|
||||
vlicense LICENSE
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
@@ -582,6 +682,22 @@ Configure via **Dashboard → Settings → Routing**.
|
||||
| **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 |
|
||||
|
||||
#### External Sticky Session Header
|
||||
|
||||
For external session affinity (for example, Claude Code/Codex agents behind reverse proxies), send:
|
||||
|
||||
```http
|
||||
X-Session-Id: your-session-key
|
||||
```
|
||||
|
||||
OmniRoute also accepts `x_session_id` and returns the effective session key in `X-OmniRoute-Session-Id`.
|
||||
|
||||
If you use Nginx and send underscore-form headers, enable:
|
||||
|
||||
```nginx
|
||||
underscores_in_headers on;
|
||||
```
|
||||
|
||||
#### Wildcard Model Aliases
|
||||
|
||||
Create wildcard patterns to remap model names:
|
||||
@@ -766,7 +882,7 @@ Access via **Dashboard → Health**. Real-time system health overview with 6 car
|
||||
|
||||
OmniRoute is available as a native desktop application for Windows, macOS, and Linux.
|
||||
|
||||
### Installation
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# From the electron directory:
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 3.1.9
|
||||
version: 3.2.6
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -45,7 +45,11 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
const key = credentials?.apiKey || credentials?.accessToken;
|
||||
|
||||
if (key) {
|
||||
headers["Authorization"] = `Bearer ${key}`;
|
||||
if (this._requestFormat === "claude") {
|
||||
headers["x-api-key"] = key;
|
||||
} else {
|
||||
headers["Authorization"] = `Bearer ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._requestFormat === "claude") {
|
||||
|
||||
+245
-124
@@ -14,10 +14,16 @@ import { createRequestLogger } from "../utils/requestLogger.ts";
|
||||
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
|
||||
import { resolveModelAlias } from "../services/modelDeprecation.ts";
|
||||
import { getUnsupportedParams } from "../config/providerRegistry.ts";
|
||||
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.ts";
|
||||
import {
|
||||
buildErrorBody,
|
||||
createErrorResult,
|
||||
parseUpstreamError,
|
||||
formatProviderError,
|
||||
} from "../utils/error.ts";
|
||||
import { HTTP_STATUS, PROVIDER_MAX_TOKENS } from "../config/constants.ts";
|
||||
import { classifyProviderError, PROVIDER_ERROR_TYPES } from "../services/errorClassifier.ts";
|
||||
import { updateProviderConnection } from "@/lib/db/providers";
|
||||
import { isDetailedLoggingEnabled, saveRequestDetailLog } from "@/lib/db/detailedLogs";
|
||||
import { logAuditEvent } from "@/lib/compliance";
|
||||
import { handleBypassRequest } from "../utils/bypassHandler.ts";
|
||||
import {
|
||||
@@ -27,6 +33,8 @@ import {
|
||||
saveCallLog,
|
||||
} from "@/lib/usageDb";
|
||||
import { getLoggedInputTokens, getLoggedOutputTokens } from "@/lib/usage/tokenAccounting";
|
||||
import { recordCost } from "@/domain/costRules";
|
||||
import { calculateCost } from "@/lib/usage/costCalculator";
|
||||
import { CLAUDE_OAUTH_TOOL_PREFIX } from "../translator/request/openai-to-claude.ts";
|
||||
import {
|
||||
getModelNormalizeToolCallId,
|
||||
@@ -70,6 +78,8 @@ import {
|
||||
EMERGENCY_FALLBACK_CONFIG,
|
||||
} from "../services/emergencyFallback.ts";
|
||||
import { resolveStreamFlag, stripMarkdownCodeFence } from "../utils/aiSdkCompat.ts";
|
||||
import { generateRequestId } from "@/shared/utils/requestId";
|
||||
import { normalizePayloadForLog } from "@/lib/logPayloads";
|
||||
|
||||
export function shouldUseNativeCodexPassthrough({
|
||||
provider,
|
||||
@@ -389,7 +399,8 @@ export async function handleChatCore({
|
||||
|
||||
credentials.providerSpecificData = nextProviderData;
|
||||
} catch (err) {
|
||||
log?.debug?.("CODEX", `Failed to persist codex quota state: ${err?.message || err}`);
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -484,6 +495,88 @@ export async function handleChatCore({
|
||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
|
||||
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
||||
const noLogEnabled = apiKeyInfo?.noLog === true;
|
||||
const detailedLoggingEnabled = !noLogEnabled && (await isDetailedLoggingEnabled());
|
||||
const persistAttemptLogs = ({
|
||||
status,
|
||||
tokens,
|
||||
responseBody,
|
||||
error,
|
||||
providerRequest,
|
||||
providerResponse,
|
||||
clientResponse,
|
||||
claudeCacheMeta,
|
||||
claudeCacheUsageMeta,
|
||||
}: {
|
||||
status: number;
|
||||
tokens?: unknown;
|
||||
responseBody?: unknown;
|
||||
error?: string | null;
|
||||
providerRequest?: unknown;
|
||||
providerResponse?: unknown;
|
||||
clientResponse?: unknown;
|
||||
claudeCacheMeta?: any;
|
||||
claudeCacheUsageMeta?: any;
|
||||
}) => {
|
||||
const callLogId = generateRequestId();
|
||||
|
||||
saveCallLog({
|
||||
id: callLogId,
|
||||
method: "POST",
|
||||
path: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
status,
|
||||
model,
|
||||
requestedModel,
|
||||
provider,
|
||||
connectionId,
|
||||
duration: Date.now() - startTime,
|
||||
tokens: tokens || {},
|
||||
requestBody: attachLogMeta((body as Record<string, unknown>) ?? undefined, {
|
||||
claudePromptCache: claudeCacheMeta,
|
||||
}),
|
||||
responseBody: attachLogMeta((responseBody as Record<string, unknown>) ?? undefined, {
|
||||
claudePromptCache: claudeCacheMeta
|
||||
? {
|
||||
applied: claudeCacheMeta.applied,
|
||||
totalBreakpoints: claudeCacheMeta.totalBreakpoints,
|
||||
anthropicBeta: claudeCacheMeta.anthropicBeta,
|
||||
}
|
||||
: null,
|
||||
claudePromptCacheUsage: claudeCacheUsageMeta,
|
||||
}),
|
||||
error: error || null,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
comboName,
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
noLog: noLogEnabled,
|
||||
}).catch(() => {});
|
||||
|
||||
if (!detailedLoggingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
saveRequestDetailLog({
|
||||
call_log_id: callLogId,
|
||||
client_request: clientRawRequest?.body ?? body,
|
||||
translated_request: providerRequest ?? null,
|
||||
provider_response: providerResponse ?? null,
|
||||
client_response: clientResponse ?? null,
|
||||
provider,
|
||||
model,
|
||||
source_format: sourceFormat,
|
||||
target_format: targetFormat,
|
||||
duration_ms: Date.now() - startTime,
|
||||
api_key_id: apiKeyInfo?.id || null,
|
||||
no_log: noLogEnabled,
|
||||
});
|
||||
} catch (err) {
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
log?.debug?.("DETAIL_LOG", `Failed to save detailed log: ${errMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Primary path: merge client model id + alias target so config on either key applies; resolved
|
||||
// id wins on same header name. T5 family fallback uses only (nextModel, resolveModelAlias(next))
|
||||
@@ -917,40 +1010,34 @@ export async function handleChatCore({
|
||||
);
|
||||
} catch (error) {
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
const failureStatus = error.name === "AbortError" ? 499 : HTTP_STATUS.BAD_GATEWAY;
|
||||
const failureMessage =
|
||||
error.name === "AbortError"
|
||||
? "Request aborted"
|
||||
: formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY);
|
||||
appendRequestLog({
|
||||
model,
|
||||
provider,
|
||||
connectionId,
|
||||
status: `FAILED ${error.name === "AbortError" ? 499 : HTTP_STATUS.BAD_GATEWAY}`,
|
||||
}).catch(() => {});
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
status: error.name === "AbortError" ? 499 : HTTP_STATUS.BAD_GATEWAY,
|
||||
model,
|
||||
requestedModel,
|
||||
provider,
|
||||
connectionId,
|
||||
duration: Date.now() - startTime,
|
||||
requestBody: attachLogMeta(body, {
|
||||
claudePromptCache: claudePromptCacheLogMeta,
|
||||
}),
|
||||
error: error.message,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
comboName,
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
noLog: apiKeyInfo?.noLog === true,
|
||||
status: `FAILED ${failureStatus}`,
|
||||
}).catch(() => {});
|
||||
persistAttemptLogs({
|
||||
status: failureStatus,
|
||||
error: failureMessage,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
clientResponse: buildErrorBody(failureStatus, failureMessage),
|
||||
claudeCacheMeta: claudePromptCacheLogMeta,
|
||||
});
|
||||
if (error.name === "AbortError") {
|
||||
streamController.handleError(error);
|
||||
return createErrorResult(499, "Request aborted");
|
||||
}
|
||||
persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, error?.name || "upstream_error");
|
||||
const errMsg = formatProviderError(error, provider, model, HTTP_STATUS.BAD_GATEWAY);
|
||||
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, errMsg);
|
||||
persistFailureUsage(
|
||||
HTTP_STATUS.BAD_GATEWAY,
|
||||
error instanceof Error && error.name ? error.name : "upstream_error"
|
||||
);
|
||||
console.log(`${COLORS.red}[ERROR] ${failureMessage}${COLORS.reset}`);
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, failureMessage);
|
||||
}
|
||||
|
||||
// Handle 401/403 - try token refresh using executor
|
||||
@@ -996,8 +1083,11 @@ export async function handleChatCore({
|
||||
if (retryResult.response.ok) {
|
||||
providerResponse = retryResult.response;
|
||||
providerUrl = retryResult.url;
|
||||
providerHeaders = retryResult.headers;
|
||||
finalBody = retryResult.transformedBody;
|
||||
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
|
||||
}
|
||||
} catch (retryError) {
|
||||
} catch {
|
||||
log?.warn?.("TOKEN", `${provider.toUpperCase()} | retry after refresh failed`);
|
||||
}
|
||||
} else {
|
||||
@@ -1010,10 +1100,12 @@ export async function handleChatCore({
|
||||
// Check provider response - return error info for fallback handling
|
||||
if (!providerResponse.ok) {
|
||||
trackPendingRequest(model, provider, connectionId, false);
|
||||
const { statusCode, message, retryAfterMs } = await parseUpstreamError(
|
||||
providerResponse,
|
||||
provider
|
||||
);
|
||||
const {
|
||||
statusCode,
|
||||
message,
|
||||
retryAfterMs,
|
||||
responseBody: upstreamErrorBody,
|
||||
} = await parseUpstreamError(providerResponse, provider);
|
||||
|
||||
// T06/T10/T36: classify provider errors and persist terminal account states.
|
||||
const errorType = classifyProviderError(statusCode, message);
|
||||
@@ -1065,26 +1157,7 @@ export async function handleChatCore({
|
||||
appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(
|
||||
() => {}
|
||||
);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
status: statusCode,
|
||||
model,
|
||||
requestedModel,
|
||||
provider,
|
||||
connectionId,
|
||||
duration: Date.now() - startTime,
|
||||
requestBody: attachLogMeta(body, {
|
||||
claudePromptCache: claudePromptCacheLogMeta,
|
||||
}),
|
||||
error: message,
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
comboName,
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
noLog: apiKeyInfo?.noLog === true,
|
||||
}).catch(() => {});
|
||||
|
||||
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
|
||||
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
|
||||
|
||||
@@ -1096,6 +1169,12 @@ export async function handleChatCore({
|
||||
|
||||
// Log error with full request body for debugging
|
||||
reqLogger.logError(new Error(message), finalBody || translatedBody);
|
||||
reqLogger.logProviderResponse(
|
||||
providerResponse.status,
|
||||
providerResponse.statusText,
|
||||
providerResponse.headers,
|
||||
upstreamErrorBody
|
||||
);
|
||||
|
||||
// Update rate limiter from error response headers
|
||||
updateFromHeaders(provider, connectionId, providerResponse.headers, statusCode, model);
|
||||
@@ -1119,24 +1198,53 @@ export async function handleChatCore({
|
||||
providerUrl = fallbackResult.url;
|
||||
providerHeaders = fallbackResult.headers;
|
||||
finalBody = fallbackResult.transformedBody;
|
||||
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
|
||||
// Continue processing with the fallback response — skip error return
|
||||
log?.info?.("MODEL_FALLBACK", `Serving ${nextModel} as fallback for ${model}`);
|
||||
// Jump to streaming/non-streaming handling below
|
||||
// We fall through by NOT returning here
|
||||
} else {
|
||||
// Fallback also failed — return original error
|
||||
persistAttemptLogs({
|
||||
status: statusCode,
|
||||
error: errMsg,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: upstreamErrorBody,
|
||||
clientResponse: buildErrorBody(statusCode, errMsg),
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
}
|
||||
} catch {
|
||||
persistAttemptLogs({
|
||||
status: statusCode,
|
||||
error: errMsg,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: upstreamErrorBody,
|
||||
clientResponse: buildErrorBody(statusCode, errMsg),
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
}
|
||||
} else {
|
||||
persistAttemptLogs({
|
||||
status: statusCode,
|
||||
error: errMsg,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: upstreamErrorBody,
|
||||
clientResponse: buildErrorBody(statusCode, errMsg),
|
||||
});
|
||||
persistFailureUsage(statusCode, "model_unavailable");
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
}
|
||||
} else {
|
||||
persistAttemptLogs({
|
||||
status: statusCode,
|
||||
error: errMsg,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: upstreamErrorBody,
|
||||
clientResponse: buildErrorBody(statusCode, errMsg),
|
||||
});
|
||||
persistFailureUsage(statusCode, `upstream_${statusCode}`);
|
||||
return createErrorResult(statusCode, errMsg, retryAfterMs);
|
||||
}
|
||||
@@ -1181,6 +1289,10 @@ export async function handleChatCore({
|
||||
});
|
||||
if (fbResult.response.ok) {
|
||||
providerResponse = fbResult.response;
|
||||
providerUrl = fbResult.url;
|
||||
providerHeaders = fbResult.headers;
|
||||
finalBody = fbResult.transformedBody;
|
||||
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
|
||||
log?.info?.(
|
||||
"EMERGENCY_FALLBACK",
|
||||
`Serving ${fbDecision.provider}/${fbDecision.model} as budget fallback for ${provider}/${model}`
|
||||
@@ -1193,7 +1305,8 @@ export async function handleChatCore({
|
||||
);
|
||||
}
|
||||
} catch (fbErr) {
|
||||
log?.warn?.("EMERGENCY_FALLBACK", `Emergency fallback error: ${fbErr?.message}`);
|
||||
const errMessage = fbErr instanceof Error ? fbErr.message : String(fbErr);
|
||||
log?.warn?.("EMERGENCY_FALLBACK", `Emergency fallback error: ${errMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1206,6 +1319,7 @@ export async function handleChatCore({
|
||||
const contentType = (providerResponse.headers.get("content-type") || "").toLowerCase();
|
||||
let responseBody;
|
||||
const rawBody = await providerResponse.text();
|
||||
const normalizedProviderPayload = normalizePayloadForLog(rawBody);
|
||||
const looksLikeSSE =
|
||||
contentType.includes("text/event-stream") || /(^|\n)\s*(event|data):/m.test(rawBody);
|
||||
|
||||
@@ -1223,11 +1337,16 @@ export async function handleChatCore({
|
||||
connectionId,
|
||||
status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}`,
|
||||
}).catch(() => {});
|
||||
const invalidSseMessage = "Invalid SSE response for non-streaming request";
|
||||
persistAttemptLogs({
|
||||
status: HTTP_STATUS.BAD_GATEWAY,
|
||||
error: invalidSseMessage,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: normalizedProviderPayload,
|
||||
clientResponse: buildErrorBody(HTTP_STATUS.BAD_GATEWAY, invalidSseMessage),
|
||||
});
|
||||
persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "invalid_sse_payload");
|
||||
return createErrorResult(
|
||||
HTTP_STATUS.BAD_GATEWAY,
|
||||
"Invalid SSE response for non-streaming request"
|
||||
);
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, invalidSseMessage);
|
||||
}
|
||||
|
||||
responseBody = parsedFromSSE;
|
||||
@@ -1241,14 +1360,34 @@ export async function handleChatCore({
|
||||
connectionId,
|
||||
status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}`,
|
||||
}).catch(() => {});
|
||||
const invalidJsonMessage = "Invalid JSON response from provider";
|
||||
persistAttemptLogs({
|
||||
status: HTTP_STATUS.BAD_GATEWAY,
|
||||
error: invalidJsonMessage,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: normalizedProviderPayload,
|
||||
clientResponse: buildErrorBody(HTTP_STATUS.BAD_GATEWAY, invalidJsonMessage),
|
||||
});
|
||||
persistFailureUsage(HTTP_STATUS.BAD_GATEWAY, "invalid_json_payload");
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Invalid JSON response from provider");
|
||||
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, invalidJsonMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) {
|
||||
responseBody = restoreClaudePassthroughToolNames(responseBody, toolNameMap);
|
||||
}
|
||||
reqLogger.logProviderResponse(
|
||||
providerResponse.status,
|
||||
providerResponse.statusText,
|
||||
providerResponse.headers,
|
||||
looksLikeSSE
|
||||
? {
|
||||
_streamed: true,
|
||||
_format: "sse-json",
|
||||
summary: responseBody,
|
||||
}
|
||||
: responseBody
|
||||
);
|
||||
|
||||
// Notify success - caller can clear error status if needed
|
||||
if (onRequestSuccess) {
|
||||
@@ -1263,36 +1402,6 @@ export async function handleChatCore({
|
||||
|
||||
// Save structured call log with full payloads
|
||||
const cacheUsageLogMeta = buildCacheUsageLogMeta(usage);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
status: 200,
|
||||
model,
|
||||
requestedModel,
|
||||
provider,
|
||||
connectionId,
|
||||
duration: Date.now() - startTime,
|
||||
tokens: usage,
|
||||
requestBody: attachLogMeta(body, {
|
||||
claudePromptCache: claudePromptCacheLogMeta,
|
||||
}),
|
||||
responseBody: attachLogMeta(responseBody, {
|
||||
claudePromptCache: claudePromptCacheLogMeta
|
||||
? {
|
||||
applied: claudePromptCacheLogMeta.applied,
|
||||
totalBreakpoints: claudePromptCacheLogMeta.totalBreakpoints,
|
||||
anthropicBeta: claudePromptCacheLogMeta.anthropicBeta,
|
||||
}
|
||||
: null,
|
||||
claudePromptCacheUsage: cacheUsageLogMeta,
|
||||
}),
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
comboName,
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
noLog: apiKeyInfo?.noLog === true,
|
||||
}).catch(() => {});
|
||||
if (usage && typeof usage === "object") {
|
||||
const msg = `[${new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}] 📊 [USAGE] ${provider.toUpperCase()} | in=${getLoggedInputTokens(usage)} | out=${getLoggedOutputTokens(usage)}${connectionId ? ` | account=${connectionId.slice(0, 8)}...` : ""}`;
|
||||
console.log(`${COLORS.green}${msg}${COLORS.reset}`);
|
||||
@@ -1315,6 +1424,11 @@ export async function handleChatCore({
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyInfo?.id && usage) {
|
||||
const estimatedCost = await calculateCost(provider, model, usage);
|
||||
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
|
||||
}
|
||||
|
||||
// Translate response to client's expected format (usually OpenAI)
|
||||
// Pass toolNameMap so Claude OAuth proxy_ prefix is stripped in tool_use blocks (#605)
|
||||
let translatedResponse = needsTranslation(targetFormat, sourceFormat)
|
||||
@@ -1350,8 +1464,9 @@ export async function handleChatCore({
|
||||
|
||||
// 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) {
|
||||
// Extracts <think> and <thinking> tags into reasoning_content
|
||||
// Target format determines output shape. If we are outputting OpenAI shape or pseudo-OpenAI shape, sanitize.
|
||||
if (targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
translatedResponse = sanitizeOpenAIResponse(translatedResponse);
|
||||
}
|
||||
|
||||
@@ -1380,6 +1495,23 @@ export async function handleChatCore({
|
||||
|
||||
// ── Phase 9.2: Save for idempotency ──
|
||||
saveIdempotency(idempotencyKey, translatedResponse, 200);
|
||||
reqLogger.logConvertedResponse(translatedResponse);
|
||||
persistAttemptLogs({
|
||||
status: 200,
|
||||
tokens: usage,
|
||||
responseBody,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: looksLikeSSE
|
||||
? {
|
||||
_streamed: true,
|
||||
_format: "sse-json",
|
||||
summary: responseBody,
|
||||
}
|
||||
: responseBody,
|
||||
clientResponse: translatedResponse,
|
||||
claudeCacheMeta: claudePromptCacheLogMeta,
|
||||
claudeCacheUsageMeta: cacheUsageLogMeta,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -1415,54 +1547,43 @@ export async function handleChatCore({
|
||||
status: streamStatus,
|
||||
usage: streamUsage,
|
||||
responseBody: streamResponseBody,
|
||||
providerPayload,
|
||||
clientPayload,
|
||||
}) => {
|
||||
const cacheUsageLogMeta = buildCacheUsageLogMeta(streamUsage);
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: clientRawRequest?.endpoint || "/v1/chat/completions",
|
||||
persistAttemptLogs({
|
||||
status: streamStatus || 200,
|
||||
model,
|
||||
requestedModel,
|
||||
provider,
|
||||
connectionId,
|
||||
duration: Date.now() - startTime,
|
||||
tokens: streamUsage || {},
|
||||
requestBody: attachLogMeta(body, {
|
||||
claudePromptCache: claudePromptCacheLogMeta,
|
||||
}),
|
||||
responseBody: attachLogMeta(streamResponseBody ?? undefined, {
|
||||
claudePromptCache: claudePromptCacheLogMeta
|
||||
? {
|
||||
applied: claudePromptCacheLogMeta.applied,
|
||||
totalBreakpoints: claudePromptCacheLogMeta.totalBreakpoints,
|
||||
anthropicBeta: claudePromptCacheLogMeta.anthropicBeta,
|
||||
}
|
||||
: null,
|
||||
claudePromptCacheUsage: cacheUsageLogMeta,
|
||||
}),
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
comboName,
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
noLog: apiKeyInfo?.noLog === true,
|
||||
}).catch(() => {});
|
||||
responseBody: streamResponseBody ?? undefined,
|
||||
providerRequest: finalBody || translatedBody,
|
||||
providerResponse: providerPayload,
|
||||
clientResponse: clientPayload ?? streamResponseBody ?? undefined,
|
||||
claudeCacheMeta: claudePromptCacheLogMeta,
|
||||
claudeCacheUsageMeta: cacheUsageLogMeta,
|
||||
});
|
||||
|
||||
if (apiKeyInfo?.id && streamUsage) {
|
||||
calculateCost(provider, model, streamUsage)
|
||||
.then((estimatedCost) => {
|
||||
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
// For Codex provider, translate response from openai-responses to openai (Chat Completions) format
|
||||
// For providers using Responses API format, translate stream back to openai (Chat Completions) format
|
||||
// UNLESS client is Droid CLI which expects openai-responses format back
|
||||
const isDroidCLI =
|
||||
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
|
||||
const needsCodexTranslation =
|
||||
provider === "codex" &&
|
||||
const needsResponsesTranslation =
|
||||
targetFormat === FORMATS.OPENAI_RESPONSES &&
|
||||
sourceFormat === FORMATS.OPENAI &&
|
||||
!isResponsesEndpoint &&
|
||||
!isDroidCLI;
|
||||
|
||||
if (needsCodexTranslation) {
|
||||
// Codex returns openai-responses, translate to openai (Chat Completions) that clients expect
|
||||
log?.debug?.("STREAM", `Codex translation mode: openai-responses → openai`);
|
||||
if (needsResponsesTranslation) {
|
||||
// Provider returns openai-responses, translate to openai (Chat Completions) that clients expect
|
||||
log?.debug?.("STREAM", `Responses translation mode: openai-responses → openai`);
|
||||
transformStream = createSSETransformStreamWithLogger(
|
||||
"openai-responses",
|
||||
"openai",
|
||||
|
||||
@@ -32,13 +32,12 @@ function toNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
// ── Think tag regex ────────────────────────────────────────────────────────
|
||||
// Matches <think>...</think> blocks (greedy, dotAll)
|
||||
const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/gi;
|
||||
// Matches <think>...</think> blocks and <thinking>...</thinking> (greedy, dotAll)
|
||||
const THINK_TAG_REGEX = /<(?:think|thinking)>([\s\S]*?)<\/(?:think|thinking)>/gi;
|
||||
|
||||
// #638: Collapse runs of 3+ consecutive newlines into \n\n
|
||||
// #638, #727: Collapse runs of 2+ consecutive newlines into \n\n
|
||||
// Tool call responses from thinking models often accumulate excessive newlines
|
||||
const EXCESSIVE_NEWLINES = /\n{3,}/g;
|
||||
const EXCESSIVE_NEWLINES = /\n{2,}/g;
|
||||
function collapseExcessiveNewlines(text: string): string {
|
||||
return text.replace(EXCESSIVE_NEWLINES, "\n\n");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ import { supportsToolCalling } from "./modelCapabilities.ts";
|
||||
|
||||
// Status codes that should mark semaphore + record circuit breaker failures
|
||||
const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504];
|
||||
const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [
|
||||
/\bprohibited_content\b/i,
|
||||
/request blocked by .*api/i,
|
||||
/provided message roles? is not valid/i,
|
||||
/unsupported .*message role/i,
|
||||
/no such tool available/i,
|
||||
/unsupported content part type/i,
|
||||
/tool(?:_call|_use)? .* not (?:available|found)/i,
|
||||
];
|
||||
|
||||
const MAX_COMBO_DEPTH = 3;
|
||||
|
||||
@@ -258,6 +267,12 @@ function extractPromptForIntent(body) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function shouldFallbackComboBadRequest(status, errorText) {
|
||||
if (status !== 400 || !errorText) return false;
|
||||
const message = String(errorText);
|
||||
return COMBO_BAD_REQUEST_FALLBACK_PATTERNS.some((pattern) => pattern.test(message));
|
||||
}
|
||||
|
||||
function mapIntentToTaskType(intent) {
|
||||
switch (intent) {
|
||||
case "code":
|
||||
@@ -449,14 +464,23 @@ export async function handleComboChat({
|
||||
const res = await handleSingleModel(b, modelStr);
|
||||
if (!res.ok) return res;
|
||||
|
||||
// Non-streaming: inject tag into JSON response (existing logic)
|
||||
// Non-streaming: inject tag into JSON response
|
||||
// Fix #721: Use OpenAI choices format (json.choices[0].message) not json.messages
|
||||
if (!b.stream) {
|
||||
try {
|
||||
const json = await res.clone().json();
|
||||
const msgs = Array.isArray(json?.messages) ? json.messages : [];
|
||||
if (msgs.length > 0) {
|
||||
const tagged = injectModelTag(msgs, modelStr);
|
||||
return new Response(JSON.stringify({ ...json, messages: tagged }), {
|
||||
const choice = json?.choices?.[0];
|
||||
if (choice?.message) {
|
||||
// Wrap single message in array for injectModelTag, then unwrap
|
||||
const tagged = injectModelTag([choice.message], modelStr);
|
||||
// If the message had tool_calls but no string content, injectModelTag
|
||||
// appends a synthetic assistant message — use the last one
|
||||
const taggedMsg = tagged[tagged.length - 1];
|
||||
const updatedJson = {
|
||||
...json,
|
||||
choices: [{ ...choice, message: taggedMsg }, ...(json.choices?.slice(1) || [])],
|
||||
};
|
||||
return new Response(JSON.stringify(updatedJson), {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
});
|
||||
@@ -487,8 +511,9 @@ export async function handleComboChat({
|
||||
|
||||
const text = decoder.decode(chunk, { stream: true });
|
||||
|
||||
// Look for the first SSE data line with non-empty content
|
||||
// Pattern: "content":"<non-empty>" — we inject tag at the start
|
||||
// Fix #721: Look for either non-empty content OR tool_calls in the
|
||||
// SSE data. Tool-call-only responses have content:null, so we inject
|
||||
// the tag when we see a finish_reason approaching, or on first content.
|
||||
const contentMatch = text.match(/"content":"([^"]+)/);
|
||||
if (contentMatch) {
|
||||
// Inject tag at the beginning of the first content value
|
||||
@@ -501,6 +526,27 @@ export async function handleComboChat({
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix #721: For tool-call-only streams, inject the tag when we see
|
||||
// the finish_reason chunk (before it reaches the client SDK which
|
||||
// would close the connection). This ensures the tag roundtrips
|
||||
// through the conversation history even when there's no text content.
|
||||
if (text.includes('"finish_reason"') && !text.includes('"finish_reason":null')) {
|
||||
// Inject a content chunk with the tag just before this finish chunk
|
||||
const tagChunk = `data: ${JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
delta: { content: tagContent },
|
||||
index: 0,
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
})}\n\n`;
|
||||
tagInjected = true;
|
||||
controller.enqueue(encoder.encode(tagChunk));
|
||||
controller.enqueue(chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
// No content yet — passthrough
|
||||
controller.enqueue(chunk);
|
||||
},
|
||||
@@ -890,17 +936,25 @@ export async function handleComboChat({
|
||||
provider,
|
||||
result.headers
|
||||
);
|
||||
const comboBadRequestFallback = shouldFallbackComboBadRequest(result.status, errorText);
|
||||
|
||||
// Record failure in circuit breaker for transient errors
|
||||
if (TRANSIENT_FOR_BREAKER.includes(result.status)) {
|
||||
breaker._onFailure();
|
||||
}
|
||||
|
||||
if (!shouldFallback) {
|
||||
if (!shouldFallback && !comboBadRequestFallback) {
|
||||
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (comboBadRequestFallback) {
|
||||
log.info(
|
||||
"COMBO",
|
||||
`Treating provider-scoped 400 from ${modelStr} as model-local failure; trying next combo target`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this is a transient error worth retrying on same model
|
||||
const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status);
|
||||
if (retry < maxRetries && isTransient) {
|
||||
@@ -1146,6 +1200,7 @@ async function handleRoundRobinCombo({
|
||||
provider,
|
||||
result.headers
|
||||
);
|
||||
const comboBadRequestFallback = shouldFallbackComboBadRequest(result.status, errorText);
|
||||
|
||||
// Transient errors → mark in semaphore AND record circuit breaker failure
|
||||
if (TRANSIENT_FOR_BREAKER.includes(result.status) && cooldownMs > 0) {
|
||||
@@ -1157,11 +1212,18 @@ async function handleRoundRobinCombo({
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldFallback) {
|
||||
if (!shouldFallback && !comboBadRequestFallback) {
|
||||
log.warn("COMBO-RR", `${modelStr} failed (no fallback)`, { status: result.status });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (comboBadRequestFallback) {
|
||||
log.info(
|
||||
"COMBO-RR",
|
||||
`Treating provider-scoped 400 from ${modelStr} as model-local failure; trying next model`
|
||||
);
|
||||
}
|
||||
|
||||
// Transient error → retry same model
|
||||
const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status);
|
||||
if (retry < maxRetries && isTransient) {
|
||||
|
||||
@@ -67,7 +67,17 @@ export function injectModelTag(messages: Message[], providerModel: string): Mess
|
||||
}
|
||||
|
||||
const msg = cleaned[lastAssistantIdx];
|
||||
if (typeof msg.content !== "string") return cleaned;
|
||||
// Fix #721: Handle messages where content is not a string (tool_calls responses).
|
||||
// In this case, append a synthetic assistant message with the tag so the pin
|
||||
// roundtrips through the conversation history.
|
||||
if (typeof msg.content !== "string") {
|
||||
// If the message has tool_calls but no string content, append a new assistant
|
||||
// message with the tag rather than silently failing.
|
||||
return [
|
||||
...cleaned,
|
||||
{ role: "assistant", content: `\n<omniModel>${providerModel}</omniModel>` },
|
||||
];
|
||||
}
|
||||
|
||||
const tagged = [...cleaned];
|
||||
tagged[lastAssistantIdx] = {
|
||||
|
||||
@@ -100,13 +100,66 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota
|
||||
return quota.total > 0 || quota.remainingPercentage !== undefined;
|
||||
}
|
||||
|
||||
// GLM (Z.AI) quota API config
|
||||
const GLM_QUOTA_URLS: Record<string, string> = {
|
||||
international: "https://api.z.ai/api/monitor/usage/quota/limit",
|
||||
china: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
|
||||
};
|
||||
|
||||
async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string, unknown>) {
|
||||
const region = providerSpecificData?.apiRegion || "international";
|
||||
const quotaUrl = GLM_QUOTA_URLS[region] || GLM_QUOTA_URLS.international;
|
||||
|
||||
const res = await fetch(quotaUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) throw new Error("Invalid API key");
|
||||
throw new Error(`GLM quota API error (${res.status})`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const data = toRecord(json.data);
|
||||
const limits: unknown[] = Array.isArray(data.limits) ? data.limits : [];
|
||||
const quotas: Record<string, UsageQuota> = {};
|
||||
|
||||
for (const limit of limits) {
|
||||
const src = toRecord(limit);
|
||||
if (src.type !== "TOKENS_LIMIT") continue;
|
||||
|
||||
const usedPercent = toNumber(src.percentage, 0);
|
||||
const resetMs = toNumber(src.nextResetTime, 0);
|
||||
const remaining = Math.max(0, 100 - usedPercent);
|
||||
|
||||
quotas["session"] = {
|
||||
used: usedPercent,
|
||||
total: 100,
|
||||
remaining,
|
||||
remainingPercentage: remaining,
|
||||
resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null,
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
const levelRaw = typeof data.level === "string" ? data.level : "";
|
||||
const plan = levelRaw
|
||||
? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase()
|
||||
: "Unknown";
|
||||
|
||||
return { plan, quotas };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage data for a provider connection
|
||||
* @param {Object} connection - Provider connection with accessToken
|
||||
* @returns {Promise<unknown>} Usage data with quotas
|
||||
*/
|
||||
export async function getUsageForProvider(connection) {
|
||||
const { provider, accessToken, providerSpecificData } = connection;
|
||||
const { provider, accessToken, apiKey, providerSpecificData } = connection;
|
||||
|
||||
switch (provider) {
|
||||
case "github":
|
||||
@@ -127,6 +180,8 @@ export async function getUsageForProvider(connection) {
|
||||
return await getQwenUsage(accessToken, providerSpecificData);
|
||||
case "iflow":
|
||||
return await getIflowUsage(accessToken);
|
||||
case "glm":
|
||||
return await getGlmUsage(apiKey, providerSpecificData);
|
||||
default:
|
||||
return { message: `Usage API not implemented for ${provider}` };
|
||||
}
|
||||
|
||||
@@ -105,13 +105,14 @@ function markMessageCacheControl(msg, ttl) {
|
||||
}
|
||||
|
||||
// Prepare request for Claude format endpoints
|
||||
// - Cleanup cache_control
|
||||
// - Cleanup cache_control (unless preserveCacheControl=true for passthrough)
|
||||
// - Filter empty messages
|
||||
// - Add thinking block for Anthropic endpoint (provider === "claude")
|
||||
// - Fix tool_use/tool_result ordering
|
||||
export function prepareClaudeRequest(body, provider = null) {
|
||||
export function prepareClaudeRequest(body, provider = null, preserveCacheControl = false) {
|
||||
// 1. System: remove all cache_control, add only to last block with ttl 1h
|
||||
if (body.system && Array.isArray(body.system)) {
|
||||
// In passthrough mode, preserve existing cache_control markers
|
||||
if (body.system && Array.isArray(body.system) && !preserveCacheControl) {
|
||||
body.system = body.system.map((block, i) => {
|
||||
const { cache_control, ...rest } = block;
|
||||
if (i === body.system.length - 1) {
|
||||
@@ -127,11 +128,12 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
let filtered = [];
|
||||
|
||||
// Pass 1: remove cache_control + filter empty messages
|
||||
// In passthrough mode, preserve existing cache_control markers
|
||||
for (let i = 0; i < len; i++) {
|
||||
const msg = body.messages[i];
|
||||
|
||||
// Remove cache_control from content blocks
|
||||
if (Array.isArray(msg.content)) {
|
||||
// Remove cache_control from content blocks (skip in passthrough mode)
|
||||
if (Array.isArray(msg.content) && !preserveCacheControl) {
|
||||
for (const block of msg.content) {
|
||||
delete block.cache_control;
|
||||
}
|
||||
@@ -177,14 +179,17 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
// Claude Code-style prompt caching:
|
||||
// - cache the second-to-last user turn for conversation reuse
|
||||
// - cache the last assistant turn so the next user turn can reuse it
|
||||
const userMessageIndexes = filtered.reduce((indexes, msg, index) => {
|
||||
if (msg?.role === "user") indexes.push(index);
|
||||
return indexes;
|
||||
}, []);
|
||||
const secondToLastUserIndex =
|
||||
userMessageIndexes.length >= 2 ? userMessageIndexes[userMessageIndexes.length - 2] : -1;
|
||||
if (secondToLastUserIndex >= 0) {
|
||||
markMessageCacheControl(filtered[secondToLastUserIndex]);
|
||||
// Skip in passthrough mode to preserve client's cache_control markers
|
||||
if (!preserveCacheControl) {
|
||||
const userMessageIndexes = filtered.reduce((indexes, msg, index) => {
|
||||
if (msg?.role === "user") indexes.push(index);
|
||||
return indexes;
|
||||
}, []);
|
||||
const secondToLastUserIndex =
|
||||
userMessageIndexes.length >= 2 ? userMessageIndexes[userMessageIndexes.length - 2] : -1;
|
||||
if (secondToLastUserIndex >= 0) {
|
||||
markMessageCacheControl(filtered[secondToLastUserIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2 (reverse): add cache_control to last assistant + handle thinking for Anthropic
|
||||
@@ -194,7 +199,8 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
|
||||
if (msg.role === "assistant" && Array.isArray(ensureMessageContentArray(msg))) {
|
||||
// Add cache_control to last block of first (from end) assistant with content
|
||||
if (!lastAssistantProcessed && markMessageCacheControl(msg)) {
|
||||
// Skip in passthrough mode to preserve client's cache_control markers
|
||||
if (!preserveCacheControl && !lastAssistantProcessed && markMessageCacheControl(msg)) {
|
||||
lastAssistantProcessed = true;
|
||||
}
|
||||
|
||||
@@ -227,7 +233,8 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
|
||||
// 3. Tools: remove all cache_control, add only to last non-deferred tool with ttl 1h
|
||||
// Tools with defer_loading=true cannot have cache_control (API rejects it)
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
// In passthrough mode, preserve existing cache_control markers
|
||||
if (body.tools && Array.isArray(body.tools) && !preserveCacheControl) {
|
||||
body.tools = body.tools.map((tool) => {
|
||||
const { cache_control, ...rest } = tool;
|
||||
return rest;
|
||||
|
||||
@@ -149,8 +149,10 @@ export function translateRequest(
|
||||
}
|
||||
|
||||
// Final step: prepare request for Claude format endpoints
|
||||
// In Claude passthrough mode (Claude → Claude), preserve cache_control markers
|
||||
if (targetFormat === FORMATS.CLAUDE) {
|
||||
result = prepareClaudeRequest(result, provider);
|
||||
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE;
|
||||
result = prepareClaudeRequest(result, provider, isClaudePassthrough);
|
||||
}
|
||||
|
||||
// Normalize openai-responses input shape for providers that require list input.
|
||||
|
||||
@@ -92,8 +92,10 @@ export function claudeToGeminiRequest(model, body, stream) {
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
// Do NOT include thoughtSignature on functionCall parts — it is only valid
|
||||
// on thinking/reasoning parts and causes HTTP 400 "invalid argument" from the
|
||||
// Gemini API when present on a functionCall part.
|
||||
parts.push({
|
||||
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
|
||||
functionCall: {
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
|
||||
@@ -167,8 +167,10 @@ function openaiToGeminiBase(model, body, stream) {
|
||||
if (tc.type !== "function") continue;
|
||||
|
||||
const args = tryParseJSON(tc.function?.arguments || "{}");
|
||||
// Do NOT include thoughtSignature on functionCall parts — it is only valid
|
||||
// on thinking/reasoning parts and causes HTTP 400 "invalid argument" from the
|
||||
// Gemini API when present on a functionCall part (#725).
|
||||
parts.push({
|
||||
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
|
||||
functionCall: {
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
|
||||
@@ -90,7 +90,6 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
tool_calls: [
|
||||
{
|
||||
index: toolCall.index,
|
||||
id: toolCall.id,
|
||||
function: { arguments: delta.partial_json },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCorsOrigin } from "./cors.ts";
|
||||
import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/constants.ts";
|
||||
import { normalizePayloadForLog } from "@/lib/logPayloads";
|
||||
|
||||
/**
|
||||
* Build OpenAI-compatible error response body
|
||||
@@ -91,14 +92,16 @@ export function parseAntigravityRetryTime(message) {
|
||||
* Parse upstream provider error response
|
||||
* @param {Response} response - Fetch response from provider
|
||||
* @param {string} provider - Provider name (for Antigravity-specific parsing)
|
||||
* @returns {Promise<{statusCode: number, message: string, retryAfterMs: number|null}>}
|
||||
* @returns {Promise<{statusCode: number, message: string, retryAfterMs: number|null, responseBody: unknown}>}
|
||||
*/
|
||||
export async function parseUpstreamError(response, provider = null) {
|
||||
let message = "";
|
||||
let retryAfterMs = null;
|
||||
let responseBody = null;
|
||||
|
||||
try {
|
||||
const text = await response.text();
|
||||
responseBody = normalizePayloadForLog(text);
|
||||
|
||||
// Try parse as JSON
|
||||
try {
|
||||
@@ -109,6 +112,7 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
}
|
||||
} catch {
|
||||
message = `Upstream error: ${response.status}`;
|
||||
responseBody = { _rawText: message };
|
||||
}
|
||||
|
||||
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
|
||||
@@ -122,6 +126,7 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
statusCode: response.status,
|
||||
message: messageStr,
|
||||
retryAfterMs,
|
||||
responseBody,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
COLORS,
|
||||
} from "./usageTracking.ts";
|
||||
import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.ts";
|
||||
import {
|
||||
createStructuredSSECollector,
|
||||
buildStreamSummaryFromEvents,
|
||||
} from "./streamPayloadCollector.ts";
|
||||
import { STREAM_IDLE_TIMEOUT_MS, HTTP_STATUS } from "../config/constants.ts";
|
||||
import {
|
||||
sanitizeStreamingChunk,
|
||||
@@ -32,6 +36,8 @@ type StreamCompletePayload = {
|
||||
usage: unknown;
|
||||
/** Minimal response body for call log (streaming: usage + note; non-streaming not used) */
|
||||
responseBody?: unknown;
|
||||
providerPayload?: unknown;
|
||||
clientPayload?: unknown;
|
||||
};
|
||||
|
||||
type StreamOptions = {
|
||||
@@ -158,6 +164,12 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
|
||||
// Guard against duplicate [DONE] events — ensures exactly one per stream
|
||||
let doneSent = false;
|
||||
const providerPayloadCollector = createStructuredSSECollector({
|
||||
stage: "provider_response",
|
||||
});
|
||||
const clientPayloadCollector = createStructuredSSECollector({
|
||||
stage: "client_response",
|
||||
});
|
||||
|
||||
// Per-stream instances to avoid shared state with concurrent streams
|
||||
const decoder = new TextDecoder();
|
||||
@@ -212,6 +224,17 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (mode === STREAM_MODE.PASSTHROUGH) {
|
||||
let output;
|
||||
let injectedUsage = false;
|
||||
let clientPayload: unknown = null;
|
||||
|
||||
if (trimmed.startsWith("data:")) {
|
||||
const providerPayload = parseSSELine(trimmed);
|
||||
if (providerPayload) {
|
||||
providerPayloadCollector.push(providerPayload);
|
||||
if ((providerPayload as { done?: unknown }).done === true) {
|
||||
clientPayloadCollector.push(providerPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("data:") && trimmed.slice(5).trim() !== "[DONE]") {
|
||||
try {
|
||||
@@ -380,6 +403,8 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
injectedUsage = true;
|
||||
}
|
||||
}
|
||||
|
||||
clientPayload = parsed;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -391,6 +416,10 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientPayload) {
|
||||
clientPayloadCollector.push(clientPayload);
|
||||
}
|
||||
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
continue;
|
||||
@@ -401,10 +430,12 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
|
||||
const parsed = parseSSELine(trimmed);
|
||||
if (!parsed) continue;
|
||||
providerPayloadCollector.push(parsed);
|
||||
|
||||
if (parsed && parsed.done) {
|
||||
if (!doneSent) {
|
||||
doneSent = true;
|
||||
clientPayloadCollector.push({ done: true });
|
||||
const output = "data: [DONE]\n\n";
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
@@ -500,30 +531,47 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
// Content for call log is accumulated only from parsed (above) to avoid double-counting;
|
||||
// do not add again from item here.
|
||||
|
||||
// #723, #727: Sanitize intermediate stream chunks if target is OpenAI format loop
|
||||
let itemSanitized: Record<string, unknown> = item;
|
||||
if (targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
itemSanitized = sanitizeStreamingChunk(itemSanitized) as Record<string, unknown>;
|
||||
|
||||
// Extract reasoning tags from content if translation generated them
|
||||
const delta = itemSanitized?.choices?.[0]?.delta;
|
||||
if (delta?.content && typeof delta.content === "string") {
|
||||
const { content, thinking } = extractThinkingFromContent(delta.content);
|
||||
delta.content = content;
|
||||
if (thinking && !delta.reasoning_content) {
|
||||
delta.reasoning_content = thinking;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter empty chunks
|
||||
if (!hasValuableContent(item, sourceFormat)) {
|
||||
if (!hasValuableContent(itemSanitized, sourceFormat)) {
|
||||
continue; // Skip this empty chunk
|
||||
}
|
||||
|
||||
// Inject estimated usage if finish chunk has no valid usage
|
||||
const isFinishChunk =
|
||||
item.type === "message_delta" || item.choices?.[0]?.finish_reason;
|
||||
itemSanitized.type === "message_delta" || itemSanitized.choices?.[0]?.finish_reason;
|
||||
if (
|
||||
state.finishReason &&
|
||||
isFinishChunk &&
|
||||
!hasValidUsage(item.usage) &&
|
||||
!hasValidUsage(itemSanitized.usage) &&
|
||||
totalContentLength > 0
|
||||
) {
|
||||
const estimated = estimateUsage(body, totalContentLength, sourceFormat);
|
||||
item.usage = filterUsageForFormat(estimated, sourceFormat); // Filter + already has buffer
|
||||
itemSanitized.usage = filterUsageForFormat(estimated, sourceFormat); // Filter + already has buffer
|
||||
state.usage = estimated;
|
||||
} else if (state.finishReason && isFinishChunk && state.usage) {
|
||||
// Add buffer and filter usage for client (but keep original in state.usage for logging)
|
||||
const buffered = addBufferToUsage(state.usage);
|
||||
item.usage = filterUsageForFormat(buffered, sourceFormat);
|
||||
itemSanitized.usage = filterUsageForFormat(buffered, sourceFormat);
|
||||
}
|
||||
|
||||
const output = formatSSE(item, sourceFormat);
|
||||
const output = formatSSE(itemSanitized, sourceFormat);
|
||||
clientPayloadCollector.push(itemSanitized);
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
}
|
||||
@@ -551,6 +599,11 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (buffer.startsWith("data:") && !buffer.startsWith("data: ")) {
|
||||
output = "data: " + buffer.slice(5);
|
||||
}
|
||||
const bufferedPayload = parseSSELine(buffer.trim());
|
||||
if (bufferedPayload) {
|
||||
providerPayloadCollector.push(bufferedPayload);
|
||||
clientPayloadCollector.push(bufferedPayload);
|
||||
}
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
}
|
||||
@@ -601,7 +654,22 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
},
|
||||
_streamed: true,
|
||||
};
|
||||
onComplete({ status: 200, usage, responseBody });
|
||||
onComplete({
|
||||
status: 200,
|
||||
usage,
|
||||
responseBody,
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
sourceFormat,
|
||||
model
|
||||
),
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
includeEvents: false,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
return;
|
||||
@@ -611,6 +679,7 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (buffer.trim()) {
|
||||
const parsed = parseSSELine(buffer.trim());
|
||||
if (parsed && !parsed.done) {
|
||||
providerPayloadCollector.push(parsed);
|
||||
// Extract usage from remaining buffer — if the usage-bearing event
|
||||
// (e.g. response.completed) is the last SSE line, it ends up here
|
||||
// in the flush handler where extractUsage was not called.
|
||||
@@ -647,6 +716,7 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (translated?.length > 0) {
|
||||
for (const item of translated) {
|
||||
const output = formatSSE(item, sourceFormat);
|
||||
clientPayloadCollector.push(item);
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
}
|
||||
@@ -666,6 +736,7 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
if (flushed?.length > 0) {
|
||||
for (const item of flushed) {
|
||||
const output = formatSSE(item, sourceFormat);
|
||||
clientPayloadCollector.push(item);
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(encoder.encode(output));
|
||||
}
|
||||
@@ -684,6 +755,7 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
// Send [DONE] (only if not already sent during transform)
|
||||
if (!doneSent) {
|
||||
doneSent = true;
|
||||
clientPayloadCollector.push({ done: true });
|
||||
const doneOutput = "data: [DONE]\n\n";
|
||||
reqLogger?.appendConvertedChunk?.(doneOutput);
|
||||
controller.enqueue(encoder.encode(doneOutput));
|
||||
@@ -747,7 +819,22 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
},
|
||||
_streamed: true,
|
||||
};
|
||||
onComplete({ status: 200, usage: state?.usage, responseBody });
|
||||
onComplete({
|
||||
status: 200,
|
||||
usage: state?.usage,
|
||||
responseBody,
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
targetFormat,
|
||||
model
|
||||
),
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
includeEvents: false,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
import { cloneLogPayload } from "@/lib/logPayloads";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
|
||||
type StructuredSSEEvent = {
|
||||
index: number;
|
||||
event?: string;
|
||||
data: unknown;
|
||||
};
|
||||
|
||||
type CollectorOptions = {
|
||||
maxEvents?: number;
|
||||
maxBytes?: number;
|
||||
stage?: string;
|
||||
};
|
||||
|
||||
type BuildOptions = {
|
||||
includeEvents?: boolean;
|
||||
};
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function getEventName(payload: unknown): string | undefined {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
||||
|
||||
if (typeof (payload as { event?: unknown }).event === "string") {
|
||||
return (payload as { event: string }).event;
|
||||
}
|
||||
if (typeof (payload as { type?: unknown }).type === "string") {
|
||||
return (payload as { type: string }).type;
|
||||
}
|
||||
if ((payload as { done?: unknown }).done === true) {
|
||||
return "[DONE]";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
function toString(value: unknown, fallback = ""): string {
|
||||
return typeof value === "string" ? value : fallback;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown, fallback = 0): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeFormat(format?: string | null): string {
|
||||
if (!format) return "";
|
||||
if (format === FORMATS.OPENAI_RESPONSE) return FORMATS.OPENAI_RESPONSES;
|
||||
return format;
|
||||
}
|
||||
|
||||
function inferFormatFromEvents(
|
||||
events: StructuredSSEEvent[],
|
||||
fallbackFormat?: string | null
|
||||
): string {
|
||||
const normalizedFallback = normalizeFormat(fallbackFormat);
|
||||
if (normalizedFallback) return normalizedFallback;
|
||||
|
||||
for (const evt of events) {
|
||||
const payload = asRecord(evt.data);
|
||||
const eventType = toString(payload.type || evt.event);
|
||||
|
||||
if (eventType.startsWith("response.") || payload.object === "response") {
|
||||
return FORMATS.OPENAI_RESPONSES;
|
||||
}
|
||||
if (
|
||||
eventType === "message_start" ||
|
||||
eventType === "content_block_start" ||
|
||||
eventType === "content_block_delta" ||
|
||||
eventType === "message_delta" ||
|
||||
eventType === "message_stop" ||
|
||||
eventType === "ping"
|
||||
) {
|
||||
return FORMATS.CLAUDE;
|
||||
}
|
||||
if (Array.isArray(payload.candidates) || payload.usageMetadata) {
|
||||
return FORMATS.GEMINI;
|
||||
}
|
||||
}
|
||||
|
||||
return FORMATS.OPENAI;
|
||||
}
|
||||
|
||||
function mergeUsage(target: JsonRecord, incoming: unknown) {
|
||||
const usage = asRecord(incoming);
|
||||
for (const [key, value] of Object.entries(usage)) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
if ((target[key] as number | undefined) === undefined || value > 0) {
|
||||
target[key] = value;
|
||||
}
|
||||
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
target[key] = { ...asRecord(target[key]), ...asRecord(value) };
|
||||
} else if (typeof value === "string" && value.trim().length > 0) {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function buildOpenAISummary(events: StructuredSSEEvent[], fallbackModel?: string | null): unknown {
|
||||
const payloads = events
|
||||
.map((evt) => asRecord(evt.data))
|
||||
.filter((payload) => Object.keys(payload).length);
|
||||
if (payloads.length === 0) return null;
|
||||
|
||||
const first = payloads[0];
|
||||
const contentParts: string[] = [];
|
||||
const reasoningParts: string[] = [];
|
||||
type ToolCall = {
|
||||
id: string | null;
|
||||
index: number;
|
||||
type: string;
|
||||
function: { name: string; arguments: string };
|
||||
};
|
||||
const toolCalls = new Map<string, ToolCall>();
|
||||
let unknownToolCallSeq = 0;
|
||||
let finishReason = "stop";
|
||||
let usage: JsonRecord | null = null;
|
||||
|
||||
const getToolCallKey = (toolCall: JsonRecord) => {
|
||||
if (Number.isInteger(toolCall.index)) return `idx:${toolCall.index}`;
|
||||
if (toolCall.id) return `id:${toolCall.id}`;
|
||||
unknownToolCallSeq += 1;
|
||||
return `seq:${unknownToolCallSeq}`;
|
||||
};
|
||||
|
||||
for (const chunk of payloads) {
|
||||
const choice = asRecord(Array.isArray(chunk.choices) ? chunk.choices[0] : null);
|
||||
const delta = asRecord(choice.delta);
|
||||
|
||||
if (typeof delta.content === "string" && delta.content.length > 0) {
|
||||
contentParts.push(delta.content);
|
||||
}
|
||||
if (Array.isArray(delta.content)) {
|
||||
for (const part of delta.content) {
|
||||
const partObj = asRecord(part);
|
||||
if (typeof partObj.text === "string" && partObj.text.length > 0) {
|
||||
contentParts.push(partObj.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
|
||||
reasoningParts.push(delta.reasoning_content);
|
||||
}
|
||||
|
||||
if (Array.isArray(delta.tool_calls)) {
|
||||
for (const item of delta.tool_calls) {
|
||||
const toolCall = asRecord(item);
|
||||
const key = getToolCallKey(toolCall);
|
||||
const existing = toolCalls.get(key);
|
||||
const deltaArgs =
|
||||
typeof asRecord(toolCall.function).arguments === "string"
|
||||
? String(asRecord(toolCall.function).arguments)
|
||||
: "";
|
||||
|
||||
if (!existing) {
|
||||
toolCalls.set(key, {
|
||||
id: typeof toolCall.id === "string" ? toolCall.id : null,
|
||||
index: Number.isInteger(toolCall.index) ? Number(toolCall.index) : toolCalls.size,
|
||||
type: toString(toolCall.type, "function"),
|
||||
function: {
|
||||
name: toString(asRecord(toolCall.function).name, "unknown"),
|
||||
arguments: deltaArgs,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.id = existing.id || (typeof toolCall.id === "string" ? toolCall.id : null);
|
||||
if (
|
||||
(!Number.isInteger(existing.index) || existing.index < 0) &&
|
||||
Number.isInteger(toolCall.index)
|
||||
) {
|
||||
existing.index = Number(toolCall.index);
|
||||
}
|
||||
if (typeof asRecord(toolCall.function).name === "string" && !existing.function.name) {
|
||||
existing.function.name = String(asRecord(toolCall.function).name);
|
||||
}
|
||||
existing.function.arguments += deltaArgs;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
|
||||
finishReason = choice.finish_reason;
|
||||
}
|
||||
if (chunk.usage && typeof chunk.usage === "object") {
|
||||
usage = { ...asRecord(chunk.usage) };
|
||||
}
|
||||
}
|
||||
|
||||
const message: JsonRecord = {
|
||||
role: "assistant",
|
||||
content: contentParts.length > 0 ? contentParts.join("") : null,
|
||||
};
|
||||
if (reasoningParts.length > 0) {
|
||||
message.reasoning_content = reasoningParts.join("");
|
||||
}
|
||||
|
||||
const finalToolCalls = [...toolCalls.values()].sort((a, b) => a.index - b.index);
|
||||
if (finalToolCalls.length > 0) {
|
||||
finishReason = "tool_calls";
|
||||
message.tool_calls = finalToolCalls;
|
||||
}
|
||||
|
||||
const result: JsonRecord = {
|
||||
id: toString(first.id, `chatcmpl-${Date.now()}`),
|
||||
object: "chat.completion",
|
||||
created: toNumber(first.created, Math.floor(Date.now() / 1000)),
|
||||
model: toString(first.model, fallbackModel || "unknown"),
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message,
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (usage && Object.keys(usage).length > 0) {
|
||||
result.usage = usage;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildResponsesSummary(
|
||||
events: StructuredSSEEvent[],
|
||||
fallbackModel?: string | null
|
||||
): unknown {
|
||||
const payloads = events
|
||||
.map((evt) => asRecord(evt.data))
|
||||
.filter((payload) => Object.keys(payload).length);
|
||||
if (payloads.length === 0) return null;
|
||||
|
||||
let completed: JsonRecord | null = null;
|
||||
let latestResponse: JsonRecord | null = null;
|
||||
let usage: JsonRecord | null = null;
|
||||
const textParts: string[] = [];
|
||||
|
||||
for (const payload of payloads) {
|
||||
const eventType = toString(payload.type);
|
||||
if (
|
||||
eventType === "response.completed" &&
|
||||
payload.response &&
|
||||
typeof payload.response === "object"
|
||||
) {
|
||||
completed = asRecord(payload.response);
|
||||
}
|
||||
if (payload.response && typeof payload.response === "object") {
|
||||
latestResponse = asRecord(payload.response);
|
||||
} else if (payload.object === "response") {
|
||||
latestResponse = payload;
|
||||
}
|
||||
if (
|
||||
eventType === "response.output_text.delta" &&
|
||||
typeof payload.delta === "string" &&
|
||||
payload.delta.length > 0
|
||||
) {
|
||||
textParts.push(payload.delta);
|
||||
}
|
||||
if (payload.usage && typeof payload.usage === "object") {
|
||||
usage = { ...asRecord(payload.usage) };
|
||||
} else if (payload.response && typeof asRecord(payload.response).usage === "object") {
|
||||
usage = { ...asRecord(asRecord(payload.response).usage) };
|
||||
}
|
||||
}
|
||||
|
||||
const picked = completed || latestResponse;
|
||||
if (picked && Object.keys(picked).length > 0) {
|
||||
return {
|
||||
id: toString(picked.id, `resp_${Date.now()}`),
|
||||
object: "response",
|
||||
model: toString(picked.model, fallbackModel || "unknown"),
|
||||
output: Array.isArray(picked.output) ? picked.output : [],
|
||||
usage: picked.usage ?? usage ?? null,
|
||||
status: toString(picked.status, completed ? "completed" : "in_progress"),
|
||||
created_at: toNumber(picked.created_at, Math.floor(Date.now() / 1000)),
|
||||
metadata: asRecord(picked.metadata),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: `resp_${Date.now()}`,
|
||||
object: "response",
|
||||
model: fallbackModel || "unknown",
|
||||
output:
|
||||
textParts.length > 0
|
||||
? [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: textParts.join("") }],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
usage: usage ?? null,
|
||||
status: "completed",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
function buildClaudeSummary(events: StructuredSSEEvent[], fallbackModel?: string | null): unknown {
|
||||
const payloads = events
|
||||
.map((evt) => asRecord(evt.data))
|
||||
.filter((payload) => Object.keys(payload).length);
|
||||
if (payloads.length === 0) return null;
|
||||
|
||||
type ClaudeBlock =
|
||||
| { type: "text"; index: number; text: string }
|
||||
| { type: "thinking"; index: number; thinking: string; signature?: string }
|
||||
| {
|
||||
type: "tool_use";
|
||||
index: number;
|
||||
id: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
inputJson: string;
|
||||
};
|
||||
|
||||
const blocks = new Map<number, ClaudeBlock>();
|
||||
const usage: JsonRecord = {};
|
||||
let messageId = "";
|
||||
let model = fallbackModel || "claude";
|
||||
let role = "assistant";
|
||||
let stopReason = "end_turn";
|
||||
let stopSequence: string | null = null;
|
||||
|
||||
for (const payload of payloads) {
|
||||
const eventType = toString(payload.type);
|
||||
if (eventType === "message_start") {
|
||||
const message = asRecord(payload.message);
|
||||
messageId = toString(message.id, messageId || `msg_${Date.now()}`);
|
||||
model = toString(message.model, model);
|
||||
role = toString(message.role, role);
|
||||
mergeUsage(usage, message.usage);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "content_block_start") {
|
||||
const index = toNumber(payload.index, blocks.size);
|
||||
const contentBlock = asRecord(payload.content_block);
|
||||
const blockType = toString(contentBlock.type);
|
||||
|
||||
if (blockType === "thinking") {
|
||||
blocks.set(index, {
|
||||
type: "thinking",
|
||||
index,
|
||||
thinking: toString(contentBlock.thinking),
|
||||
signature:
|
||||
typeof contentBlock.signature === "string" ? contentBlock.signature : undefined,
|
||||
});
|
||||
} else if (blockType === "tool_use") {
|
||||
blocks.set(index, {
|
||||
type: "tool_use",
|
||||
index,
|
||||
id: toString(contentBlock.id, `toolu_${Date.now()}_${index}`),
|
||||
name: toString(contentBlock.name),
|
||||
input: cloneLogPayload(contentBlock.input ?? {}),
|
||||
inputJson: "",
|
||||
});
|
||||
} else {
|
||||
blocks.set(index, {
|
||||
type: "text",
|
||||
index,
|
||||
text: toString(contentBlock.text),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "content_block_delta") {
|
||||
const index = toNumber(payload.index, 0);
|
||||
const delta = asRecord(payload.delta);
|
||||
const deltaType = toString(delta.type);
|
||||
const existing = blocks.get(index);
|
||||
|
||||
if (deltaType === "input_json_delta") {
|
||||
const toolUse =
|
||||
existing && existing.type === "tool_use"
|
||||
? existing
|
||||
: {
|
||||
type: "tool_use" as const,
|
||||
index,
|
||||
id: `toolu_${Date.now()}_${index}`,
|
||||
name: "",
|
||||
input: {},
|
||||
inputJson: "",
|
||||
};
|
||||
toolUse.inputJson += toString(delta.partial_json);
|
||||
blocks.set(index, toolUse);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deltaType === "thinking_delta" || typeof delta.thinking === "string") {
|
||||
const thinking =
|
||||
existing && existing.type === "thinking"
|
||||
? existing
|
||||
: { type: "thinking" as const, index, thinking: "", signature: undefined };
|
||||
thinking.thinking += toString(delta.thinking);
|
||||
blocks.set(index, thinking);
|
||||
continue;
|
||||
}
|
||||
|
||||
const textBlock =
|
||||
existing && existing.type === "text"
|
||||
? existing
|
||||
: {
|
||||
type: "text" as const,
|
||||
index,
|
||||
text: "",
|
||||
};
|
||||
textBlock.text += toString(delta.text);
|
||||
blocks.set(index, textBlock);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventType === "message_delta") {
|
||||
const delta = asRecord(payload.delta);
|
||||
stopReason = toString(delta.stop_reason, stopReason);
|
||||
stopSequence =
|
||||
typeof delta.stop_sequence === "string" ? String(delta.stop_sequence) : stopSequence;
|
||||
mergeUsage(usage, payload.usage);
|
||||
continue;
|
||||
}
|
||||
|
||||
mergeUsage(usage, payload.usage);
|
||||
}
|
||||
|
||||
const content = [...blocks.values()]
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.flatMap((block) => {
|
||||
if (block.type === "text") {
|
||||
return block.text
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: block.text,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
if (block.type === "thinking") {
|
||||
return block.thinking
|
||||
? [
|
||||
{
|
||||
type: "thinking",
|
||||
thinking: block.thinking,
|
||||
...(block.signature ? { signature: block.signature } : {}),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
const parsedInput =
|
||||
block.inputJson.trim().length > 0
|
||||
? tryParseJson(block.inputJson)
|
||||
: cloneLogPayload(block.input);
|
||||
return [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
input: parsedInput,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
id: messageId || `msg_${Date.now()}`,
|
||||
type: "message",
|
||||
role,
|
||||
model,
|
||||
content,
|
||||
stop_reason: stopReason,
|
||||
...(stopSequence ? { stop_sequence: stopSequence } : {}),
|
||||
...(Object.keys(usage).length > 0 ? { usage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildGeminiSummary(events: StructuredSSEEvent[], fallbackModel?: string | null): unknown {
|
||||
const payloads = events
|
||||
.map((evt) => asRecord(evt.data))
|
||||
.filter((payload) => Object.keys(payload).length);
|
||||
if (payloads.length === 0) return null;
|
||||
|
||||
const parts: JsonRecord[] = [];
|
||||
const usageMetadata: JsonRecord = {};
|
||||
let modelVersion = fallbackModel || "gemini";
|
||||
let finishReason = "STOP";
|
||||
let role = "model";
|
||||
|
||||
const appendPart = (part: JsonRecord) => {
|
||||
const last = parts[parts.length - 1];
|
||||
if (
|
||||
last &&
|
||||
typeof last.text === "string" &&
|
||||
typeof part.text === "string" &&
|
||||
Boolean(last.thought) === Boolean(part.thought)
|
||||
) {
|
||||
last.text += part.text;
|
||||
return;
|
||||
}
|
||||
parts.push(part);
|
||||
};
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (typeof payload.modelVersion === "string" && payload.modelVersion.length > 0) {
|
||||
modelVersion = payload.modelVersion;
|
||||
}
|
||||
mergeUsage(usageMetadata, payload.usageMetadata);
|
||||
|
||||
const candidate = asRecord(Array.isArray(payload.candidates) ? payload.candidates[0] : null);
|
||||
if (typeof candidate.finishReason === "string" && candidate.finishReason.length > 0) {
|
||||
finishReason = candidate.finishReason;
|
||||
}
|
||||
|
||||
const content = asRecord(candidate.content);
|
||||
if (typeof content.role === "string" && content.role.length > 0) {
|
||||
role = content.role;
|
||||
}
|
||||
|
||||
if (!Array.isArray(content.parts)) continue;
|
||||
for (const item of content.parts) {
|
||||
const part = asRecord(item);
|
||||
if (part.functionCall && typeof part.functionCall === "object") {
|
||||
parts.push({
|
||||
functionCall: cloneLogPayload(part.functionCall),
|
||||
});
|
||||
} else if (typeof part.text === "string" && part.text.length > 0) {
|
||||
appendPart({
|
||||
text: part.text,
|
||||
...(part.thought === true ? { thought: true } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
candidates: [
|
||||
{
|
||||
index: 0,
|
||||
content: {
|
||||
role,
|
||||
parts,
|
||||
},
|
||||
finishReason,
|
||||
},
|
||||
],
|
||||
...(Object.keys(usageMetadata).length > 0 ? { usageMetadata } : {}),
|
||||
modelVersion,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildStreamSummaryFromEvents(
|
||||
events: StructuredSSEEvent[],
|
||||
fallbackFormat?: string | null,
|
||||
fallbackModel?: string | null
|
||||
): unknown {
|
||||
const format = inferFormatFromEvents(events, fallbackFormat);
|
||||
|
||||
switch (format) {
|
||||
case FORMATS.OPENAI_RESPONSES:
|
||||
return buildResponsesSummary(events, fallbackModel);
|
||||
case FORMATS.CLAUDE:
|
||||
return buildClaudeSummary(events, fallbackModel);
|
||||
case FORMATS.GEMINI:
|
||||
case FORMATS.GEMINI_CLI:
|
||||
case FORMATS.ANTIGRAVITY:
|
||||
return buildGeminiSummary(events, fallbackModel);
|
||||
default:
|
||||
return buildOpenAISummary(events, fallbackModel);
|
||||
}
|
||||
}
|
||||
|
||||
export function compactStructuredStreamPayload(payload: unknown): unknown {
|
||||
const record = asRecord(payload);
|
||||
if (record._streamed !== true || !("summary" in record)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const streamMeta: JsonRecord = {
|
||||
format: toString(record._format, "sse-json"),
|
||||
stage: toString(record._stage, "response"),
|
||||
eventCount: toNumber(record._eventCount, 0),
|
||||
};
|
||||
if (record._truncated === true) {
|
||||
streamMeta.truncated = true;
|
||||
}
|
||||
if (typeof record._droppedEvents === "number" && record._droppedEvents > 0) {
|
||||
streamMeta.droppedEvents = record._droppedEvents;
|
||||
}
|
||||
|
||||
const summary = cloneLogPayload(record.summary);
|
||||
if (summary && typeof summary === "object" && !Array.isArray(summary)) {
|
||||
return {
|
||||
...(summary as JsonRecord),
|
||||
_omniroute_stream: streamMeta,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
summary,
|
||||
_omniroute_stream: streamMeta,
|
||||
};
|
||||
}
|
||||
|
||||
export function createStructuredSSECollector(options: CollectorOptions = {}) {
|
||||
const { maxEvents = 200, maxBytes = 49152, stage } = options;
|
||||
const events: StructuredSSEEvent[] = [];
|
||||
let usedBytes = 0;
|
||||
let droppedEvents = 0;
|
||||
|
||||
return {
|
||||
push(payload: unknown, explicitEvent?: string) {
|
||||
if (payload === null || payload === undefined) return;
|
||||
|
||||
const event: StructuredSSEEvent = {
|
||||
index: events.length + droppedEvents,
|
||||
data: cloneLogPayload(payload),
|
||||
};
|
||||
|
||||
const eventName = explicitEvent || getEventName(payload);
|
||||
if (eventName) {
|
||||
event.event = eventName;
|
||||
}
|
||||
|
||||
const serializedSize = JSON.stringify(event).length;
|
||||
if (events.length >= maxEvents || usedBytes + serializedSize > maxBytes) {
|
||||
droppedEvents += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
usedBytes += serializedSize;
|
||||
events.push(event);
|
||||
},
|
||||
|
||||
getEvents() {
|
||||
return events.map((event) => cloneLogPayload(event));
|
||||
},
|
||||
|
||||
build(summary?: unknown, buildOptions: BuildOptions = {}) {
|
||||
const { includeEvents = true } = buildOptions;
|
||||
return {
|
||||
_streamed: true,
|
||||
_format: "sse-json",
|
||||
...(stage ? { _stage: stage } : {}),
|
||||
_eventCount: events.length + droppedEvents,
|
||||
...(droppedEvents > 0 ? { _truncated: true, _droppedEvents: droppedEvents } : {}),
|
||||
...(includeEvents ? { events } : {}),
|
||||
...(summary === undefined ? {} : { summary: cloneLogPayload(summary) }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Generated
+11
-11
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.1.9",
|
||||
"version": "3.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "3.1.9",
|
||||
"version": "3.2.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -6346,9 +6346,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7574,9 +7574,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15527,9 +15527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
|
||||
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.1.9",
|
||||
"version": "3.2.6",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -161,6 +161,7 @@
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"dompurify": "^3.3.2"
|
||||
"dompurify": "^3.3.2",
|
||||
"path-to-regexp": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export default function ApiManagerPageClient() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [usageStats, setUsageStats] = useState<Record<string, KeyUsageStats>>({});
|
||||
const [sessionCounts, setSessionCounts] = useState<Record<string, number>>({});
|
||||
const [allowKeyReveal, setAllowKeyReveal] = useState(false);
|
||||
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
@@ -150,6 +151,7 @@ export default function ApiManagerPageClient() {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setKeys(data.keys || []);
|
||||
setAllowKeyReveal(data.allowKeyReveal === true);
|
||||
// Fetch usage stats after keys are loaded
|
||||
fetchUsageStats(data.keys || []);
|
||||
fetchSessionCounts(data.keys || []);
|
||||
@@ -288,6 +290,25 @@ export default function ApiManagerPageClient() {
|
||||
setShowPermissionsModal(true);
|
||||
};
|
||||
|
||||
const handleCopyExistingKey = async (keyId: string) => {
|
||||
if (!keyId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/keys/${encodeURIComponent(keyId)}/reveal`);
|
||||
if (!res.ok) {
|
||||
console.log("Error revealing key:", await res.text());
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (typeof data?.key === "string") {
|
||||
await copy(data.key, `existing_key_${keyId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error copying existing key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePermissions = async (
|
||||
allowedModels: string[],
|
||||
noLog: boolean,
|
||||
@@ -560,12 +581,25 @@ export default function ApiManagerPageClient() {
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center gap-1.5">
|
||||
<code className="text-sm text-text-muted font-mono truncate">{key.key}</code>
|
||||
<span
|
||||
className="p-1 text-text-muted/40 opacity-0 group-hover:opacity-100 transition-all shrink-0 cursor-help"
|
||||
title={t("keyOnlyAvailableAtCreation")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">lock</span>
|
||||
</span>
|
||||
{allowKeyReveal ? (
|
||||
<button
|
||||
onClick={() => handleCopyExistingKey(key.id)}
|
||||
className="p-1 text-text-muted/60 hover:text-primary transition-colors shrink-0"
|
||||
title={tc("copy")}
|
||||
aria-label={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === `existing_key_${key.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="p-1 text-text-muted/40 opacity-0 group-hover:opacity-100 transition-all shrink-0 cursor-help"
|
||||
title={t("keyOnlyAvailableAtCreation")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">lock</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Button, EmptyState } from "@/shared/components";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SemanticCacheStats {
|
||||
memoryEntries: number;
|
||||
dbEntries: number;
|
||||
hits: number;
|
||||
misses: number;
|
||||
hitRate: string;
|
||||
tokensSaved: number;
|
||||
}
|
||||
|
||||
interface IdempotencyStats {
|
||||
activeKeys: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
interface CacheStats {
|
||||
semanticCache: SemanticCacheStats;
|
||||
idempotency: IdempotencyStats;
|
||||
}
|
||||
|
||||
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
valueClass = "text-text",
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub?: string;
|
||||
valueClass?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-4 rounded-xl bg-surface-raised border border-border/40">
|
||||
<div className="flex items-center gap-1.5 text-text-muted text-xs">
|
||||
<span className="material-symbols-outlined text-base leading-none" aria-hidden="true">
|
||||
{icon}
|
||||
</span>
|
||||
{label}
|
||||
</div>
|
||||
<div className={`text-2xl font-semibold tabular-nums ${valueClass}`}>{value}</div>
|
||||
{sub && <div className="text-xs text-text-muted">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HitRateBar({ hitRate, label }: { hitRate: number; label: string }) {
|
||||
const colorClass = hitRate >= 70 ? "bg-green-500" : hitRate >= 40 ? "bg-amber-400" : "bg-red-500";
|
||||
const textClass =
|
||||
hitRate >= 70 ? "text-green-500" : hitRate >= 40 ? "text-amber-400" : "text-red-500";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
role="progressbar"
|
||||
aria-label={label}
|
||||
aria-valuenow={hitRate}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<div className="flex justify-between text-xs mb-1.5">
|
||||
<span className="text-text-muted">{label}</span>
|
||||
<span className={`font-semibold tabular-nums ${textClass}`}>{hitRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-surface/50 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${colorClass}`}
|
||||
style={{ width: `${Math.min(hitRate, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ icon, children }: { icon: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-2 text-sm text-text-muted">
|
||||
<span
|
||||
className="material-symbols-outlined text-base leading-5 text-blue-400 shrink-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const REFRESH_INTERVAL_MS = 10_000;
|
||||
const REFRESH_INTERVAL_SECONDS = REFRESH_INTERVAL_MS / 1000;
|
||||
|
||||
export default function CachePage() {
|
||||
const t = useTranslations("cache");
|
||||
const [stats, setStats] = useState<CacheStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const notify = useNotificationStore();
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/cache");
|
||||
if (res.ok) {
|
||||
const data: CacheStats = await res.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Network error — keep stale stats rather than clearing the UI
|
||||
console.error("[CachePage] Failed to fetch cache stats:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStats();
|
||||
const id = setInterval(() => void fetchStats(), REFRESH_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchStats]);
|
||||
|
||||
const handleClearAll = async () => {
|
||||
setClearing(true);
|
||||
try {
|
||||
const res = await fetch("/api/cache", { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
notify.add({
|
||||
type: "success",
|
||||
message: t("clearSuccess", { count: data.expiredRemoved ?? 0 }),
|
||||
});
|
||||
await fetchStats();
|
||||
} else {
|
||||
notify.add({ type: "error", message: t("clearError") });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CachePage] Failed to clear cache:", error);
|
||||
notify.add({ type: "error", message: t("clearError") });
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sc = stats?.semanticCache;
|
||||
const idp = stats?.idempotency;
|
||||
const hitRate = sc ? parseFloat(sc.hitRate) : 0;
|
||||
const totalRequests = sc ? sc.hits + sc.misses : 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted mt-0.5">{t("description")}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="refresh"
|
||||
size="sm"
|
||||
onClick={() => void fetchStats()}
|
||||
disabled={loading}
|
||||
aria-label={t("refresh")}
|
||||
>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="delete_sweep"
|
||||
size="sm"
|
||||
onClick={() => void handleClearAll()}
|
||||
disabled={clearing || loading}
|
||||
loading={clearing}
|
||||
aria-label={t("clearAll")}
|
||||
>
|
||||
{t("clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loading skeleton */}
|
||||
{loading && (
|
||||
<div
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4"
|
||||
aria-busy="true"
|
||||
aria-label="Loading cache statistics"
|
||||
>
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-xl bg-surface-raised animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error / empty state */}
|
||||
{!loading && !stats && (
|
||||
<EmptyState
|
||||
icon="cached"
|
||||
title={t("unavailable")}
|
||||
description={t("unavailableDesc")}
|
||||
actionLabel={t("refresh")}
|
||||
onAction={() => void fetchStats()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
{!loading && stats && (
|
||||
<>
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
icon="memory"
|
||||
label={t("memoryEntries")}
|
||||
value={sc?.memoryEntries ?? 0}
|
||||
sub={t("memoryEntriesSub")}
|
||||
/>
|
||||
<StatCard
|
||||
icon="storage"
|
||||
label={t("dbEntries")}
|
||||
value={sc?.dbEntries ?? 0}
|
||||
sub={t("dbEntriesSub")}
|
||||
/>
|
||||
<StatCard
|
||||
icon="trending_up"
|
||||
label={t("cacheHits")}
|
||||
value={sc?.hits ?? 0}
|
||||
sub={t("cacheHitsSub", { total: totalRequests })}
|
||||
valueClass="text-green-500"
|
||||
/>
|
||||
<StatCard
|
||||
icon="token"
|
||||
label={t("tokensSaved")}
|
||||
value={(sc?.tokensSaved ?? 0).toLocaleString()}
|
||||
sub={t("tokensSavedSub")}
|
||||
valueClass="text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hit rate + breakdown */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-medium text-sm">{t("performance")}</h2>
|
||||
<span className="text-xs text-text-muted">
|
||||
{t("autoRefresh", { seconds: REFRESH_INTERVAL_SECONDS })}
|
||||
</span>
|
||||
</div>
|
||||
<HitRateBar hitRate={hitRate} label={t("hitRate")} />
|
||||
<div className="grid grid-cols-3 gap-4 pt-3 border-t border-border/30 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums text-green-500">
|
||||
{sc?.hits ?? 0}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("hits")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums text-red-400">
|
||||
{sc?.misses ?? 0}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("misses")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums">{totalRequests}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("total")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cache behavior */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-3">
|
||||
<h2 className="font-medium text-sm">{t("behavior")}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<InfoRow icon="info">{t("behaviorDeterministic")}</InfoRow>
|
||||
<InfoRow icon="info">
|
||||
{t.rich("behaviorBypass", {
|
||||
header: () => (
|
||||
<code className="bg-surface px-1 py-0.5 rounded text-xs font-mono">
|
||||
X-OmniRoute-No-Cache: true
|
||||
</code>
|
||||
),
|
||||
})}
|
||||
</InfoRow>
|
||||
<InfoRow icon="info">{t("behaviorTwoTier")}</InfoRow>
|
||||
<InfoRow icon="info">
|
||||
{t.rich("behaviorTtl", {
|
||||
envVar: () => (
|
||||
<code className="bg-surface px-1 py-0.5 rounded text-xs font-mono">
|
||||
SEMANTIC_CACHE_TTL_MS
|
||||
</code>
|
||||
),
|
||||
})}
|
||||
</InfoRow>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Idempotency */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="material-symbols-outlined text-base text-text-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
fingerprint
|
||||
</span>
|
||||
<h2 className="font-medium text-sm">{t("idempotency")}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 rounded-lg bg-surface/50">
|
||||
<div className="text-lg font-semibold tabular-nums">{idp?.activeKeys ?? 0}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("activeDedupKeys")}</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-surface/50">
|
||||
<div className="text-lg font-semibold tabular-nums">
|
||||
{idp ? `${(idp.windowMs / 1000).toFixed(0)}s` : "—"}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("dedupWindow")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1122,6 +1122,7 @@ function TestResultsView({ results }) {
|
||||
{results.results?.map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
title={r.error || undefined}
|
||||
className="flex items-center gap-2 text-xs px-2 py-1.5 rounded bg-black/[0.02] dark:bg-white/[0.02]"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -4289,6 +4289,7 @@ function AddApiKeyModal({
|
||||
const defaultBailianUrl = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";
|
||||
const isVertex = provider === "vertex";
|
||||
const defaultRegion = "us-central1";
|
||||
const isGlm = provider === "glm";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -4296,6 +4297,7 @@ function AddApiKeyModal({
|
||||
priority: 1,
|
||||
baseUrl: isBailian ? defaultBailianUrl : "",
|
||||
region: isVertex ? defaultRegion : "",
|
||||
apiRegion: "international",
|
||||
validationModelId: "",
|
||||
});
|
||||
const [validating, setValidating] = useState(false);
|
||||
@@ -4385,6 +4387,10 @@ function AddApiKeyModal({
|
||||
payload.providerSpecificData = {
|
||||
region: formData.region,
|
||||
};
|
||||
} else if (isGlm) {
|
||||
payload.providerSpecificData = {
|
||||
apiRegion: formData.apiRegion,
|
||||
};
|
||||
}
|
||||
|
||||
const error = await onSave(payload);
|
||||
@@ -4484,6 +4490,22 @@ function AddApiKeyModal({
|
||||
hint="ex: us-central1 ou europe-west4. Partner models usam a região global automaticamente."
|
||||
/>
|
||||
)}
|
||||
{isGlm && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>
|
||||
<select
|
||||
value={formData.apiRegion}
|
||||
onChange={(e) => setFormData({ ...formData, apiRegion: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="international">International (api.z.ai)</option>
|
||||
<option value="china">China Mainland (open.bigmodel.cn)</option>
|
||||
</select>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Select the endpoint region for API access and quota tracking.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -4533,6 +4555,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
healthCheckInterval: 60,
|
||||
baseUrl: "",
|
||||
region: "",
|
||||
apiRegion: "international",
|
||||
validationModelId: "",
|
||||
tag: "",
|
||||
});
|
||||
@@ -4548,6 +4571,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
const isBailian = connection?.provider === "bailian-coding-plan";
|
||||
const defaultBailianUrl = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";
|
||||
const isVertex = connection?.provider === "vertex";
|
||||
const isGlm = connection?.provider === "glm";
|
||||
const defaultRegion = "us-central1";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -4563,6 +4587,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
healthCheckInterval: connection.healthCheckInterval ?? 60,
|
||||
baseUrl: existingBaseUrl || (isBailian ? defaultBailianUrl : ""),
|
||||
region: existingRegion || (isVertex ? defaultRegion : ""),
|
||||
apiRegion: (connection.providerSpecificData?.apiRegion as string) || "international",
|
||||
validationModelId: (connection.providerSpecificData?.validationModelId as string) || "",
|
||||
tag: (connection.providerSpecificData?.tag as string) || "",
|
||||
});
|
||||
@@ -4698,6 +4723,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
updates.providerSpecificData.baseUrl = validatedBailianBaseUrl;
|
||||
} else if (isVertex) {
|
||||
updates.providerSpecificData.region = formData.region;
|
||||
} else if (isGlm) {
|
||||
updates.providerSpecificData.apiRegion = formData.apiRegion;
|
||||
}
|
||||
} else {
|
||||
// Also persist tag for OAuth accounts
|
||||
@@ -4832,6 +4859,23 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGlm && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>
|
||||
<select
|
||||
value={formData.apiRegion}
|
||||
onChange={(e) => setFormData({ ...formData, apiRegion: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="international">International (api.z.ai)</option>
|
||||
<option value="china">China Mainland (open.bigmodel.cn)</option>
|
||||
</select>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Select the endpoint region for API access and quota tracking.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* T07: Extra API Keys for round-robin rotation */}
|
||||
{!isOAuth && (
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -6,17 +6,35 @@ import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import useThemeStore, { COLOR_THEMES } from "@/store/themeStore";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
HIDDEN_SIDEBAR_ITEMS_SETTING_KEY,
|
||||
SIDEBAR_SECTIONS,
|
||||
SIDEBAR_SETTINGS_UPDATED_EVENT,
|
||||
normalizeHiddenSidebarItems,
|
||||
type HideableSidebarItemId,
|
||||
} from "@/shared/constants/sidebarVisibility";
|
||||
|
||||
export default function AppearanceTab() {
|
||||
const { theme, setTheme, isDark } = useTheme();
|
||||
const { colorTheme, customColor, setColorTheme, setCustomColorTheme } = useThemeStore();
|
||||
const t = useTranslations("settings");
|
||||
const tSidebar = useTranslations("sidebar");
|
||||
const [settings, setSettings] = useState<Record<string, any>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [customThemeColor, setCustomThemeColor] = useState(customColor || "#3b82f6");
|
||||
const isValidHex = /^#([0-9a-fA-F]{6})$/.test(customThemeColor.startsWith("#") ? customThemeColor : `#${customThemeColor}`);
|
||||
const isValidHex = /^#([0-9a-fA-F]{6})$/.test(
|
||||
customThemeColor.startsWith("#") ? customThemeColor : `#${customThemeColor}`
|
||||
);
|
||||
const hiddenSidebarItems = normalizeHiddenSidebarItems(
|
||||
settings[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]
|
||||
);
|
||||
const hiddenSidebarSet = new Set(hiddenSidebarItems);
|
||||
|
||||
const getSettingsLabel = (key: string, fallback: string) =>
|
||||
typeof t.has === "function" && t.has(key) ? t(key) : fallback;
|
||||
const getSidebarLabel = (key: string, fallback: string) =>
|
||||
typeof tSidebar.has === "function" && tSidebar.has(key) ? tSidebar(key) : fallback;
|
||||
|
||||
// Subscribe to store changes (e.g. from another tab) via Zustand external subscription
|
||||
useEffect(() => {
|
||||
const unsubscribe = useThemeStore.subscribe((state) => {
|
||||
if (state.customColor && state.customColor !== customThemeColor) {
|
||||
@@ -25,6 +43,7 @@ export default function AppearanceTab() {
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const themeOptionLabels: Record<string, string> = {
|
||||
light: t("themeLight"),
|
||||
dark: t("themeDark"),
|
||||
@@ -40,7 +59,12 @@ export default function AppearanceTab() {
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setSettings(data);
|
||||
setSettings({
|
||||
...data,
|
||||
[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]: normalizeHiddenSidebarItems(
|
||||
data[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]
|
||||
),
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
@@ -54,7 +78,18 @@ export default function AppearanceTab() {
|
||||
body: JSON.stringify({ [key]: value }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
[key]:
|
||||
key === HIDDEN_SIDEBAR_ITEMS_SETTING_KEY ? normalizeHiddenSidebarItems(value) : value,
|
||||
}));
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(SIDEBAR_SETTINGS_UPDATED_EVENT, {
|
||||
detail: { [key]: value },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to update ${key}:`, err);
|
||||
@@ -71,6 +106,20 @@ export default function AppearanceTab() {
|
||||
{ id: "cyan", color: COLOR_THEMES.cyan, label: t("themeCyan") },
|
||||
];
|
||||
|
||||
const sidebarSections = SIDEBAR_SECTIONS.map((section) => ({
|
||||
...section,
|
||||
title: getSidebarLabel(section.titleKey, section.titleFallback),
|
||||
items: section.items.map((item) => ({ ...item, label: tSidebar(item.i18nKey) })),
|
||||
}));
|
||||
|
||||
const toggleSidebarItem = (itemId: HideableSidebarItemId) => {
|
||||
const nextHiddenItems = hiddenSidebarSet.has(itemId)
|
||||
? hiddenSidebarItems.filter((id) => id !== itemId)
|
||||
: [...hiddenSidebarItems, itemId];
|
||||
|
||||
updateSetting(HIDDEN_SIDEBAR_ITEMS_SETTING_KEY, nextHiddenItems);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
@@ -164,10 +213,61 @@ export default function AppearanceTab() {
|
||||
maxLength={7}
|
||||
className={`flex-1 h-10 px-3 rounded-lg bg-surface border text-sm text-text-main focus:outline-none ${isValidHex ? "border-border focus:border-primary" : "border-red-400 focus:border-red-500"}`}
|
||||
/>
|
||||
<Button onClick={() => setCustomColorTheme(customThemeColor)} disabled={!isValidHex}>{t("themeCreate")}</Button>
|
||||
<Button onClick={() => setCustomColorTheme(customThemeColor)} disabled={!isValidHex}>
|
||||
{t("themeCreate")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<div className="mb-3">
|
||||
<p className="font-medium">
|
||||
{getSettingsLabel("sidebarVisibility", "Hide sidebar items")}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
{getSettingsLabel(
|
||||
"sidebarVisibilityDesc",
|
||||
"Hide any sidebar navigation entry to reduce visual clutter without disabling any features"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{sidebarSections.map((section) => (
|
||||
<div key={section.id} className="rounded-lg border border-border bg-surface/40">
|
||||
<div className="px-4 py-3 border-b border-border/70">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted/70">
|
||||
{section.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border/70">
|
||||
{section.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between gap-4 px-4 py-3"
|
||||
>
|
||||
<p className="font-medium">{item.label}</p>
|
||||
<Toggle
|
||||
checked={hiddenSidebarSet.has(item.id)}
|
||||
onChange={() => toggleSidebarItem(item.id)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-xs text-text-muted">
|
||||
{getSettingsLabel(
|
||||
"sidebarVisibilityHint",
|
||||
"Any sidebar section is hidden automatically when all of its entries are hidden"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@@ -4,7 +4,13 @@ import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { parseQuotaData, calculatePercentage, normalizePlanTier } from "./utils";
|
||||
import {
|
||||
parseQuotaData,
|
||||
calculatePercentage,
|
||||
formatQuotaLabel,
|
||||
normalizePlanTier,
|
||||
resolvePlanValue,
|
||||
} from "./utils";
|
||||
import Card from "@/shared/components/Card";
|
||||
import Badge from "@/shared/components/Badge";
|
||||
import { CardSkeleton } from "@/shared/components/Loading";
|
||||
@@ -16,6 +22,8 @@ const LS_EXPANDED_GROUPS = "omniroute:limits:expandedGroups";
|
||||
|
||||
const REFRESH_INTERVAL_MS = 120000;
|
||||
const MIN_FETCH_INTERVAL_MS = 30000; // Debounce per-connection fetches
|
||||
const QUOTA_BAR_GREEN_THRESHOLD = 50;
|
||||
const QUOTA_BAR_YELLOW_THRESHOLD = 20;
|
||||
|
||||
// Provider display config
|
||||
const PROVIDER_CONFIG = {
|
||||
@@ -40,33 +48,14 @@ const TIER_FILTERS = [
|
||||
{ key: "unknown", labelKey: "tierUnknown" },
|
||||
];
|
||||
|
||||
// Short model display names for quota bars
|
||||
function getShortModelName(name) {
|
||||
const map = {
|
||||
"gemini-3-pro-high": "G3 Pro",
|
||||
"gemini-3-pro-low": "G3 Pro Low",
|
||||
"gemini-3-flash": "G3 Flash",
|
||||
"gemini-2.5-flash": "G2.5 Flash",
|
||||
"claude-opus-4-6-thinking": "Opus 4.6 Tk",
|
||||
"claude-opus-4-5-thinking": "Opus 4.5 Tk",
|
||||
"claude-opus-4-5": "Opus 4.5",
|
||||
"claude-sonnet-4-5-thinking": "Sonnet 4.5 Tk",
|
||||
"claude-sonnet-4-5": "Sonnet 4.5",
|
||||
chat: "Chat",
|
||||
completions: "Completions",
|
||||
premium_interactions: "Premium",
|
||||
session: "Session",
|
||||
weekly: "Weekly",
|
||||
agentic_request: "Agentic",
|
||||
agentic_request_freetrial: "Agentic (Trial)",
|
||||
};
|
||||
return map[name] || name;
|
||||
}
|
||||
|
||||
// Get bar color based on remaining percentage
|
||||
function getBarColor(remaining) {
|
||||
if (remaining > 70) return { bar: "#22c55e", text: "#22c55e", bg: "rgba(34,197,94,0.12)" };
|
||||
if (remaining >= 30) return { bar: "#eab308", text: "#eab308", bg: "rgba(234,179,8,0.12)" };
|
||||
function getBarColor(remainingPercentage) {
|
||||
if (remainingPercentage > QUOTA_BAR_GREEN_THRESHOLD) {
|
||||
return { bar: "#22c55e", text: "#22c55e", bg: "rgba(34,197,94,0.12)" };
|
||||
}
|
||||
if (remainingPercentage > QUOTA_BAR_YELLOW_THRESHOLD) {
|
||||
return { bar: "#eab308", text: "#eab308", bg: "rgba(234,179,8,0.12)" };
|
||||
}
|
||||
return { bar: "#ef4444", text: "#ef4444", bg: "rgba(239,68,68,0.12)" };
|
||||
}
|
||||
|
||||
@@ -297,14 +286,22 @@ export default function ProviderLimits() {
|
||||
);
|
||||
}, [filteredConnections]);
|
||||
|
||||
const tierByConnection = useMemo(() => {
|
||||
const resolvedPlanByConnection = useMemo(() => {
|
||||
const out = {};
|
||||
for (const conn of sortedConnections) {
|
||||
out[conn.id] = normalizePlanTier(quotaData[conn.id]?.plan);
|
||||
out[conn.id] = resolvePlanValue(quotaData[conn.id]?.plan, conn.providerSpecificData);
|
||||
}
|
||||
return out;
|
||||
}, [sortedConnections, quotaData]);
|
||||
|
||||
const tierByConnection = useMemo(() => {
|
||||
const out = {};
|
||||
for (const conn of sortedConnections) {
|
||||
out[conn.id] = normalizePlanTier(resolvedPlanByConnection[conn.id]);
|
||||
}
|
||||
return out;
|
||||
}, [sortedConnections, resolvedPlanByConnection]);
|
||||
|
||||
const tierCounts = useMemo(() => {
|
||||
const counts = {
|
||||
all: sortedConnections.length,
|
||||
@@ -313,6 +310,7 @@ export default function ProviderLimits() {
|
||||
business: 0,
|
||||
ultra: 0,
|
||||
pro: 0,
|
||||
plus: 0,
|
||||
free: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
@@ -533,6 +531,7 @@ export default function ProviderLimits() {
|
||||
color: "#666",
|
||||
};
|
||||
const tierMeta = tierByConnection[conn.id] || normalizePlanTier(null);
|
||||
const resolvedPlan = resolvedPlanByConnection[conn.id];
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -558,21 +557,29 @@ export default function ProviderLimits() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[13px] font-semibold text-text-main truncate">
|
||||
{conn.name || config.label}
|
||||
{conn.name || conn.displayName || conn.email || config.label}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<div className="flex items-center gap-1.5 mt-1 min-h-5">
|
||||
<span
|
||||
title={
|
||||
quota?.plan
|
||||
? t("rawPlanWithValue", { plan: quota.plan })
|
||||
resolvedPlan
|
||||
? t("rawPlanWithValue", { plan: resolvedPlan })
|
||||
: t("noPlanFromProvider")
|
||||
}
|
||||
className="inline-flex items-center shrink-0"
|
||||
>
|
||||
<Badge variant={tierMeta.variant} size="sm" dot>
|
||||
<Badge
|
||||
variant={tierMeta.variant}
|
||||
size="sm"
|
||||
dot
|
||||
className="h-5 leading-none"
|
||||
>
|
||||
{tierMeta.label}
|
||||
</Badge>
|
||||
</span>
|
||||
<span className="text-[11px] text-text-muted">{config.label}</span>
|
||||
<span className="text-[11px] leading-none text-text-muted">
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,17 +604,19 @@ export default function ProviderLimits() {
|
||||
<div className="text-xs text-text-muted italic">{quota.message}</div>
|
||||
) : quota?.quotas?.length > 0 ? (
|
||||
quota.quotas.map((q, i) => {
|
||||
const remaining =
|
||||
q.remainingPercentage !== undefined
|
||||
? Math.round(q.remainingPercentage)
|
||||
: calculatePercentage(q.used, q.total);
|
||||
const colors = getBarColor(remaining);
|
||||
const remainingPercentage = calculatePercentage(q.used, q.total);
|
||||
const colors = getBarColor(remainingPercentage);
|
||||
const cd = formatCountdown(q.resetAt);
|
||||
const shortName = getShortModelName(q.name);
|
||||
const shortName = formatQuotaLabel(q.name);
|
||||
const staleAfterReset = q.staleAfterReset === true;
|
||||
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-1.5 min-w-[200px] shrink-0">
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center gap-1.5 min-w-[200px] shrink-0 ${
|
||||
i > 0 ? "border-l border-border/80 pl-3 ml-1" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Model label */}
|
||||
<span
|
||||
className="text-[11px] font-semibold py-0.5 px-2 rounded whitespace-nowrap min-w-[60px] text-center"
|
||||
@@ -632,7 +641,7 @@ export default function ProviderLimits() {
|
||||
<div
|
||||
className="h-full rounded-sm transition-[width] duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.min(remaining, 100)}%`,
|
||||
width: `${Math.min(remainingPercentage, 100)}%`,
|
||||
background: colors.bar,
|
||||
}}
|
||||
/>
|
||||
@@ -643,7 +652,7 @@ export default function ProviderLimits() {
|
||||
className="text-[11px] font-semibold min-w-[32px] text-right"
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
{remaining}%
|
||||
{remainingPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,81 @@
|
||||
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { safePercentage } from "@/shared/utils/formatting";
|
||||
|
||||
const PROVIDER_PLAN_FALLBACKS = new Set([
|
||||
"claude code",
|
||||
"kimi coding",
|
||||
"kiro",
|
||||
"openai codex",
|
||||
"codex",
|
||||
"github copilot",
|
||||
]);
|
||||
|
||||
const QUOTA_LABEL_MAP: Record<string, string> = {
|
||||
"gemini-3-pro-high": "G3 Pro",
|
||||
"gemini-3-pro-low": "G3 Pro Low",
|
||||
"gemini-3-flash": "G3 Flash",
|
||||
"gemini-2.5-flash": "G2.5 Flash",
|
||||
"claude-opus-4-6-thinking": "Opus 4.6 Tk",
|
||||
"claude-opus-4-5-thinking": "Opus 4.5 Tk",
|
||||
"claude-opus-4-5": "Opus 4.5",
|
||||
"claude-sonnet-4-5-thinking": "Sonnet 4.5 Tk",
|
||||
"claude-sonnet-4-5": "Sonnet 4.5",
|
||||
chat: "Chat",
|
||||
completions: "Completions",
|
||||
premium_interactions: "Premium",
|
||||
session: "Session",
|
||||
weekly: "Weekly",
|
||||
code_review: "Code Review",
|
||||
agentic_request: "Agentic",
|
||||
agentic_request_freetrial: "Agentic (Trial)",
|
||||
};
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function normalizePlanCandidate(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.toLowerCase() === "unknown") return null;
|
||||
if (PROVIDER_PLAN_FALLBACKS.has(trimmed.toLowerCase())) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function toTitleCaseWords(value: string) {
|
||||
return value
|
||||
.split(/[\s_-]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function formatQuotaLabel(name: string) {
|
||||
const trimmed = typeof name === "string" ? name.trim() : "";
|
||||
if (!trimmed) return "";
|
||||
|
||||
const mapped = QUOTA_LABEL_MAP[trimmed];
|
||||
if (mapped) return mapped;
|
||||
|
||||
if (/^session\s*\(\d+[hm]\)$/i.test(trimmed)) {
|
||||
return "Session";
|
||||
}
|
||||
|
||||
if (/^weekly\s*\(\d+d\)$/i.test(trimmed)) {
|
||||
return "Weekly";
|
||||
}
|
||||
|
||||
const weeklyModelMatch = trimmed.match(/^weekly\s+(.+?)\s*\(\d+d\)$/i);
|
||||
if (weeklyModelMatch) {
|
||||
return `Weekly ${toTitleCaseWords(weeklyModelMatch[1])}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit)
|
||||
* @param {string|Date} date - ISO date string or Date object
|
||||
@@ -210,9 +285,32 @@ export function parseQuotaData(provider, data) {
|
||||
return normalizedQuotas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best available plan label using live usage first, then persisted
|
||||
* provider-specific connection metadata.
|
||||
*/
|
||||
export function resolvePlanValue(plan, providerSpecificData) {
|
||||
const psd = toRecord(providerSpecificData);
|
||||
const candidates = [
|
||||
plan,
|
||||
psd.workspacePlanType,
|
||||
psd.plan,
|
||||
psd.subscription,
|
||||
psd.tier,
|
||||
psd.accountTier,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizePlanCandidate(candidate);
|
||||
if (normalized) return normalized;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize provider-specific plan labels into a shared tier taxonomy.
|
||||
* Supported tiers: enterprise, business, team, ultra, pro, free, unknown.
|
||||
* Supported tiers: enterprise, business, team, ultra, pro, plus, free, unknown.
|
||||
*/
|
||||
export function normalizePlanTier(plan) {
|
||||
const raw = typeof plan === "string" ? plan.trim() : "";
|
||||
@@ -223,12 +321,12 @@ export function normalizePlanTier(plan) {
|
||||
const upper = raw.toUpperCase();
|
||||
|
||||
// Provider names that are not real plan tiers — treat as unknown
|
||||
if (upper === "CLAUDE CODE" || upper === "KIMI CODING" || upper === "KIRO") {
|
||||
return { key: "unknown", label: raw, variant: "default", rank: 0, raw };
|
||||
if (PROVIDER_PLAN_FALLBACKS.has(raw.toLowerCase())) {
|
||||
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("PRO+") || upper.includes("PRO PLUS") || upper.includes("PROPLUS")) {
|
||||
return { key: "plus", label: "Pro+", variant: "secondary", rank: 4, raw };
|
||||
return { key: "plus", label: "Pro+", variant: "success", rank: 4, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("ENTERPRISE") || upper.includes("CORP") || upper.includes("ORG")) {
|
||||
@@ -245,7 +343,7 @@ export function normalizePlanTier(plan) {
|
||||
}
|
||||
|
||||
if (upper.includes("STUDENT")) {
|
||||
return { key: "pro", label: "Student", variant: "primary", rank: 3, raw };
|
||||
return { key: "pro", label: "Student", variant: "success", rank: 3, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("ULTRA")) {
|
||||
@@ -253,11 +351,11 @@ export function normalizePlanTier(plan) {
|
||||
}
|
||||
|
||||
if (upper.includes("PRO") || upper.includes("PREMIUM")) {
|
||||
return { key: "pro", label: "Pro", variant: "primary", rank: 3, raw };
|
||||
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("PLUS") || upper.includes("PAID")) {
|
||||
return { key: "plus", label: "Plus", variant: "secondary", rank: 2, raw };
|
||||
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Vendored
+67
-8
@@ -1,7 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCacheStats, clearCache, cleanExpiredEntries } from "@/lib/semanticCache";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getCacheStats,
|
||||
clearCache,
|
||||
cleanExpiredEntries,
|
||||
invalidateByModel,
|
||||
invalidateBySignature,
|
||||
invalidateStale,
|
||||
} from "@/lib/semanticCache";
|
||||
import { getIdempotencyStats } from "@/lib/idempotencyLayer";
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/cache — Cache statistics
|
||||
*/
|
||||
@@ -15,19 +26,67 @@ export async function GET() {
|
||||
idempotency: idempotencyStats,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/cache — Clear all caches
|
||||
* DELETE /api/cache — Clear all caches or targeted invalidation.
|
||||
*
|
||||
* Exactly one optional query parameter may be provided:
|
||||
* ?model=<name> — invalidate all entries for a specific model
|
||||
* ?signature=<hex> — invalidate a single entry by its SHA-256 signature
|
||||
* ?staleMs=<number> — invalidate entries older than N milliseconds
|
||||
* (no params) — clear all cache entries
|
||||
*
|
||||
* Providing more than one parameter returns 400 Bad Request.
|
||||
*/
|
||||
export async function DELETE() {
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const model = searchParams.get("model");
|
||||
const signature = searchParams.get("signature");
|
||||
const staleMsParam = searchParams.get("staleMs");
|
||||
|
||||
// Enforce mutual exclusivity — only one invalidation mode per request
|
||||
const paramCount = [model, signature, staleMsParam].filter(Boolean).length;
|
||||
if (paramCount > 1) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"Only one invalidation parameter (model, signature, or staleMs) may be provided per request.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (model) {
|
||||
const removed = invalidateByModel(model);
|
||||
return NextResponse.json({ ok: true, invalidated: removed, scope: "model", model });
|
||||
}
|
||||
|
||||
if (signature) {
|
||||
const removed = invalidateBySignature(signature);
|
||||
return NextResponse.json({ ok: true, invalidated: removed ? 1 : 0, scope: "signature" });
|
||||
}
|
||||
|
||||
if (staleMsParam) {
|
||||
const maxAgeMs = parseInt(staleMsParam, 10);
|
||||
if (Number.isNaN(maxAgeMs) || maxAgeMs <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "staleMs must be a positive integer (milliseconds)." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const removed = invalidateStale(maxAgeMs);
|
||||
return NextResponse.json({ ok: true, invalidated: removed, scope: "stale", maxAgeMs });
|
||||
}
|
||||
|
||||
// Full clear
|
||||
clearCache();
|
||||
const cleaned = cleanExpiredEntries();
|
||||
return NextResponse.json({ ok: true, expiredRemoved: cleaned });
|
||||
const expiredRemoved = cleanExpiredEntries();
|
||||
return NextResponse.json({ ok: true, expiredRemoved, scope: "all" });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildComboTestRequestBody, extractComboTestResponseText } from "@/lib/combos/testHealth";
|
||||
import { getComboByName } from "@/lib/localDb";
|
||||
import { testComboSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
/**
|
||||
* POST /api/combos/test - Quick test a combo
|
||||
* Sends a minimal request through each model in the combo to verify availability
|
||||
* Sends a real chat completion request through each model in the combo
|
||||
* and only reports success when the model returns usable text content.
|
||||
*/
|
||||
export async function POST(request) {
|
||||
let rawBody;
|
||||
@@ -48,38 +50,55 @@ export async function POST(request) {
|
||||
for (const modelStr of models) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// Send a minimal chat request to the internal SSE handler
|
||||
// Use OpenAI-compatible format — universally accepted by all providers via the translator
|
||||
const testBody = {
|
||||
model: modelStr,
|
||||
messages: [{ role: "user", content: "Hi" }],
|
||||
max_tokens: 5,
|
||||
stream: false,
|
||||
};
|
||||
// Send a minimal but real chat request through the same internal
|
||||
// endpoint an external OpenAI-compatible client would use.
|
||||
const testBody = buildComboTestRequestBody(modelStr);
|
||||
|
||||
const internalUrl = `${getBaseUrl(request)}/v1/chat/completions`;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 20000); // 20s timeout (was 15s, slow providers need more)
|
||||
const timeout = setTimeout(() => controller.abort(), 20000);
|
||||
|
||||
const res = await fetch(internalUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Fix #350: bypass REQUIRE_API_KEY for internal admin combo tests
|
||||
"X-Internal-Test": "combo-health-check",
|
||||
},
|
||||
body: JSON.stringify(testBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(internalUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Internal dashboard tests still use the normal /v1 pipeline but
|
||||
// bypass REQUIRE_API_KEY so admins can test with local session auth.
|
||||
"X-Internal-Test": "combo-health-check",
|
||||
},
|
||||
body: JSON.stringify(testBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
const latencyMs = Date.now() - startTime;
|
||||
|
||||
if (res.ok) {
|
||||
results.push({ model: modelStr, status: "ok", latencyMs });
|
||||
let responseBody = null;
|
||||
try {
|
||||
responseBody = await res.json();
|
||||
} catch {
|
||||
responseBody = null;
|
||||
}
|
||||
|
||||
const responseText = extractComboTestResponseText(responseBody);
|
||||
if (!responseText) {
|
||||
results.push({
|
||||
model: modelStr,
|
||||
status: "error",
|
||||
statusCode: res.status,
|
||||
error: "Provider returned HTTP 200 but no text content.",
|
||||
latencyMs,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({ model: modelStr, status: "ok", latencyMs, responseText });
|
||||
if (!resolvedBy) resolvedBy = modelStr;
|
||||
// For test, we can stop after first success (like a real combo would)
|
||||
// But let's test all models to show full health
|
||||
} else {
|
||||
let errorMsg = "";
|
||||
try {
|
||||
@@ -88,6 +107,7 @@ export async function POST(request) {
|
||||
} catch {
|
||||
errorMsg = res.statusText;
|
||||
}
|
||||
|
||||
results.push({
|
||||
model: modelStr,
|
||||
status: "error",
|
||||
@@ -101,7 +121,7 @@ export async function POST(request) {
|
||||
results.push({
|
||||
model: modelStr,
|
||||
status: "error",
|
||||
error: error.name === "AbortError" ? "Timeout (15s)" : error.message,
|
||||
error: error.name === "AbortError" ? "Timeout (20s)" : error.message,
|
||||
latencyMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeyById } from "@/lib/localDb";
|
||||
import { isApiKeyRevealEnabled } from "@/lib/apiKeyExposure";
|
||||
|
||||
// GET /api/keys/[id]/reveal - Reveal full API key for explicit copy actions
|
||||
export async function GET(_request, { params }) {
|
||||
try {
|
||||
if (!isApiKeyRevealEnabled()) {
|
||||
return NextResponse.json({ error: "API key reveal is disabled" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const key = await getApiKeyById(id);
|
||||
|
||||
if (!key || typeof key.key !== "string") {
|
||||
return NextResponse.json({ error: "Key not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ key: key.key });
|
||||
} catch (error) {
|
||||
console.log("Error revealing key:", error);
|
||||
return NextResponse.json({ error: "Failed to reveal key" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,17 @@ import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { createKeySchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { isApiKeyRevealEnabled, maskStoredApiKey } from "@/lib/apiKeyExposure";
|
||||
|
||||
// GET /api/keys - List API keys
|
||||
export async function GET() {
|
||||
try {
|
||||
const keys = await getApiKeys();
|
||||
// Mask key values — users should never see full keys after creation
|
||||
const maskedKeys = keys.map((k) => ({
|
||||
...k,
|
||||
key: typeof k.key === "string" ? k.key.slice(0, 8) + "****" + k.key.slice(-4) : null,
|
||||
key: maskStoredApiKey(k.key),
|
||||
}));
|
||||
return NextResponse.json({ keys: maskedKeys });
|
||||
return NextResponse.json({ keys: maskedKeys, allowKeyReveal: isApiKeyRevealEnabled() });
|
||||
} catch (error) {
|
||||
console.log("Error fetching keys:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch keys" }, { status: 500 });
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* GET /api/logs/detail — List detailed request logs
|
||||
* GET /api/logs/detail/:id — Get specific detailed log
|
||||
* POST /api/logs/detail/toggle — Enable/disable detailed logging
|
||||
* GET /api/logs/detail — List detailed request logs + current enabled flag
|
||||
* POST /api/logs/detail — Enable/disable detailed logging
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import {
|
||||
getRequestDetailLogs,
|
||||
getRequestDetailLogCount,
|
||||
@@ -15,9 +14,8 @@ import { updateSettings } from "@/lib/db/settings";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const authError = await requireManagementAuth(req);
|
||||
if (authError) return authError;
|
||||
|
||||
const url = new URL(req.url);
|
||||
const limit = Math.min(Number(url.searchParams.get("limit") ?? 50), 200);
|
||||
@@ -31,9 +29,8 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const authError = await requireManagementAuth(req);
|
||||
if (authError) return authError;
|
||||
|
||||
const body = await req.json();
|
||||
const enabled = body.enabled === true || body.enabled === "1";
|
||||
|
||||
@@ -3,6 +3,10 @@ import { getProviderConnectionById } from "@/models";
|
||||
import { replaceCustomModels } from "@/lib/db/models";
|
||||
import { saveCallLog } from "@/lib/usage/callLogs";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
buildModelSyncInternalHeaders,
|
||||
isModelSyncInternalRequest,
|
||||
} from "@/shared/services/modelSyncScheduler";
|
||||
|
||||
/**
|
||||
* POST /api/providers/[id]/sync-models
|
||||
@@ -19,7 +23,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
if (!(await isAuthenticated(request)) && !isModelSyncInternalRequest(request)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Authentication required", type: "invalid_api_key" } },
|
||||
{ status: 401 }
|
||||
@@ -41,7 +45,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
method: "GET",
|
||||
headers: {
|
||||
cookie: request.headers.get("cookie") || "",
|
||||
"x-internal": "model-sync",
|
||||
...buildModelSyncInternalHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -126,11 +126,23 @@ export async function GET(
|
||||
return Response.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Only OAuth connections have usage APIs
|
||||
if (connection.authType !== "oauth") {
|
||||
// Only OAuth connections and specific API key providers have usage APIs
|
||||
const apikeyUsageProviders = ["glm"];
|
||||
if (connection.authType !== "oauth" && !apikeyUsageProviders.includes(connection.provider)) {
|
||||
return Response.json({ message: "Usage not available for API key connections" });
|
||||
}
|
||||
|
||||
// API key providers skip OAuth refresh — call usage fetcher directly
|
||||
if (connection.authType !== "oauth") {
|
||||
try {
|
||||
const usageData = await getUsageForProvider(connection);
|
||||
return Response.json(usageData);
|
||||
} catch (error) {
|
||||
console.error("[Usage API] Error fetching usage:", error);
|
||||
return Response.json({ error: (error as any).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve proxy for this connection FIRST (key → combo → provider → global → direct)
|
||||
// so that both credential refresh AND usage fetch go through the proxy.
|
||||
const proxyInfo = await resolveProxyForConnection(connectionId);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getCallLogById } from "@/lib/usageDb";
|
||||
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const { id } = await params;
|
||||
const log = await getCallLogById(id);
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getCallLogs } from "@/lib/usageDb";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const filter: Record<string, any> = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CORS_ORIGIN, CORS_HEADERS } from "@/shared/utils/cors";
|
||||
import { handleChat } from "@/sse/handlers/chat";
|
||||
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
|
||||
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
|
||||
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function POST(request: Request) {
|
||||
headers: request.headers,
|
||||
body: JSON.stringify(normalized),
|
||||
});
|
||||
return await handleChat(newRequest);
|
||||
return await handleChat(newRequest, buildClientRawRequest(request, body));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
||||
import { handleChat } from "@/sse/handlers/chat";
|
||||
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
|
||||
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
@@ -91,5 +91,5 @@ export async function POST(request, { params }) {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return await handleChat(newRequest);
|
||||
return await handleChat(newRequest, buildClientRawRequest(request, rawBody));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
||||
import { handleChat } from "@/sse/handlers/chat";
|
||||
import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
|
||||
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
|
||||
import { v1betaGeminiGenerateSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
@@ -87,7 +87,7 @@ export async function POST(request, { params }) {
|
||||
body: JSON.stringify(convertedBody),
|
||||
});
|
||||
|
||||
return await handleChat(newRequest);
|
||||
return await handleChat(newRequest, buildClientRawRequest(request, rawBody));
|
||||
} catch (error) {
|
||||
console.log("Error handling Gemini request:", error);
|
||||
return Response.json({ error: { message: error.message, code: 500 } }, { status: 500 });
|
||||
|
||||
+14
-1
@@ -219,7 +219,7 @@ body {
|
||||
|
||||
/* Material Symbols */
|
||||
.material-symbols-outlined {
|
||||
font-family: "Material Symbols Outlined", sans-serif;
|
||||
font-family: "Material Symbols Outlined", sans-serif !important;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
@@ -337,3 +337,16 @@ button .material-symbols-outlined,
|
||||
.traffic-light.green {
|
||||
background: var(--color-traffic-green);
|
||||
}
|
||||
|
||||
/* ── Mobile Layout Fixes (Issue #659) ── */
|
||||
@media (max-width: 768px) {
|
||||
.ant-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
min-width: 600px; /* Prevent columns from crushing together */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
* @property {string} [instanceName] - Instance display name
|
||||
* @property {string} [corsOrigins] - Allowed CORS origins
|
||||
* @property {number} [logRetentionDays] - Log retention in days
|
||||
* @property {string[]} [hiddenSidebarItems] - Sidebar entry ids hidden for visual decluttering
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "وكلاء",
|
||||
"cliToolsShort": "أدوات",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "المواضيع",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "انا بحاجة الى مساعدة مع",
|
||||
"userFollowUp": "هل يمكنك توضيح ذلك؟"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Агенти",
|
||||
"cliToolsShort": "Инструменти",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Теми",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Имам нужда от помощ за",
|
||||
"userFollowUp": "Можете ли да разкажете по-подробно за това?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "Fialová",
|
||||
"themeOrange": "Oranžová",
|
||||
"themeCyan": "Azurová",
|
||||
"cliToolsShort": "Nástroje"
|
||||
"cliToolsShort": "Nástroje",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Motivy",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Potřebuji pomoct",
|
||||
"userFollowUp": "Můžete to upřesnit?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenter",
|
||||
"cliToolsShort": "Værktøjer",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Temaer",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Jeg har brug for hjælp til",
|
||||
"userFollowUp": "Kan du uddybe det?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenten",
|
||||
"cliToolsShort": "Werkzeuge",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themen",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ich brauche Hilfe dabei",
|
||||
"userFollowUp": "Können Sie das näher erläutern?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,11 @@
|
||||
"debug": "Debug",
|
||||
"system": "System",
|
||||
"help": "Help",
|
||||
"primarySection": "Main",
|
||||
"cliSection": "CLI",
|
||||
"debugSection": "Debug",
|
||||
"systemSection": "System",
|
||||
"helpSection": "Help",
|
||||
"serverDisconnected": "Server Disconnected",
|
||||
"serverDisconnectedMsg": "The proxy server has been stopped or is restarting.",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
@@ -185,7 +190,9 @@
|
||||
"themeViolet": "Violet",
|
||||
"themeOrange": "Orange",
|
||||
"themeCyan": "Cyan",
|
||||
"cliToolsShort": "Tools"
|
||||
"cliToolsShort": "Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -1722,6 +1729,9 @@
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"themeSystem": "System",
|
||||
"sidebarVisibility": "Hide sidebar items",
|
||||
"sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features",
|
||||
"sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden",
|
||||
"hideHealthLogs": "Hide Health Check Logs",
|
||||
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
|
||||
"themeAccent": "Theme color",
|
||||
@@ -2849,5 +2859,37 @@
|
||||
"userInitial": "I need help with",
|
||||
"userFollowUp": "Can you elaborate on that?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntries": "DB Entries",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHits": "Cache Hits",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behavior": "Cache Behavior",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Herramientas",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Temas",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "necesito ayuda con",
|
||||
"userFollowUp": "¿Puedes dar más detalles sobre eso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentit",
|
||||
"cliToolsShort": "Työkalut",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Teemat",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Tarvitsen apua",
|
||||
"userFollowUp": "Voitko tarkentaa sitä?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agents",
|
||||
"cliToolsShort": "Outils",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Thèmes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "J'ai besoin d'aide pour",
|
||||
"userFollowUp": "Pouvez-vous développer cela ?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "סוכנים",
|
||||
"cliToolsShort": "כלים",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "אני צריך עזרה עם",
|
||||
"userFollowUp": "אתה יכול לפרט על זה?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@
|
||||
"agents": "एजेंट",
|
||||
"cliToolsShort": "उपकरण",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2689,5 +2691,37 @@
|
||||
"domainPlaceholder": "example.com",
|
||||
"requestTimedOut": "Request timed out ({seconds}s)",
|
||||
"networkError": "Network error"
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Ügynökök",
|
||||
"cliToolsShort": "Eszközök",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Témák",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Segítségre van szükségem",
|
||||
"userFollowUp": "Kifejtenéd ezt bővebben?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agen",
|
||||
"cliToolsShort": "Alat",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Saya butuh bantuan",
|
||||
"userFollowUp": "Bisakah Anda menjelaskannya lebih lanjut?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "बैंगनी",
|
||||
"themeOrange": "नारंगी",
|
||||
"themeCyan": "सियान",
|
||||
"cliToolsShort": "उपकरण"
|
||||
"cliToolsShort": "उपकरण",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "थीम्स",
|
||||
@@ -2843,5 +2845,37 @@
|
||||
"userInitial": "मुझे मदद चाहिए",
|
||||
"userFollowUp": "क्या आप इसके बारे में विस्तार से बता सकते हैं?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenti",
|
||||
"cliToolsShort": "Strumenti",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ho bisogno di aiuto con",
|
||||
"userFollowUp": "Puoi approfondire questo argomento?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "エージェント",
|
||||
"cliToolsShort": "ツール",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "助けが必要です",
|
||||
"userFollowUp": "それについて詳しく教えてもらえますか?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "에이전트",
|
||||
"cliToolsShort": "도구",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "도움이 필요해요",
|
||||
"userFollowUp": "그것에 대해 자세히 설명해주실 수 있나요?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Ejen",
|
||||
"cliToolsShort": "Alat",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Saya perlukan bantuan",
|
||||
"userFollowUp": "Bolehkah anda menghuraikan perkara itu?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenten",
|
||||
"cliToolsShort": "Gereedschap",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ik heb hulp nodig bij",
|
||||
"userFollowUp": "Kunt u dat nader toelichten?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenter",
|
||||
"cliToolsShort": "Verktøy",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Jeg trenger hjelp med",
|
||||
"userFollowUp": "Kan du utdype det?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Mga Agent",
|
||||
"cliToolsShort": "Mga Tool",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Kailangan ko ng tulong sa",
|
||||
"userFollowUp": "Maaari mo bang ipaliwanag iyon?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenci",
|
||||
"cliToolsShort": "Narzędzia",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Potrzebuję pomocy",
|
||||
"userFollowUp": "Możesz to rozwinąć?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"playground": "Playground",
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Ferramentas",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2865,5 +2867,37 @@
|
||||
"userInitial": "preciso de ajuda com",
|
||||
"userFollowUp": "Você pode explicar isso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Ferramentas",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "preciso de ajuda com",
|
||||
"userFollowUp": "Você pode explicar isso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenți",
|
||||
"cliToolsShort": "Instrumente",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Am nevoie de ajutor cu",
|
||||
"userFollowUp": "Puteți detalia asta?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Агенты",
|
||||
"cliToolsShort": "Инструменты",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Темы",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "мне нужна помощь с",
|
||||
"userFollowUp": "Можете ли вы рассказать об этом подробнее?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user