mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 23:52:18 +03:00
Compare commits
174 Commits
fix/releas
...
fix/codex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec5e1708a | ||
|
|
0f4a92718c | ||
|
|
527da6565d | ||
|
|
d077e88456 | ||
|
|
00c80fd14a | ||
|
|
7c2dba0b9b | ||
|
|
92a083ab8c | ||
|
|
5b92dbded1 | ||
|
|
5cf16028fe | ||
|
|
b5e4c2c0ce | ||
|
|
af90cb7f9b | ||
|
|
578db866a4 | ||
|
|
e32b9264e8 | ||
|
|
a55ee5dc05 | ||
|
|
e0a22ff619 | ||
|
|
162ef913da | ||
|
|
158c6ec233 | ||
|
|
34463f6b36 | ||
|
|
a054ac408f | ||
|
|
7fdd2e0f2a | ||
|
|
b81f2b646a | ||
|
|
a7e09eda5c | ||
|
|
855243ab18 | ||
|
|
8f390efffd | ||
|
|
968fa96105 | ||
|
|
c8369193c1 | ||
|
|
90ba281e7f | ||
|
|
84bd382d94 | ||
|
|
8d59ab363b | ||
|
|
fb421bc580 | ||
|
|
3d8448b45e | ||
|
|
e66181b182 | ||
|
|
7af14d2696 | ||
|
|
1135cbea80 | ||
|
|
6e9553eb93 | ||
|
|
0b41259f39 | ||
|
|
0fe1bb2390 | ||
|
|
98289a8c99 | ||
|
|
2264cffe40 | ||
|
|
8a42aeebb8 | ||
|
|
35a34ac34f | ||
|
|
ef8414b022 | ||
|
|
dfe73f4609 | ||
|
|
7e573546a0 | ||
|
|
c1e9fbf279 | ||
|
|
957c1c6171 | ||
|
|
1f6e781c4f | ||
|
|
a255488817 | ||
|
|
dea5345397 | ||
|
|
0a7bd62401 | ||
|
|
17897cc392 | ||
|
|
5853e22343 | ||
|
|
8d6870f96e | ||
|
|
a966c7520b | ||
|
|
8c7338651c | ||
|
|
b6fc55911e | ||
|
|
376b49d8a5 | ||
|
|
c92bd40b88 | ||
|
|
8fa3e314c8 | ||
|
|
47147e0bcd | ||
|
|
b25b2eacb3 | ||
|
|
f131b64a6e | ||
|
|
79c5bdf681 | ||
|
|
2dd20331a7 | ||
|
|
9689dcef9f | ||
|
|
c018bb41a7 | ||
|
|
e52d2db449 | ||
|
|
230017196c | ||
|
|
e73ab0040c | ||
|
|
d888f1a08b | ||
|
|
eb5797370a | ||
|
|
3ef54fc55b | ||
|
|
eb9fa33ee7 | ||
|
|
62ab93d789 | ||
|
|
80b8d2a8a2 | ||
|
|
60f25eff98 | ||
|
|
1058426120 | ||
|
|
b3844550d0 | ||
|
|
f3875759ac | ||
|
|
92ef3c71ea | ||
|
|
ce66d319b1 | ||
|
|
2edb7a1fdf | ||
|
|
de5e237a88 | ||
|
|
7246e5ac2d | ||
|
|
805252a924 | ||
|
|
ec6010f018 | ||
|
|
d3ac1a600c | ||
|
|
64b7389fe9 | ||
|
|
5818cfb80f | ||
|
|
c19c73fdc8 | ||
|
|
445121f61c | ||
|
|
7360ca4242 | ||
|
|
e15af18d17 | ||
|
|
80d931ae2d | ||
|
|
56540f24c5 | ||
|
|
aa12873686 | ||
|
|
b2509bcd05 | ||
|
|
367ae2fb97 | ||
|
|
4220c810ee | ||
|
|
50fc0d7299 | ||
|
|
9a67185297 | ||
|
|
3157e8a7ad | ||
|
|
a51b8ba563 | ||
|
|
dae3a72e82 | ||
|
|
5631e91f0d | ||
|
|
1dd0173468 | ||
|
|
c89fc6ba2a | ||
|
|
84c9dfdd2c | ||
|
|
f3b190ba3e | ||
|
|
d021423af3 | ||
|
|
78b4082361 | ||
|
|
efc7134167 | ||
|
|
b44f22a949 | ||
|
|
02a078e95f | ||
|
|
d9b3ce266f | ||
|
|
da490a759f | ||
|
|
d91238b720 | ||
|
|
9629693a3c | ||
|
|
742ccb98a0 | ||
|
|
6cd4d38e21 | ||
|
|
b6412c6fed | ||
|
|
e06f8b7ec0 | ||
|
|
5a60a46e22 | ||
|
|
7ddbaf69a4 | ||
|
|
7ffa3efaf0 | ||
|
|
7c39e95972 | ||
|
|
8643e0f57c | ||
|
|
7e48be8061 | ||
|
|
9b801b7e09 | ||
|
|
0ff0490ada | ||
|
|
f968496cc6 | ||
|
|
c9775366f9 | ||
|
|
666e4aaca2 | ||
|
|
1c920eb8b8 | ||
|
|
02a6c3d90b | ||
|
|
ae2de4511b | ||
|
|
d01a4ae6cf | ||
|
|
9349af29c4 | ||
|
|
eb4fd74b13 | ||
|
|
99111f39fb | ||
|
|
4e3e53ee4d | ||
|
|
a928fad895 | ||
|
|
d61eec63b5 | ||
|
|
861ac69e4b | ||
|
|
484cb6e562 | ||
|
|
2ab16d3214 | ||
|
|
3caa59107e | ||
|
|
9469b9c79e | ||
|
|
4ac157b76d | ||
|
|
8f0d0a0d03 | ||
|
|
00dfdadf93 | ||
|
|
efece42fec | ||
|
|
f24226f219 | ||
|
|
137e49e393 | ||
|
|
9459546621 | ||
|
|
0fffb4548e | ||
|
|
fefca17762 | ||
|
|
143fd78a1a | ||
|
|
3f0b4caa98 | ||
|
|
30e20c6b60 | ||
|
|
eba58cc8b4 | ||
|
|
79e1a8f9c6 | ||
|
|
464ccb3142 | ||
|
|
1f4bde1817 | ||
|
|
bc0a272bfc | ||
|
|
8d076327f1 | ||
|
|
054cfae044 | ||
|
|
48addd51df | ||
|
|
a492d6d780 | ||
|
|
effc542a6a | ||
|
|
60829241fd | ||
|
|
c130f2aa1c | ||
|
|
089a5e7cab | ||
|
|
ad3f7bc008 |
64
.env.example
64
.env.example
@@ -87,6 +87,10 @@ DISABLE_SQLITE_AUTO_BACKUP=false
|
||||
# Used by: src/shared/utils/rateLimiter.ts
|
||||
# Example: redis://localhost:6379 (or redis://redis:6379 in Docker)
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
# Namespace prefix for ALL OmniRoute Redis keys (rate limiter + auth cache +
|
||||
# quota store). Prevents key collisions when OmniRoute shares a Redis instance
|
||||
# with other apps (e.g. on 127.0.0.1:6379). Default when unset: omniroute:
|
||||
# REDIS_KEY_PREFIX=omniroute:
|
||||
# Host interface docker-compose publishes the Redis sidecar on.
|
||||
# Default: 127.0.0.1 (loopback only). The compose Redis runs WITHOUT
|
||||
# `requirepass`, and app containers reach it over the compose network
|
||||
@@ -372,9 +376,8 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# NO_LOG_API_KEY_IDS=key_abc123,key_def456
|
||||
|
||||
# Fallback per-day request budget applied to API keys whose `rate_limits`
|
||||
# column is null. Default (unset/empty/malformed) preserves the legacy
|
||||
# 1000/day, 5000/week, 20000/month windows so existing deployments do not
|
||||
# silently lose rate limiting on upgrade.
|
||||
# column is null. Default (unset/empty) is unlimited (no implicit caps).
|
||||
# Malformed values preserve the legacy 1000/day, 5000/week, 20000/month windows.
|
||||
# Set explicitly to "0" to opt out entirely (unlimited fallback). Any
|
||||
# positive integer N enables N/day, 5N/week, 20N/month.
|
||||
# Used by: src/shared/utils/apiKeyPolicy.ts — checkRateLimit() fallback.
|
||||
@@ -1300,6 +1303,36 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# set to true/1/yes to enable. Used by: open-sse/executors/codex.ts.
|
||||
# OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS=true
|
||||
|
||||
# Codex app-server WebSocket transport (opt-in). When a WebSocket URL and a
|
||||
# capability token are both provided, Codex requests are routed through a local
|
||||
# `codex app-server` sidecar over JSON-RPC instead of the HTTP Responses API.
|
||||
# Each var is also settable per-connection via providerSpecificData; the env var
|
||||
# is the process-wide fallback. Used by:
|
||||
# open-sse/executors/codex/appServerConfig.ts.
|
||||
#
|
||||
# WebSocket endpoint of the codex app-server (ws:// or wss://). Required to
|
||||
# enable the transport; leaving it unset keeps Codex on its HTTP transports.
|
||||
# OMNIROUTE_CODEX_APPSERVER_WS=ws://127.0.0.1:8081
|
||||
# Inline capability/bearer token presented to the app-server.
|
||||
# OMNIROUTE_CODEX_APPSERVER_WS_TOKEN=deadbeef...
|
||||
# Path to a file holding the capability token (produced by
|
||||
# `codex app-server --ws-token-file <path>`). Used when the inline token above
|
||||
# is not set.
|
||||
# OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE=/run/codex-ws-token
|
||||
# Working directory the app-server turn runs in (defaults to /tmp).
|
||||
# OMNIROUTE_CODEX_APPSERVER_CWD=/tmp
|
||||
# Approval policy passed to the app-server turn (e.g. never, on-request).
|
||||
# OMNIROUTE_CODEX_APPSERVER_APPROVAL=never
|
||||
# Sandbox policy passed to the app-server turn (e.g. read-only,
|
||||
# workspace-write, danger-full-access). When unset, the executor defaults to
|
||||
# "workspace-write" (hardened; used to be "danger-full-access").
|
||||
# OMNIROUTE_CODEX_APPSERVER_SANDBOX=read-only
|
||||
# Auto-approve the app-server's own approval prompts (command/file/permission
|
||||
# execution on the host). Defaults to OFF — prompts are auto-denied. Set to
|
||||
# true/1/yes only when you trust the deployment to run codex-decided host
|
||||
# commands. Per-connection override: providerSpecificData.codexAppServerAutoApprove.
|
||||
# OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE=false
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1483,6 +1516,20 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# OMNIROUTE_BROWSER_POOL=on
|
||||
# WEB_COOKIE_USE_BROWSER=0
|
||||
|
||||
# ── Kimi Web (international kimi.ai Connect-RPC) ──
|
||||
# Used by: open-sse/executors/kimi-web.ts. Override the base/chat URLs only if
|
||||
# you need a mirror or proxy endpoint; defaults target https://www.kimi.ai with
|
||||
# the Connect-RPC chat path /apiv2/kimi.gateway.chat.v1.ChatService/Chat.
|
||||
# KIMI_WEB_BASE_URL=https://www.kimi.ai
|
||||
# KIMI_WEB_CHAT_URL=https://www.kimi.ai/apiv2/kimi.gateway.chat.v1.ChatService/Chat
|
||||
|
||||
# When OIDC is enabled, disable password login so users can only authenticate
|
||||
# via OIDC Single Sign-On. The bare alias OIDC_DISABLE_PASSWORD_LOGIN is also
|
||||
# accepted; the Dashboard Feature Flag takes precedence. Used by:
|
||||
# src/app/api/auth/login/route.ts, src/app/api/settings/require-login/route.ts.
|
||||
# OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN=false
|
||||
# OIDC_DISABLE_PASSWORD_LOGIN=false
|
||||
|
||||
# ── Adobe Firefly browser sign-in (system Chrome/Edge CDP) ──
|
||||
# Used by: open-sse/services/adobeFireflyBrowserLogin.ts. The Firefly login
|
||||
# flow drives a real, system-installed Chrome or Microsoft Edge via CDP so the
|
||||
@@ -1927,10 +1974,6 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 300000 (5 minutes)
|
||||
# SEARCH_CACHE_TTL_MS=300000
|
||||
|
||||
# ── OpenAI-compatible multi-connection ──
|
||||
# Allow multiple simultaneous connections per OpenAI-compatible provider node.
|
||||
# Used by: src/app/api/providers/route.ts
|
||||
# ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE=false
|
||||
|
||||
# ── CC-compatible provider (experimental) ──
|
||||
# Enable the Claude Code compatible provider endpoint.
|
||||
@@ -2564,6 +2607,11 @@ APP_LOG_TO_FILE=true
|
||||
# intended to be published as `omniroute-secure`. See SECURITY.md.
|
||||
# OMNIROUTE_BUILD_PROFILE=full
|
||||
|
||||
# Override the standalone build output directory consumed by the post-build
|
||||
# colocation step. Default: the real Next.js standalone output under .build/.
|
||||
# Used by: scripts/build/colocate-standalone.mjs (build tooling, not runtime).
|
||||
# OMNIROUTE_STANDALONE_DIR=
|
||||
|
||||
# Skip emitting `.tar.gz` tarballs during optional-pack staging for the Electron
|
||||
# standalone tree (pack directories + optional-packs.index.json are still produced).
|
||||
# Used by the desktop release workflow to trim artifact upload size.
|
||||
@@ -2578,6 +2626,8 @@ APP_LOG_TO_FILE=true
|
||||
# ELECTRON_SMOKE_DATA_DIR=
|
||||
# ELECTRON_SMOKE_KEEP_DATA=0
|
||||
# ELECTRON_SMOKE_STREAM_LOGS=0
|
||||
# #7592: second launch against the same DATA_DIR must pick the native driver.
|
||||
# ELECTRON_SMOKE_COLD_RESTART=0
|
||||
|
||||
# Playground Studio
|
||||
# Default model used by the improve-prompt route (optional; falls back to model in request body).
|
||||
|
||||
9
.github/workflows/dast-smoke.yml
vendored
9
.github/workflows/dast-smoke.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
env:
|
||||
PORT: "20128"
|
||||
INJECTION_GUARD_MODE: block
|
||||
REQUIRE_API_KEY: "false"
|
||||
run: |
|
||||
node dist/server.js > server.log 2>&1 &
|
||||
echo $! > server.pid
|
||||
@@ -64,16 +65,20 @@ jobs:
|
||||
# those 302s as "the API accepted a schema-violating request" and the configured-off
|
||||
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
|
||||
# still right (operators need it); fuzzing it is not what this smoke is for.
|
||||
# /api/auth/login has brute-force rate limiting: repeated failed logins return 429,
|
||||
# which Schemathesis flags as rejection of schema-compliant requests.
|
||||
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
|
||||
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
|
||||
--exclude-path-regex '^/api/auth/oidc/' \
|
||||
--exclude-path-regex '^/api/auth/(oidc/|login)' \
|
||||
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
|
||||
--request-timeout 20 --suppress-health-check all --no-color
|
||||
- name: Install promptfoo
|
||||
run: npm install -g promptfoo@0.122.0
|
||||
- name: promptfoo injection-guard (blocking)
|
||||
env:
|
||||
OMNIROUTE_URL: http://localhost:20128
|
||||
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
|
||||
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
|
||||
run: promptfoo eval -c promptfooconfig.yaml --no-cache
|
||||
- name: Stop server
|
||||
if: always()
|
||||
run: kill "$(cat server.pid)" || true
|
||||
|
||||
76
.github/workflows/docker-publish.yml
vendored
76
.github/workflows/docker-publish.yml
vendored
@@ -183,15 +183,55 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Build and push BUN base platform image by digest
|
||||
id: build-bun-base
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.bun
|
||||
target: runner-base
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
${{ env.GHCR_IMAGE_NAME }}
|
||||
cache-from: type=gha,scope=docker-bun-base-${{ matrix.arch }}
|
||||
cache-to: type=gha,scope=docker-bun-base-${{ matrix.arch }},mode=max
|
||||
no-cache: false
|
||||
env:
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Build and push BUN web platform image by digest
|
||||
id: build-bun-web
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.bun
|
||||
target: runner-web
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
${{ env.GHCR_IMAGE_NAME }}
|
||||
cache-from: type=gha,scope=docker-bun-web-${{ matrix.arch }}
|
||||
cache-to: type=gha,scope=docker-bun-web-${{ matrix.arch }},mode=max
|
||||
no-cache: false
|
||||
env:
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Export digests
|
||||
env:
|
||||
DIGEST_BASE: ${{ steps.build.outputs.digest }}
|
||||
DIGEST_WEB: ${{ steps.build-web.outputs.digest }}
|
||||
DIGEST_BUN_BASE: ${{ steps.build-bun-base.outputs.digest }}
|
||||
DIGEST_BUN_WEB: ${{ steps.build-bun-web.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/digests/base /tmp/digests/web
|
||||
mkdir -p /tmp/digests/base /tmp/digests/web /tmp/digests/bun-base /tmp/digests/bun-web
|
||||
touch "/tmp/digests/base/${DIGEST_BASE#sha256:}"
|
||||
touch "/tmp/digests/web/${DIGEST_WEB#sha256:}"
|
||||
touch "/tmp/digests/bun-base/${DIGEST_BUN_BASE#sha256:}"
|
||||
touch "/tmp/digests/bun-web/${DIGEST_BUN_WEB#sha256:}"
|
||||
|
||||
- name: Upload base digests
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -209,6 +249,22 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload bun-base digests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: digests-bun-base-${{ matrix.arch }}
|
||||
path: /tmp/digests/bun-base/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload bun-web digests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: digests-bun-web-${{ matrix.arch }}
|
||||
path: /tmp/digests/bun-web/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Publish multi-arch manifests
|
||||
needs:
|
||||
@@ -263,6 +319,20 @@ jobs:
|
||||
path: /tmp/digests/web
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download bun-base digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: digests-bun-base-*
|
||||
path: /tmp/digests/bun-base
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download bun-web digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: digests-bun-web-*
|
||||
path: /tmp/digests/bun-web
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create Docker Hub manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -286,6 +356,8 @@ jobs:
|
||||
|
||||
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base
|
||||
create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web
|
||||
|
||||
- name: Create GHCR manifest
|
||||
run: |
|
||||
@@ -310,6 +382,8 @@ jobs:
|
||||
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web
|
||||
|
||||
- name: Inspect image
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
|
||||
@@ -92,5 +92,9 @@
|
||||
# - x-api-key PUBLICO do Firefly web (documentado em open-sse/utils/publicCreds.ts:207);
|
||||
# as duas ocorrencias sinalizadas estao em COMENTARIOS JSDoc, o runtime le de resolvePublicCred().
|
||||
'''omniroute-kimi-sponsor-banner-dismissed-v\d+''',
|
||||
# CheaperInference sponsor banner localStorage key (upstream #11196 /
|
||||
# eb5797370). Same UI-identifier pattern as the kimi banner above, not a
|
||||
# credential; the generic-api-key rule flags the long hyphenated string.
|
||||
'''omniroute-cheaperinference-sponsor-banner-dismissed-v\d+''',
|
||||
'''SunbreakWebUI1''',
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -1161,6 +1161,8 @@ export interface OmniRouteRawModelEntry {
|
||||
attachment?: boolean;
|
||||
structured_output?: boolean;
|
||||
temperature?: boolean;
|
||||
/** Runtime-learned or synced reasoning tiers (server-gated, blind-mapped). */
|
||||
effort_tiers?: string[];
|
||||
};
|
||||
release_date?: string;
|
||||
last_updated?: string;
|
||||
@@ -1302,6 +1304,18 @@ export function mapRawModelToModelV2(
|
||||
ctx: { providerId: string; baseURL: string; apiFormat?: { anthropicPrefixes?: string[] } }
|
||||
): ModelV2 {
|
||||
const caps = raw.capabilities ?? {};
|
||||
// effort_tiers loop: server-declared tiers become ModelV2 variants so the
|
||||
// UI offers exactly the tiers OmniRoute vouches for (instead of opencode's
|
||||
// invented [low, medium, high] fallback). Blind: filtering/exclusion rules
|
||||
// live server-side. Absent/empty/malformed => key omitted ENTIRELY (an
|
||||
// empty variants object would suppress opencode's fallback for this model).
|
||||
const declaredTiers = Array.isArray(caps.effort_tiers)
|
||||
? caps.effort_tiers.filter((t): t is string => typeof t === "string" && t.length > 0)
|
||||
: [];
|
||||
const variants =
|
||||
declaredTiers.length > 0
|
||||
? Object.fromEntries(declaredTiers.map((tier) => [tier, { reasoningEffort: tier }]))
|
||||
: undefined;
|
||||
const inMods = new Set(raw.input_modalities ?? ["text"]);
|
||||
const outMods = new Set(raw.output_modalities ?? ["text"]);
|
||||
|
||||
@@ -1315,10 +1329,7 @@ export function mapRawModelToModelV2(
|
||||
// OpenCode looks up `-m <plugin>/<combo>` as model id `<combo>` under
|
||||
// the plugin provider (#10345). Other bare ids still prefix with
|
||||
// `providerId` so credentials resolve as `(omniroute, model)`.
|
||||
id:
|
||||
raw.id.includes("/") || raw.owned_by === "combo"
|
||||
? raw.id
|
||||
: `${ctx.providerId}/${raw.id}`,
|
||||
id: raw.id.includes("/") || raw.owned_by === "combo" ? raw.id : `${ctx.providerId}/${raw.id}`,
|
||||
/**
|
||||
* Display name. Falls back to raw.id when no enrichment is available;
|
||||
* the caller (`createOmniRouteProviderHook`) overlays
|
||||
@@ -1357,6 +1368,7 @@ export function mapRawModelToModelV2(
|
||||
...(typeof raw.max_input_tokens === "number" ? { input: raw.max_input_tokens } : {}),
|
||||
output: typeof raw.max_output_tokens === "number" ? raw.max_output_tokens : 0,
|
||||
},
|
||||
...(variants ? { variants } : {}),
|
||||
status: "active",
|
||||
options: {},
|
||||
headers: {},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* effort_tiers loop — plugin maps server-declared tiers to ModelV2 variants.
|
||||
* Blind mapping (I3): no owned_by/provider knowledge here — the SERVER gates
|
||||
* eligibility (shouldExposeSyncedEffortVariants). Absence semantics (M3):
|
||||
* no tiers => NO variants key at all (an empty object would also kill
|
||||
* opencode's own fallback for non-tiered models).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mapRawModelToModelV2, type OmniRouteRawModelEntry } from "../src/index.js";
|
||||
|
||||
const CTX = { providerId: "omniroute", baseURL: "http://127.0.0.1:20128" } as const;
|
||||
|
||||
test("maps declared tiers to reasoningEffort variants", () => {
|
||||
const raw: OmniRouteRawModelEntry = {
|
||||
id: "oc/x-preview-f-free",
|
||||
owned_by: "opencode",
|
||||
capabilities: { reasoning: true, effort_tiers: ["low", "high", "max"] },
|
||||
};
|
||||
const model = mapRawModelToModelV2(raw, { ...CTX });
|
||||
const variants = (model as unknown as Record<string, unknown>).variants as
|
||||
Record<string, Record<string, unknown>> | undefined;
|
||||
assert.ok(variants, "variants key present when tiers declared");
|
||||
assert.deepEqual(Object.keys(variants).sort(), ["high", "low", "max"]);
|
||||
assert.deepEqual(variants.max, { reasoningEffort: "max" });
|
||||
assert.deepEqual(variants.low, { reasoningEffort: "low" });
|
||||
});
|
||||
|
||||
test("no tiers => NO variants key (not an empty object)", () => {
|
||||
const raw: OmniRouteRawModelEntry = {
|
||||
id: "plain-model",
|
||||
capabilities: { reasoning: true },
|
||||
};
|
||||
const model = mapRawModelToModelV2(raw, { ...CTX }) as unknown as Record<string, unknown>;
|
||||
assert.equal("variants" in model, false);
|
||||
});
|
||||
|
||||
test("empty or malformed tiers array => NO variants key", () => {
|
||||
const empty = mapRawModelToModelV2(
|
||||
{ id: "m", capabilities: { effort_tiers: [] } },
|
||||
{ ...CTX }
|
||||
) as unknown as Record<string, unknown>;
|
||||
assert.equal("variants" in empty, false);
|
||||
|
||||
const junk = mapRawModelToModelV2(
|
||||
{ id: "m", capabilities: { effort_tiers: [42, null, "ok"] as unknown as string[] } },
|
||||
{ ...CTX }
|
||||
) as unknown as Record<string, unknown>;
|
||||
const variants = junk.variants as Record<string, Record<string, unknown>> | undefined;
|
||||
assert.deepEqual(Object.keys(variants ?? {}), ["ok"], "non-string tokens dropped");
|
||||
});
|
||||
|
||||
test("static registry entry WITH tiers also gets variants (N1 blast radius)", () => {
|
||||
const raw: OmniRouteRawModelEntry = {
|
||||
id: "some-static-model",
|
||||
owned_by: "registry",
|
||||
capabilities: { effort_tiers: ["minimal", "high"] },
|
||||
};
|
||||
const model = mapRawModelToModelV2(raw, { ...CTX }) as unknown as Record<string, unknown>;
|
||||
const variants = model.variants as Record<string, Record<string, unknown>> | undefined;
|
||||
assert.deepEqual(Object.keys(variants ?? {}).sort(), ["high", "minimal"]);
|
||||
});
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 346 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 350 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -56,9 +56,9 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (157 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (159 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -2,6 +2,18 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(sse): STRICT_ZERO_COST** — opt-in, off-by-default `freeAccessPolicy: "strict"` setting
|
||||
that hard-verifies every auto-combo candidate against live quota state and per-connection
|
||||
economic safety before it can be dispatched, going beyond `hidePaidModels`'s static catalog
|
||||
check. Adds curated `hardStopGuaranteed` metadata to `FREE_MODEL_BUDGETS`, a short-TTL quota
|
||||
cache reusing `getUsageForProvider()`, and a connection-safety guarantee: a candidate backed
|
||||
by multiple accounts has its `allowedConnectionIds` narrowed to exactly the connections
|
||||
independently verified `SAFE`, so dispatch can never use an unverified account. An
|
||||
`excludeTosAvoid` guard (default `false`) is available separately for contractual risk. See
|
||||
`docs/routing/STRICT_ZERO_COST.md`.
|
||||
|
||||
---
|
||||
|
||||
## [3.8.50] — TBD
|
||||
@@ -9,6 +21,7 @@
|
||||
_Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._
|
||||
|
||||
### ✨ New Features
|
||||
- **feat(search):** first-class X Search provider (`x-search`) on `POST /v1/search` and MCP `omniroute_x_search` using SuperGrok / xAI server-side `x_search`. Explicit provider or `search_type: "x"` only — never auto-selected for web. Reuses `xai-oauth` / `xao` / `xai` credentials. Not the X Developer Platform MCP. ([#10985](https://github.com/diegosouzapw/OmniRoute/issues/10985))
|
||||
- **feat(core):** add Layer A capability filter at router (#5696)
|
||||
- **feat(providers):** add DeepAI as paid API-key image provider ([#6671](https://github.com/diegosouzapw/OmniRoute/issues/6671))
|
||||
- **feat(providers):** add Naga.ac and ChatAnywhere aggregator gateway providers (#6674 — thanks @chirag127)
|
||||
|
||||
146
Dockerfile.bun
Normal file
146
Dockerfile.bun
Normal file
@@ -0,0 +1,146 @@
|
||||
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
||||
FROM oven/bun:1.3.14-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
python3 \
|
||||
python-is-python3 \
|
||||
make \
|
||||
g++ \
|
||||
libsecret-1-0 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Builder stage (100% Bun Native Install & Build) ─────────────────────────
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
# Fast Bun native package install
|
||||
RUN bun install --include=optional --quiet
|
||||
|
||||
# Compile native better-sqlite3 Node-API addon under Bun
|
||||
RUN if [ -d "node_modules/better-sqlite3" ]; then \
|
||||
(cd node_modules/better-sqlite3 && bunx node-gyp rebuild); \
|
||||
fi
|
||||
|
||||
# Fetch tls-client-node native binary if script exists
|
||||
RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ]; then \
|
||||
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
|
||||
fi
|
||||
|
||||
# Disable Turbopack for Bun builder stage (Turbopack V8 internal worker bindings require Node)
|
||||
ENV OMNIROUTE_USE_TURBOPACK=0
|
||||
|
||||
ARG OMNIROUTE_BASE_PATH=""
|
||||
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
||||
|
||||
ARG DASHBOARD_ALLOW_EMBED=""
|
||||
ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Bun native Next.js build execution
|
||||
RUN bun run --quiet build
|
||||
|
||||
# ── Runner Base stage (100% Bun Native Production Runtime) ──────────────────
|
||||
FROM oven/bun:1.3.14-slim AS runner-base
|
||||
|
||||
LABEL org.opencontainers.image.title="omniroute" \
|
||||
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \
|
||||
org.opencontainers.image.url="https://omniroute.online" \
|
||||
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libsecret-1-0 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=20128
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV OMNIROUTE_MEMORY_MB=1024
|
||||
|
||||
ENV DATA_DIR=/app/data
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
COPY --from=builder /app/.build/next/standalone ./
|
||||
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
|
||||
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
||||
|
||||
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
|
||||
|
||||
EXPOSE 20128
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD bun healthcheck.mjs || exit 1
|
||||
|
||||
ENTRYPOINT ["bun", "dev/run-standalone.mjs"]
|
||||
|
||||
# ── Runner Web stage (Bun Native + Chromium/Playwright for Web providers) ───
|
||||
FROM runner-base AS runner-web
|
||||
|
||||
USER root
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
chromium \
|
||||
chromium-driver \
|
||||
fonts-liberation \
|
||||
libasound2t64 \
|
||||
gconf-service \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libc6 \
|
||||
libcairo2 \
|
||||
libcups2 \
|
||||
libdbus-1-3 \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgbm1 \
|
||||
libgcc-s1 \
|
||||
libglib2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libpango-1.0-0 \
|
||||
pangocairo-1.0-0 \
|
||||
stdc++6 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
ca-certificates \
|
||||
fonts-gargi \
|
||||
fonts-ipafont-gothic \
|
||||
fonts-kacst \
|
||||
fonts-thai-tlwg \
|
||||
fonts-wqy-zenhei \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
# Return to the base image non-root user after the apt install (mirrors the
|
||||
# Node Dockerfile runner-web stage, which re-asserts USER node).
|
||||
USER bun
|
||||
447
PROVIDER_REFERENCE.md
Normal file
447
PROVIDER_REFERENCE.md
Normal file
@@ -0,0 +1,447 @@
|
||||
---
|
||||
title: "Provider Reference"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-21
|
||||
---
|
||||
|
||||
# Provider Reference
|
||||
|
||||
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-08-21
|
||||
|
||||
Total providers: **349**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
- **Free** — free tier with API key (configured via dashboard)
|
||||
- **No-auth** — public endpoints that require no key or sign-in at all
|
||||
- **OAuth** — sign-in flow handled by OmniRoute, no API key needed
|
||||
- **Web cookie** — wraps the provider's web app via cookie auth
|
||||
- **API key** — paid provider configured via API key (free credits may apply)
|
||||
- **Local** — runs on the user's machine (Ollama, LM Studio, vLLM, etc.)
|
||||
- **Search** — web search providers
|
||||
- **Audio** — audio-only providers (TTS/STT)
|
||||
- **Upstream proxy** — providers that proxy to other providers
|
||||
- **Cloud agent** — long-running coding agents (Codex Cloud, Devin, Jules)
|
||||
- **System** — OmniRoute-internal providers (loopback, etc.)
|
||||
|
||||
Additional tags: `image`, `video`, `aggregator`, `enterprise`, `embed/rerank`, `self-hosted`.
|
||||
|
||||
`Tool calling` (where shown): `native` — real function-calling API; `emulated` — the `tools` array is prompt-emulated via `webTools.ts` (regex-parsed `<tool>{...}</tool>` blocks); `none` — `tools` is currently silently dropped. See #7286.
|
||||
|
||||
Use the dashboard at `/dashboard/providers` to enable, configure, and test each provider.
|
||||
|
||||
---
|
||||
|
||||
## No-auth Providers (no key required) (11)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
| `aihorde` | `horde` | AI Horde | No-auth | [link](https://aihorde.net) | No API key required — uses AI Horde's documented anonymous key. Adding a free aihorde.net key is optional and only buys higher queue priority (kudos). | — |
|
||||
| `auggie` | `aug` | Augment (Auggie CLI) | No-auth | [link](https://augmentcode.com) | No API key stored by OmniRoute. Install the Auggie CLI and run `auggie login` on this machine, then OmniRoute spawns it locally for each request. | — |
|
||||
| `chipotle` | `pepper` | Chipotle Pepper AI (Free) | No-auth | [link](https://amelia.chipotle.com) | No credentials required. Uses Chipotle's public support chatbot via reverse-engineered SockJS/STOMP protocol. | — |
|
||||
| `cloudflare-playground` | `cfp` | Cloudflare AI Playground | No-auth | [link](https://playground.ai.cloudflare.com) | No credentials required — anonymous browser sessions over a reverse-engineered cf_agent WebSocket protocol (Playwright transport). | — |
|
||||
| `devin-cli-agentic` | `dva` | Devin CLI Agentic Bridge | No-auth | [link](https://docs.devin.ai/work-with-devin/devin-cli) | Authentication is owned by the official Devin CLI in its isolated bridge volume. | emulated |
|
||||
| `duckduckgo-web` | `ddgw` | DuckDuckGo AI Chat | No-auth | [link](https://duckduckgo.com/duckchat) | No credentials required — DuckDuckGo AI Chat is anonymous and free. | emulated |
|
||||
| `felo-web` | `felo` | Felo | No-auth | [link](https://felo.ai) | No credentials required — Felo is a free, no-signup chat/search aggregator. | — |
|
||||
| `opencode` | `oc` | OpenCode Free | No-auth | [link](https://opencode.ai) | No API key required — uses OpenCode's public free endpoint. | — |
|
||||
| `theoldllm` | `tllm` | The Old LLM (Free) | No-auth | [link](https://theoldllm.vercel.app) | No credentials required. The executor auto-generates access tokens via an embedded Playwright browser instance. | — |
|
||||
| `veoaifree-web` | `veo-free` | Veo AI Free | No-auth, video | [link](https://veoaifree.com) | No auth required. Rate limited to 6 requests/hour per IP. | — |
|
||||
| `zcode` | `zc` | ZCode (GLM Coding Plan) | No-auth | [link](https://zcode.z.ai) | No API key stored by OmniRoute. The local ZCode app-server uses the existing builtin:zai-coding-plan login. | — |
|
||||
|
||||
## OAuth Providers (25)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `agy` | `agy` | Antigravity CLI | OAuth | [link](https://antigravity.google) | Import your Antigravity CLI (`agy`) login (paste/upload its token file), auto-detect a local CLI login, or sign in with Google. Shares the Antigravity backend (incl. Claude models). |
|
||||
| `amazon-q` | `aq` | Amazon Q | OAuth | [link](https://aws.amazon.com/q/developer/) | Uses the same AWS Builder ID or imported refresh-token flow as Kiro, but keeps Amazon Q connections separate. |
|
||||
| `antigravity` | — | Antigravity | OAuth | — | — |
|
||||
| `claude` | `cc` | Claude Code | OAuth | — | — |
|
||||
| `cline` | `cl` | Cline | OAuth | — | — |
|
||||
| `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. |
|
||||
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
|
||||
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
|
||||
| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
|
||||
| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
|
||||
| `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. |
|
||||
| `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. |
|
||||
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
|
||||
| `gitlab-duo` | `gitlab-duo` | GitLab Duo | OAuth | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab Duo OAuth is not configured. Register an OAuth application at https://gitlab.com/-/profile/applications with redirect URI http://localhost:20128/callback and scopes "ai_features read_user", then set GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart. |
|
||||
| `grok-cli` | `gc` | Grok Build | OAuth | — | Sign in with your browser, or paste your ~/.grok/auth.json (or the JWT access token) from the Grok Build CLI; refresh_token is rotated automatically either way. |
|
||||
| `kilocode` | `kc` | Kilo Code | OAuth | — | — |
|
||||
| `kimi-coding` | `kmc` | Kimi Code CLI | OAuth | [link](https://www.kimi.com/code?aff=omniroute) | Sign in with the same Kimi account used by Kimi Code CLI. OmniRoute uses the CLI OAuth flow and Kimi Coding Plan endpoints. |
|
||||
| `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. |
|
||||
| `openference` | `of` | Openference | OAuth | [link](https://openference.com) | Sign in with your Openference account to route requests through api.openference.com. An active plan is required for inference — OAuth may authenticate but return 402 without one. |
|
||||
| `qoder` | `if` | Qoder | OAuth | — | — |
|
||||
| `raycast` | `rc` | Raycast Pro AI | OAuth | [link](https://raycast.com/ai) | Unofficial integration — uses your Raycast Pro subscription via credentials from the macOS app (Auto-Import or manual capture). May break on Raycast updates. Not for redistribution; personal use only. |
|
||||
| `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT <token>', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. |
|
||||
| `xai-oauth` | `xao` | xAI OAuth (Grok) | OAuth | [link](https://x.ai) | Sign in with xAI to use api.x.ai models such as Grok 4.5. This is separate from Grok Build JWT sessions, which use cli-chat-proxy.grok.com and grok-build model aliases. |
|
||||
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
|
||||
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
|
||||
|
||||
## Web Cookie Providers (35)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes | Tool calling |
|
||||
|----|-------|------|------|---------|-------|--------------|
|
||||
| `adapta-web` | `adp-web` | Adapta.org (Adapta One Web) | Web cookie | [link](https://agent.adapta.one) | Paste your __client cookie value from .clerk.agent.adapta.one (DevTools → Application → Cookies) | emulated |
|
||||
| `adobe-firefly` | `firefly` | Adobe Firefly (Image/Video) | Web cookie | [link](https://firefly.adobe.com) | RECOMMENDED: firefly.adobe.com signed-in → F12 → Network → click firefly-3p.ff.adobe.io (generate-async or models/discovery) → Request Headers → Authorization → copy the token AFTER 'Bearer ' (starts with eyJ…). Cookie-only from firefly.adobe.com mints a GUEST token → 401/403; only multi-domain IMS cookies (adobelogin.com) or that Bearer JWT work. Unofficial/experimental media + Limits. | — |
|
||||
| `blackbox-web` | `bb-web` | Blackbox Web (Subscription) | Web cookie | [link](https://app.blackbox.ai) | Paste your __Secure-authjs.session-token value or full cookie header from app.blackbox.ai | emulated |
|
||||
| `chatgpt-web` | `cgpt-web` | ChatGPT Web (Plus/Pro) | Web cookie | [link](https://chatgpt.com) | Paste your __Secure-next-auth.session-token cookie value from chatgpt.com | emulated |
|
||||
| `chatgpt-web-codex` | `cgpt-codex` | ChatGPT Web (Codex) | Web cookie | [link](https://chatgpt.com) | Paste the full ChatGPT Cookie header. OmniRoute verifies it in an isolated headless browser profile. | native |
|
||||
| `claude-web` | `cw` | Claude Web | Web cookie | [link](https://claude.ai) | Paste your session cookie from claude.ai | none |
|
||||
| `conol-web` | `cnl` | Conol (Unofficial/Experimental) | Web cookie | [link](https://conol.ai) | Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required. | — |
|
||||
| `copilot-m365-web` | `m365copilot` | Microsoft 365 Copilot (BizChat) | Web cookie | [link](https://m365.cloud.microsoft/chat) | Sign in at m365.cloud.microsoft/chat, then open DevTools → Network → filter 'WS' → click the Chathub WebSocket connection. Copy both the access_token query parameter AND the account-specific Chathub path segment from its request URL (wss://…/Chathub/<path>?…&access_token=…). It is NOT an Authorization: Bearer header on an XHR/Fetch request. The token is short-lived; this is an unofficial integration. Optional: store a refresh_token in providerSpecificData.refreshToken (any Microsoft device-code/refresh flow for the substrate.office.com/sydney scopes) and OmniRoute pre-flight-refreshes the access token itself — otherwise re-capture after every ~75 min expiry. | — |
|
||||
| `copilot-web` | `copilot` | Microsoft Copilot Web | Web cookie | [link](https://copilot.microsoft.com) | Paste the access_token from an authenticated copilot.microsoft.com request (DevTools → Network → Authorization), or export a HAR while logged in | — |
|
||||
| `deepseek-web` | `ds-web` | DeepSeek Web | Web cookie | [link](https://chat.deepseek.com) | Paste your userToken from chat.deepseek.com — DevTools → Application → Local Storage → userToken | emulated |
|
||||
| `doubao-web` | `db` | Dola Web (ByteDance) | Web cookie | [link](https://www.dola.com) | Paste the full Cookie header from www.dola.com. It should include sessionid, ttwid, and s_v_web_id. If s_v_web_id is unavailable, fp=verify_... from a chat/completion request URL can be used as a fallback. | — |
|
||||
| `gemini-business` | `gembiz` | Gemini Business (Enterprise) | Web cookie | [link](https://business.gemini.google) | From your enterprise account: open business.gemini.google/home/cid/{your-cid}, then copy __Secure-1PSID and __Secure-1PSIDTS cookies from DevTools → Application → Cookies. Paste as a cookie header below. | — |
|
||||
| `gemini-web` | `gweb` | Gemini Web (Free) | Web cookie | [link](https://gemini.google.com) | Paste your __Secure-1PSID cookie value from gemini.google.com. Optionally add __Secure-1PSIDTS separated by semicolon. | emulated |
|
||||
| `grok-web` | `gw` | Grok Web (Subscription) | Web cookie | [link](https://grok.com) | Paste the full grok.com cookie line from DevTools → Application → Cookies. Include both `sso` and `sso-rw` (e.g. `sso=...; sso-rw=...`) — Grok's anti-bot rejects `sso` on its own. | — |
|
||||
| `hailuo-web` | `hailuo-web` | Hailuo Web (MiniMax) | Web cookie | [link](https://hailuo.ai) | Open hailuo.ai, log in, then open DevTools → Application → Local Storage → copy the "_token" value. device_id/uuid fingerprint fields are derived automatically; if requests fail, re-capture _token (sessions can expire). | — |
|
||||
| `huggingchat` | `huggingchat` | HuggingChat (Free) | Web cookie | [link](https://huggingface.co/chat) | Paste the full Cookie header from huggingface.co/chat (DevTools → Network → /chat/conversation → Request Headers → Cookie). It should include hf-chat and may also include token / aws-waf-token. | — |
|
||||
| `hyperagent` | `ha` | HyperAgent (Unofficial/Experimental) | Web cookie | [link](https://hyperagent.com) | Paste the full Cookie header from hyperagent.com (DevTools → Network → any request → Request Headers → Cookie). Session cookies power chat + billing usage. | — |
|
||||
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com | emulated |
|
||||
| `kimi-web` | `kimi-web` | Kimi Web | Web cookie | [link](https://www.kimi.com/code?aff=omniroute) | Paste access_token from www.kimi.com DevTools → Application → Local Storage. A legacy kimi-auth cookie is also accepted. | — |
|
||||
| `lmarena` | `lma` | Arena (Free) | Web cookie | [link](https://arena.ai) | Paste the full Cookie header from arena.ai (DevTools → Network → request → Cookie). Include arena-auth-prod-v1.0/.1… and cf_clearance/__cf_bm when present. OmniRoute uses Chrome TLS impersonation; if Arena still 403s, set providerSpecificData.recaptchaV3Token from a live browser session. | — |
|
||||
| `microsoft-designer-web` | `msdesigner` | Microsoft Designer (Image Generation) | Web cookie | [link](https://designer.microsoft.com) | Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration. | — |
|
||||
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess cookie AND the ecto1:... WS auth token from meta.ai. Capture the ecto1: token in DevTools → Network → WS → the clippy request's Authorization query param. Example: ecto_1_sess=4240a308...NVDg0; ecto1:ABCD... | emulated |
|
||||
| `notion-web` | `nw` | Notion AI Web (Unofficial/Experimental) | Web cookie | [link](https://www.notion.so) | Paste only the token_v2 cookie VALUE from app.notion.com (DevTools → Application → Cookies → token_v2). Do not paste token_v2= or the full Cookie header. Workspace is auto-detected; space_id / notion_user_id are optional. | — |
|
||||
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai | emulated |
|
||||
| `poe-web` | `poe` | Poe Web (Subscription) | Web cookie | [link](https://poe.com) | Paste your p-b cookie value from poe.com (DevTools → Application → Cookies → p-b) | — |
|
||||
| `promptql` | `pql` | PromptQL (Unofficial/Experimental) | Web cookie | [link](https://prompt.ql.app) | Paste the Bearer JWT from prompt.ql.app DevTools → Network → graphql → Authorization (token only). Optional projectId + session Cookie for refresh. | — |
|
||||
| `qwen-web` | `qwen-web` | Qwen Web (Free) | Web cookie | [link](https://chat.qwen.ai) | Open chat.qwen.ai, log in, then open DevTools → Application → Local Storage → copy the "token" value (or use tongyi_sso_ticket cookie as Bearer token). | emulated |
|
||||
| `t3-web` | `t3chat` | t3.chat (Pro/Free) | Web cookie | [link](https://t3.chat) | Open t3.chat in your browser, log in, then open DevTools → Application → Local Storage → https://t3.chat. Copy the value of 'convex-session-id'. Also open DevTools → Network, copy the Cookie header from any request. Paste both values here. See provider setup docs for a step-by-step guide. | emulated |
|
||||
| `tencent-aistudio-web` | `tasw` | Tencent AI Studio (Free) | Web cookie | [link](https://aistudio.tencent.ai) | Log in to aistudio.tencent.ai, open DevTools -> Network, copy any request Cookie header containing session tokens. | — |
|
||||
| `tinycms-web` | `tcw` | TinyCMS Web (Free/Sub) | Web cookie | [link](https://site.tinycms.xyz) | Go to site.tinycms.xyz, open DevTools → Application → Local Storage, copy the value of 'app-config-uuid' (starts with 'R'), and paste it here. | — |
|
||||
| `v0-vercel-web` | `v0-vercel-web` | v0 Vercel Web (Code Gen) | Web cookie | [link](https://v0.dev) | Paste your session cookie from v0.dev (DevTools → Application → Cookies) | — |
|
||||
| `venice-web` | `ven` | Venice Web (Privacy) | Web cookie | [link](https://venice.ai) | Paste your session cookie from venice.ai (DevTools → Application → Cookies) | — |
|
||||
| `yuanbao-web` | `ybw` | Tencent Yuanbao (Free) | Web cookie | [link](https://yuanbao.tencent.com) | Log in to yuanbao.tencent.com, then paste the full Cookie header (DevTools → Network → any /api request → Request Headers → Cookie). It must contain hy_user and hy_token. | — |
|
||||
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
|
||||
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
|
||||
|
||||
## API Key Providers (paid / paid-with-free-credits) (233)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `360ai` | `360ai` | 360 AI | API key | [link](https://ai.360.cn) | Get API key at ai.360.cn |
|
||||
| `agentrouter` | `agentrouter` | AgentRouter | API key, aggregator | [link](https://agentrouter.org) | $200 free credits on signup - multi-model routing gateway |
|
||||
| `agnes` | `agnes` | Agnes AI | API key, video | [link](https://agnes-ai.com) | Get API key at agnes-ai.com |
|
||||
| `ai21` | `ai21` | AI21 Labs | API key | [link](https://www.ai21.com) | $10 trial credits on signup (valid 3 months), no credit card required |
|
||||
| `aimlapi` | `aiml` | AI/ML API | API key, aggregator | [link](https://aimlapi.com) | Free tier paused (2026) — AI/ML API is now pay-as-you-go only (min $20 top-up); no recurring free credits. |
|
||||
| `ainative` | `ainative` | AINative Studio | API key | [link](https://ainative.studio) | Create a free API key at ainative.studio (no card), then paste it here as a Bearer token. |
|
||||
| `aion` | `aion` | Aion Labs | API key | [link](https://www.aionlabs.ai) | Create a free API key at aionlabs.ai (no card), then paste it here as a Bearer token. |
|
||||
| `alibaba` | `ali` | Alibaba Cloud Model Studio | API key | [link](https://bailian.console.alibabacloud.com/) | — |
|
||||
| `alibaba-cn` | `ali-cn` | Alibaba (China) | API key | [link](https://dashscope.console.aliyun.com/) | — |
|
||||
| `ant-ling` | `ling` | Ant Ling / Ring (inclusionAI) | API key | [link](https://developer.ant-ling.com/en/docs/) | Register and create an API key at the Ant Ling API console (https://chat.ant-ling.com/open), then paste it here. OmniRoute routes chat traffic to https://api.ant-ling.com/v1/chat/completions; the provider is OpenAI-compatible and also exposes an Anthropic-compatible surface. |
|
||||
| `anthropic` | `anthropic` | Anthropic | API key | [link](https://platform.claude.com) | — |
|
||||
| `anyapi` | `anyapi` | AnyAPI AI | API key, aggregator | [link](https://anyapi.ai) | Free plan: 100,000 ANY Tokens/day and 100 RPM for eligible Free/Basic models; no credit card required. |
|
||||
| `api-airforce` | `af` | Api.airforce | API key | [link](https://api.airforce) | 55 free tier models including Grok-3, Claude 3.7, Qwen3, Kimi-K2, Gemini 2.5 Flash, DeepSeek-V3 |
|
||||
| `arcee-ai` | `arcee` | Arcee AI | API key | [link](https://arcee.ai) | Get API key at arcee.ai |
|
||||
| `auriko` | `auriko` | Auriko | API key, aggregator | [link](https://www.auriko.ai) | Free plan publishes 1,000 Platform RPM and 10,000 BYOK RPM. Platform inference still passes through provider cost; this is not a free-token pool or unlimited free inference. |
|
||||
| `azure-ai` | `azure-ai` | Azure AI Foundry | API key, enterprise | [link](https://learn.microsoft.com/azure/ai-foundry) | Use your Azure AI Foundry key. Base URL can be https://<resource>.services.ai.azure.com/openai/v1/ or https://<resource>.openai.azure.com/openai/v1/. |
|
||||
| `azure-openai` | `azure` | Azure OpenAI | API key, enterprise | [link](https://azure.microsoft.com/products/ai-services/openai-service) | Use your Azure OpenAI API key. Base URL should be your resource endpoint, for example https://my-resource.openai.azure.com. |
|
||||
| `bai` | `bai` | b.ai | API key | [link](https://b.ai) | Bearer API key for the b.ai OpenAI-compatible LLM gateway (distinct from TheB.AI). Create a key at https://docs.b.ai, then use https://api.b.ai/v1 as the OpenAI-compatible base URL. |
|
||||
| `baichuan` | `baichuan` | Baichuan | API key | [link](https://www.baichuan-ai.com/) | Get API key at platform.baichuan-ai.com |
|
||||
| `baidu` | `baidu` | Baidu (ERNIE) | API key | [link](https://ernie.baidu.com/) | Get API key at console.bce.baidu.com |
|
||||
| `bailian-coding-plan` | `bcp` | Alibaba Token Plan | API key | [link](https://www.alibabacloud.com/help/en/model-studio/token-plan-overview) | — |
|
||||
| `baseten` | `baseten` | Baseten | API key | [link](https://baseten.co) | $30 free trial credits for GPU inference |
|
||||
| `bazaarlink` | `bzl` | BazaarLink | API key | [link](https://bazaarlink.ai) | Use your BazaarLink API key (starts with sk-bl-) in Authorization: Bearer <key>. OpenAI SDK works with base URL https://bazaarlink.ai/api/v1. Models use provider/model-name format. |
|
||||
| `bedrock` | `bedrock` | Amazon Bedrock | API key, enterprise | [link](https://aws.amazon.com/bedrock) | Use your Amazon Bedrock API key and configure the AWS region where your models are enabled (for example eu-west-2). OmniRoute calls Bedrock's native Converse API directly. |
|
||||
| `black-forest-labs` | `bfl` | Black Forest Labs | API key, image | [link](https://blackforestlabs.ai) | — |
|
||||
| `blackbox` | `bb` | Blackbox AI | API key | [link](https://blackbox.ai) | Limited free access is available through Blackbox; model availability and account limits apply |
|
||||
| `bluesminds` | `bm` | BluesMinds | API key | [link](https://www.bluesminds.com) | Free daily pi credits — supports 200+ models including GPT-4o, GPT-4.1, Claude Sonnet 4.5, Gemini 2.0 Flash, DeepSeek V4, Qwen, Kimi K2 |
|
||||
| `byteplus` | `bpm` | BytePlus ModelArk | API key | [link](https://console.byteplus.com/ark) | — |
|
||||
| `bytez` | `bytez` | Bytez | API key | [link](https://bytez.com) | $1 free credits, refreshes every 4 weeks |
|
||||
| `cerebras` | `cerebras` | Cerebras | API key | [link](https://inference.cerebras.ai) | Free Trial: 1M tokens/day, 30K TPM, 5 RPM — no credit card. |
|
||||
| `charm-hyper` | `charm-hyper` | Charm Hyper | API key | [link](https://hyper.charm.land) | 100 free monthly Hypercredits on signup |
|
||||
| `chat-oripe` | `chat-oripe` | Chat Oripe | API key, aggregator | [link](https://api.oriper.com) | Official metadata advertises 2M tokens/month, but the public site and documentation were blocked during audit; treat the quota and brand mapping as unconfirmed. |
|
||||
| `chatanywhere` | `chatanywhere` | ChatAnywhere | API key, aggregator | [link](https://chatanywhere.tech) | Personal, educational or research use only: public documentation cites 10,000 points/day and 200 requests/day per IP/key; do not use for commercial traffic. |
|
||||
| `cheaperinference` | `cinf` | Cheaper Inference | API key | [link](https://cheaperinference.com/?utm_source=omniroute) | — |
|
||||
| `chenzk` | `chenzk` | Chenzk API | API key | [link](https://chenzk.top) | — |
|
||||
| `chutes` | `chutes` | Chutes.ai | API key, aggregator | [link](https://chutes.ai) | Bearer API key for the Chutes OpenAI-compatible gateway. |
|
||||
| `clarifai` | `clarifai` | Clarifai | API key, enterprise | [link](https://docs.clarifai.com) | Use your Clarifai PAT or app-specific API key. OmniRoute targets the OpenAI-compatible endpoint at https://api.clarifai.com/v2/ext/openai/v1 and authenticates with Authorization: Key <token>. |
|
||||
| `cloudcode-one` | `cloudcode-one` | CloudCode.ONE | API key, aggregator | [link](https://cloudcode.one) | Published free models include glm-4.7-flash and glm-4.6v-flash; no numeric quota is published, and key creation may require credit or a coupon. |
|
||||
| `cloudflare-ai` | `cf` | Cloudflare Workers AI | API key | [link](https://developers.cloudflare.com/workers-ai) | Requires API Token AND Account ID (found at dash.cloudflare.com) |
|
||||
| `clova-studio` | `clova` | Naver CLOVA Studio | API key | [link](https://api.ncloud-docs.com/docs/en/ai-naver-clovastudio-summary) | — |
|
||||
| `codestral` | `codestral` | Codestral | API key | [link](https://mistral.ai) | — |
|
||||
| `cohere` | `cohere` | Cohere | API key | [link](https://cohere.com) | Free Trial: 1,000 API calls/month for testing, no credit card required |
|
||||
| `command-code` | `cmd` | Command Code | API key | [link](https://commandcode.ai/) | Use a Command Code API key. Requests are sent to Command Code's /alpha/generate endpoint. |
|
||||
| `coze` | `coze` | Coze | API key | [link](https://coze.com) | Get API key at coze.com/open/api |
|
||||
| `crof` | `crof` | CrofAI | API key | [link](https://crof.ai) | — |
|
||||
| `cursor-api` | `cua` | Cursor API | API key | [link](https://cursor.com/dashboard/api) | Paste a Cursor user API key (crsr_...) from cursor.com/dashboard/api. OmniRoute exchanges it for a session token on demand; no IDE or cursor-agent install is needed. Usage bills to the Cursor plan that owns the key. |
|
||||
| `dahl` | `dahl` | Dahl | API key | [link](https://inference.dahl.global) | Click 'Add Account' to auto-generate a token, or add a manual API key. |
|
||||
| `databricks` | `databricks` | Databricks | API key, enterprise | [link](https://www.databricks.com) | — |
|
||||
| `datarobot` | `datarobot` | DataRobot | API key, enterprise | [link](https://docs.datarobot.com) | Use your DataRobot API token. Optional Base URL can be the account root (for LLM Gateway) or a deployment URL under /api/v2/deployments/<id>. |
|
||||
| `deepai` | `deepai` | DeepAI | API key, image | [link](https://deepai.org) | Use your DeepAI API key. Get one at deepai.org — requires a Pro subscription ($9.99/mo). |
|
||||
| `deepinfra` | `deepinfra` | DeepInfra | API key | [link](https://deepinfra.com) | Free signup credits for API testing and model exploration |
|
||||
| `deepseek` | `ds` | DeepSeek | API key | [link](https://platform.deepseek.com) | 5M free tokens on signup - no credit card required |
|
||||
| `dgrid` | `dgrid` | DGrid | API key | [link](https://dgrid.ai) | DGrid Free Models Router: 10 requests/minute and 100 requests/day. A $5 lifetime top-up unlocks up to 20 requests/minute and 1,000 requests/day. |
|
||||
| `dify` | `dify` | Dify | API key | [link](https://dify.ai) | Get API key from your Dify instance. |
|
||||
| `digitalocean` | `digitalocean` | DigitalOcean | API key | [link](https://docs.digitalocean.com/products/ai-platform/) | — |
|
||||
| `dit` | `dai` | DIT.ai | API key | [link](https://dit.ai) | Use your dit.ai API key in Authorization: Bearer <key>. Fully OpenAI-compatible — a drop-in replacement, just change the base URL to https://api.dit.ai/v1. |
|
||||
| `doubao` | `doubao` | Doubao | API key | [link](https://doubao.com) | Get API key at console.volcengine.com |
|
||||
| `dxnt` | `dxnt` | DXNT / DX Token | API key, aggregator | [link](https://www.dxnt.com) | Free accounts are documented at 100 calls/day; the quota may increase through invitations and can vary by account. |
|
||||
| `electronhub` | `electronhub` | Electron Hub | API key, aggregator | [link](https://www.electronhub.ai) | Free plan: 5 RPM, $0.25 weekly credits and 10 Neutrinos/day for :free models; family budgets also apply. |
|
||||
| `empower` | `empower` | Empower | API key, aggregator | [link](https://docs.empower.dev) | Bearer API key for the Empower OpenAI-compatible endpoint. |
|
||||
| `factory` | `factory` | Factory | API key | [link](https://factory.ai) | Bearer API key for the Factory OpenAI-compatible gateway. |
|
||||
| `fal-ai` | `fal` | Fal.ai | API key, image | [link](https://fal.ai) | — |
|
||||
| `fastrouter` | `fastrouter` | FastRouter | API key, aggregator | [link](https://fastrouter.ai) | Models with the :free suffix allow 10 requests/day per organization and model; availability may change. |
|
||||
| `featherless-ai` | `featherless` | Featherless AI | API key | [link](https://featherless.ai) | Free tier available — no credit card required |
|
||||
| `fenayai` | `fenayai` | FenayAI | API key, aggregator | [link](https://fenayai.com) | Bearer API key for the FenayAI OpenAI-compatible gateway. |
|
||||
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
|
||||
| `free-ai` | `free-ai` | Free.ai | API key, aggregator | [link](https://free.ai) | 30,000 tokens/day cover self-hosted models after email verification. Usage beyond the pool can bill at raw cost, and premium external models are paid. |
|
||||
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
|
||||
| `freebuff` | `freebuff` | Freebuff | API key | [link](https://freebuff.com) | Enter Freebuff / Codebuff Auth Token (obtained via CLI login or automated harvester). |
|
||||
| `freeinference` | `freeinference` | FreeInference | API key, aggregator | [link](https://freeinference.org) | Free research access without a card; non-Harvard applicants require manual approval and no numeric quota is publicly guaranteed. |
|
||||
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
|
||||
| `freetheai` | `fta` | FreeTheAi | API key, aggregator | [link](https://freetheai.xyz) | Join the FreeTheAi Discord to get your free API key. |
|
||||
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
|
||||
| `g4f-gemini` | `g4fgem` | g4f.space — Gemini | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-groq` | `g4fgroq` | g4f.space — Groq | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-nvidia` | `g4fnv` | g4f.space — NVIDIA | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-ollama` | `g4foll` | g4f.space — Ollama | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `g4f-pollinations` | `g4fpol` | g4f.space — Pollinations | API key, aggregator | [link](https://g4f.space) | No auth required. Free tier is limited to 5 requests/minute — sign up at g4f.dev/members.html for higher limits. |
|
||||
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
|
||||
| `gemini` | `gemini` | Gemini (Google AI Studio) | API key | [link](https://aistudio.google.com) | Free tier available through Google AI Studio; current per-model quotas and regional limits apply |
|
||||
| `getgoapi` | `ggo` | GoAPI | API key, aggregator | [link](https://api.getgoapi.com) | — |
|
||||
| `gigachat` | `gigachat` | GigaChat (Sber) | API key | [link](https://developers.sber.ru) | — |
|
||||
| `gitlab` | `gitlab` | GitLab Duo PAT | API key | [link](https://docs.gitlab.com/user/duo_agent_platform/code_suggestions/) | GitLab personal access token for the public Code Suggestions API. Configure a self-hosted base URL when not using gitlab.com. |
|
||||
| `gitlawb` | `glb` | Gitlawb Opengateway (MiMo) | API key | [link](https://opengateway.gitlawb.com) | Free MiMo (xiaomi/mimo-v2.5) revoked 2026-05 — Opengateway is now a pay-as-you-go credit gateway; no recurring free model. |
|
||||
| `gitlawb-gmi` | `glb-gmi` | Gitlawb Opengateway (GMI Cloud) | API key | [link](https://opengateway.gitlawb.com) | Free Nemotron promo ended 2026-06 — the GMI Cloud route is now pay-as-you-go credit only. |
|
||||
| `glm` | `glm` | GLM Coding | API key | [link](https://z.ai/subscribe) | — |
|
||||
| `glm-cn` | `glmcn` | GLM Coding (China) | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `glmt` | `glmt` | GLM Thinking | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `groq` | `groq` | Groq | API key | [link](https://groq.com) | Free tier: 30 RPM / 14.4K RPD — no credit card |
|
||||
| `hackclub` | `hc` | Hackclub AI | API key, aggregator | [link](https://ai.hackclub.com) | Sign in with your Hack Club account at ai.hackclub.com. |
|
||||
| `haiper` | `hp` | Haiper | API key, video | [link](https://haiper.ai) | Get API key at haiper.ai/haiper-api |
|
||||
| `hcnsec` | `hcnsec` | Huancheng Public API | API key | [link](https://api.hcnsec.cn) | Get API key at api.hcnsec.cn |
|
||||
| `helixmind` | `helixmind` | HelixMind | API key, aggregator | [link](https://helixmind.online) | Previously circulated 3 RPM/50 RPD and no-card claims were not confirmed during the 2026-08-02 audit; current quota and billing require account verification. |
|
||||
| `helyxai` | `helyxai` | Helyx AI | API key, aggregator | [link](https://helyxai.space) | Operational Free plan documents 100,000 tokens/day; the site's separate 2M+ marketing claim conflicts and is not treated as a quota guarantee. |
|
||||
| `heroku` | `heroku` | Heroku AI | API key, enterprise | [link](https://www.heroku.com) | — |
|
||||
| `huggingface` | `hf` | HuggingFace | API key | [link](https://huggingface.co) | Free Inference API for thousands of models (Whisper, VITS, SDXL…) |
|
||||
| `hyperbolic` | `hyp` | Hyperbolic | API key | [link](https://hyperbolic.xyz) | $1-5 trial credits on signup for serverless inference |
|
||||
| `ideogram` | `ideo` | Ideogram | API key | [link](https://ideogram.ai) | Get API key at ideogram.ai/docs/api |
|
||||
| `iflytek` | `iflytek` | iFlytek Spark | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
|
||||
| `inception` | `inception` | Inception | API key | [link](https://docs.inceptionlabs.ai) | 10M free tokens on signup, no credit card required. |
|
||||
| `inference-net` | `inet` | Inference.net | API key | [link](https://inference.net) | $25 free credits on signup plus research grants available |
|
||||
| `internlm` | `internlm` | InternLM (Intern-S1) | API key | [link](https://internlm.intern-ai.org.cn/) | Free monthly quota ~1M input / 3M output tokens (~10 RPM) |
|
||||
| `jina-ai` | `jina` | Jina AI (Foundation API) | API key, embed/rerank | [link](https://jina.ai) | Bearer API key for api.jina.ai — embeddings, rerank, classify, segment, and search. Dashboard keys take precedence over JINA_AI_API_KEY. This is not the Reader / r.jina.ai card and does not fetch URLs. |
|
||||
| `jina-reader` | `jr` | Jina Reader (r.jina.ai) | API key | [link](https://jina.ai/reader) | Bearer API key for r.jina.ai URL-to-markdown (/v1/web/fetch only). Does not serve /v1/embeddings or /v1/rerank. The same Jina token as Foundation API works; OmniRoute reuses a jina-ai dashboard key or JINA_AI_API_KEY when this card is empty. |
|
||||
| `kenari` | `kenari` | Kenari | API key | [link](https://kenari.id) | Use your Kenari API key (kn-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://kenari.id/v1. |
|
||||
| `kie` | `kie` | KIE.AI | API key | [link](https://kie.ai) | — |
|
||||
| `kilo-gateway` | `kg` | Kilo Gateway | API key, aggregator | [link](https://kilo.ai) | — |
|
||||
| `kimi` | `kimi` | Kimi (Legacy Moonshot API) | API key | [link](https://platform.kimi.ai?aff=omniroute) | — |
|
||||
| `kimi-coding-apikey` | `kmca` | Kimi Code API Key | API key | [link](https://www.kimi.com/code?aff=omniroute) | — |
|
||||
| `lambda-ai` | `lambda` | Lambda AI | API key | [link](https://lambda.ai) | — |
|
||||
| `laozhang` | `lz` | LaoZhang AI | API key, aggregator | [link](https://api.laozhang.ai) | — |
|
||||
| `leonardo` | `leo` | Leonardo AI | API key, video | [link](https://leonardo.ai) | Get API key at leonardo.ai/developer |
|
||||
| `liquid` | `liquid` | Liquid AI | API key | [link](https://liquid.ai) | Get API key at liquid.ai |
|
||||
| `literouter` | `literouter` | LiteRouter | API key, aggregator | [link](https://literouter.com) | Free model variants use the :free suffix; daily credit limits vary by model and free input is capped at 5,000 tokens. |
|
||||
| `llamagate` | `llamagate` | LlamaGate | API key | [link](https://llamagate.ai) | — |
|
||||
| `llm-kiwi` | `llmkiwi` | LLM.Kiwi | API key, aggregator | [link](https://llm.kiwi) | Free plan exposes auto and hrLLM; the published 40 requests/hour limit applies to hrLLM. |
|
||||
| `llm7` | `llm7` | LLM7.io | API key | [link](https://llm7.io) | Use any non-empty key (for example 'unused'). If older built-in models return model_unavailable, use Available Models → Import from /models or Auto-Sync; verified live model: gemini-3.1-flash-lite. |
|
||||
| `llmgateway` | `llmgateway` | LLM Gateway | API key, aggregator | [link](https://llmgateway.io) | Hosted Free plan: free-priced models are limited to 5 requests per 10 minutes when the account has no credits. |
|
||||
| `logfare` | `logfare` | Logfare | API key, aggregator | [link](https://logfare.ai) | Create a free account at https://logfare.ai/register (username/password, no email verification) to get an instant API key, then paste it here as a Bearer token. |
|
||||
| `longcat` | `lc` | LongCat AI | API key | [link](https://longcat.chat/platform/docs) | Free: one-time 10M-token grant after account signup + KYC verification (LongCat-2.0). One-time only — not a recurring daily/monthly allowance. |
|
||||
| `magnific` | `freepik` | Magnific | API key, image | [link](https://www.magnific.com) | Get an API key at magnific.com/user/api-keys (header x-magnific-api-key). Legacy Freepik developer keys still work. |
|
||||
| `maritalk` | `maritalk` | Maritalk | API key | [link](https://www.maritaca.ai) | — |
|
||||
| `meganova-ai` | `meganova-ai` | MegaNova AI | API key, aggregator | [link](https://meganova.ai) | Free signup without a card. Published Tier 1 per-model quotas total 550 requests/day; they are not a shared global pool, and paid overage can apply if enabled. |
|
||||
| `meta-llama` | `meta` | Meta Llama API | API key | [link](https://llama.developer.meta.com) | — |
|
||||
| `minimax` | `minimax` | Minimax Coding | API key, video | [link](https://www.minimax.io) | — |
|
||||
| `minimax-cn` | `minimax-cn` | Minimax (China) | API key | [link](https://www.minimaxi.com) | — |
|
||||
| `mistral` | `mistral` | Mistral | API key | [link](https://mistral.ai) | Free Experiment tier: rate-limited access to all models, no credit card required |
|
||||
| `mixedbread` | `mxbai` | Mixedbread AI | API key | [link](https://www.mixedbread.com) | Bearer API key for the Mixedbread embeddings API. |
|
||||
| `mixlayer` | `mixlayer` | Mixlayer | API key, aggregator | [link](https://www.mixlayer.com) | The qwen/qwen3.5-4b-free model is free for prototyping and rate-limited; no fixed public RPM or daily quota is confirmed. |
|
||||
| `mnn-ai` | `mnn-ai` | MNN AI | API key, aggregator | [link](https://mnnai.ru) | Free plan: $1 monthly credits, 10 RPM and access only to models marked Free. |
|
||||
| `modal` | `mdl` | Modal | API key, enterprise | [link](https://modal.com/docs) | Use the bearer token that protects your Modal deployment, if enabled. Base URL should point to your OpenAI-compatible Modal app, for example https://<workspace>--<app>.modal.run/v1. |
|
||||
| `modelscope` | `ms` | ModelScope | API key | [link](https://modelscope.cn) | Free tier via ModelScope API-Inference — Alibaba account required. |
|
||||
| `monsterapi` | `monster` | MonsterAPI | API key | [link](https://monsterapi.ai) | ⚠️ **DEPRECATED.** Monster API shuttered operations on 2026-06-30. Use alternative OpenAI-compatible providers. |
|
||||
| `moonshot` | `moonshot` | Kimi | API key | [link](https://platform.kimi.ai?aff=omniroute) | — |
|
||||
| `morph` | `morph` | Morph | API key | [link](https://morphllm.com) | Free tier: 250K credits/month, $0 |
|
||||
| `muse-code` | `mc` | Muse Code (Meta) | API key | [link](https://github.com/meta-llama/llama-stack) | Use your META_API_KEY env var as a Bearer token. Muse Code CLI uses the OpenAI Responses API wire format (POST /responses). |
|
||||
| `naga-ac` | `naga` | Naga.ac | API key, aggregator | [link](https://naga.ac) | Get API key at naga.ac — Google/GitHub/Discord signup available. |
|
||||
| `naga-ai` | `naga-ai` | Naga AI | API key, aggregator | [link](https://naga.ac) | Models marked :free are publicly listed, but no numeric quota is confirmed. Naga's policy warns that free-tier prompts and outputs may be collected or used for training. |
|
||||
| `nanogpt` | `nanogpt` | NanoGPT | API key | [link](https://nano-gpt.com) | — |
|
||||
| `nara` | `nara` | NaraRouter | API key | [link](https://bynara.id) | Get a free API key via NaraRouter's Telegram channel, then paste it here as a Bearer token. |
|
||||
| `navy` | `navy` | NavyAI | API key | [link](https://api.navy) | Create a free API key from the NavyAI dashboard, then paste it here as a Bearer token. |
|
||||
| `nebius` | `nebius` | Nebius AI | API key | [link](https://nebius.com) | ~$1 trial credits on signup for API testing |
|
||||
| `nlpcloud` | `nlpc` | NLP Cloud | API key | [link](https://docs.nlpcloud.com) | Use your NLP Cloud API key in Authorization: Token <key>. OmniRoute targets the chatbot endpoint on https://api.nlpcloud.io/v1/gpu/<model>/chatbot by default. |
|
||||
| `nomic` | `nomic` | Nomic | API key | [link](https://nomic.ai) | Get API key at atlas.nomic.ai |
|
||||
| `nous-research` | `nous` | Nous Research | API key | [link](https://portal.nousresearch.com/help) | Use your Nous Portal API key. OmniRoute targets the official OpenAI-compatible inference endpoint at https://inference-api.nousresearch.com/v1. |
|
||||
| `novita` | `novita` | Novita AI | API key, video, aggregator | [link](https://novita.ai) | $0.50 trial credits on signup (valid about 1 year) |
|
||||
| `nscale` | `nscale` | nScale | API key | [link](https://nscale.com) | $5 free credits on signup for inference testing |
|
||||
| `nube` | `nube` | Nube.sh | API key | [link](https://nube.sh) | — |
|
||||
| `nvidia` | `nvidia` | NVIDIA NIM | API key | [link](https://build.nvidia.com) | Free dev access: ~40 RPM, 70+ models (Kimi K2.5, GLM 4.7, DeepSeek V3.2...) |
|
||||
| `oci` | `oci` | OCI Generative AI | API key, enterprise | [link](https://www.oracle.com/artificial-intelligence/generative-ai) | Use your OCI Generative AI API key or IAM bearer token. Base URL can be https://inference.generativeai.<region>.oci.oraclecloud.com/openai/v1/. |
|
||||
| `ofoxai` | `ofoxai` | OfoxAI | API key, aggregator | [link](https://ofox.ai) | The current catalog advertises 10+ free models without a public numeric quota; review upstream provenance, retention and training terms before production use. |
|
||||
| `ollama-cloud` | `ollamacloud` | Ollama Cloud | API key | [link](https://ollama.com/settings/keys) | — |
|
||||
| `openadapter` | `oad` | OpenAdapter | API key | [link](https://openadapter.dev) | Use your OpenAdapter API key in Authorization: Bearer sk-cv-<key>. Fully OpenAI-compatible. API base URL: https://api.openadapter.in/v1. |
|
||||
| `openai` | `openai` | OpenAI | API key | [link](https://platform.openai.com) | — |
|
||||
| `opencode-go` | `opencode-go` | OpenCode Go | API key | [link](https://opencode.ai/go) | — |
|
||||
| `opencode-zen` | `opencode-zen` | OpenCode Zen | API key | [link](https://opencode.ai/zen) | — |
|
||||
| `openference-api` | `ofa` | Openference API | API key | [link](https://openference.com) | Free plan: 3-day trial with open-source models — no credit card required |
|
||||
| `openrouter` | `openrouter` | OpenRouter | API key, aggregator | [link](https://openrouter.ai) | Free models at $0/token with :free suffix - 20 RPM / 200 RPD |
|
||||
| `openvecta` | `openvecta` | OpenVecta | API key | [link](https://openvecta.com) | Free credits on signup for OpenAI-compatible inference across LLMs, embeddings, and reasoning models |
|
||||
| `orcarouter` | `orcarouter` | OrcaRouter | API key | [link](https://www.orcarouter.ai) | — |
|
||||
| `ovhcloud` | `ovh` | OVHcloud AI | API key | [link](https://www.ovhcloud.com) | — |
|
||||
| `perplexity` | `pplx` | Perplexity | API key | [link](https://www.perplexity.ai) | — |
|
||||
| `piapi` | `pi` | PiAPI | API key, aggregator | [link](https://piapi.ai) | — |
|
||||
| `pioneer` | `pn` | Pioneer AI | API key | [link](https://pioneer.ai) | $75 free usage credits — no credit card required |
|
||||
| `plamo` | `plamo` | PLaMo | API key | [link](https://plamo.preferredai.jp/api) | — |
|
||||
| `poe` | `poe` | Poe | API key, aggregator | [link](https://creator.poe.com/api-reference) | Bearer API key for the Poe OpenAI-compatible API. |
|
||||
| `poixe-ai` | `poixe-ai` | Poixe AI | API key, aggregator | [link](https://poixe.com) | Current public free limits are small and model-group specific: 2 RPM/5 RPD for large-cup models and 20 RPM/50 RPD for small-cup models. |
|
||||
| `pollinations` | `pol` | Pollinations AI | API key, video | [link](https://pollinations.ai) | Anonymous/keyless access to the documented free models is best-effort. Local v3.8.50 verification (2026-07-31) returned 401 via OmniRoute and Cloudflare 1010 on direct upstream probes from the same network. Premium models still require a Pollinations API key from enter.pollinations.ai. |
|
||||
| `poolside` | `poolside` | Poolside | API key | [link](https://poolside.ai) | Laguna S 2.1 and XS 2.1 are free during Preview; no public numeric quota is published. |
|
||||
| `predibase` | `predibase` | Predibase | API key | [link](https://predibase.com) | ⚠️ **DEPRECATED.** serving.app.predibase.com no longer resolves (sweep 2026-06-19); the managed serving API appears discontinued. |
|
||||
| `publicai` | `publicai` | PublicAI | API key | [link](https://publicai.co) | Requires an API key — one-time signup credit, then paid |
|
||||
| `qianfan` | `qianfan` | Baidu Qianfan | API key | [link](https://cloud.baidu.com/product-s/qianfan_home) | — |
|
||||
| `qiniu` | `qiniu` | Qiniu | API key | [link](https://www.qiniu.com) | — |
|
||||
| `qwen-cloud` | `qwc` | Qwen Cloud | API key | [link](https://www.qwencloud.com/) | — |
|
||||
| `qwen-cloud-token-plan` | `qct` | Qwen Cloud Token Plan | API key | [link](https://www.qwencloud.com/pricing/token-plan) | — |
|
||||
| `recraft` | `recraft` | Recraft | API key, image | [link](https://recraft.ai) | — |
|
||||
| `regolo` | `regolo` | Regolo AI | API key | [link](https://regolo.ai) | Get your Regolo API key from regolo.ai, then paste it here as a Bearer token. |
|
||||
| `reka` | `reka` | Reka | API key | [link](https://docs.reka.ai/chat/overview) | Use your Reka API key. OmniRoute supports the OpenAI-compatible base URL https://api.reka.ai/v1 and sends both Authorization and X-Api-Key headers for compatibility. |
|
||||
| `requesty` | `requesty` | Requesty | API key | [link](https://requesty.ai) | Free tier ~200 requests/day - multi-model routing gateway (300+ models) |
|
||||
| `routeway` | `routeway` | Routeway | API key | [link](https://routeway.ai) | Create a free API key at routeway.ai, then paste it here as a Bearer token. |
|
||||
| `runwayml` | `runway` | Runway | API key, video | [link](https://docs.dev.runwayml.com) | Use your Runway API key in Authorization: Bearer <key>. OmniRoute targets the current Runway API at https://api.dev.runwayml.com/v1 and sends the required X-Runway-Version header automatically. |
|
||||
| `sambanova` | `samba` | SambaNova | API key | [link](https://sambanova.ai) | $5 free credits on signup (30-day validity), no credit card required |
|
||||
| `sap` | `sap` | SAP Generative AI Hub | API key, enterprise | [link](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/generative-ai-hub-in-sap-ai-core) | Use your SAP AI Core bearer token. Base URL can be your AI_API_URL root or a deploymentUrl from Generative AI Hub. |
|
||||
| `sarvam` | `sarvam` | Sarvam AI | API key | [link](https://docs.sarvam.ai) | ₹1,000 in free signup credits — never expire |
|
||||
| `scaleway` | `scw` | Scaleway AI | API key | [link](https://www.scaleway.com/en/docs/ai-data/generative-apis/) | 1M free tokens for new accounts — EU/GDPR compliant (Paris), Qwen3 235B & Llama 70B |
|
||||
| `sealion` | `sealion` | SEA-LION | API key | [link](https://sea-lion.ai) | Sign in at sea-lion.ai with Google (no card, no region wall), create an API key, then paste it here. |
|
||||
| `segmind` | `segmind` | Segmind | API key, image, video | [link](https://segmind.com) | Use your Segmind API key in the x-api-key header. OmniRoute targets https://api.segmind.com/v1/<model> and returns the generated image/video bytes directly. |
|
||||
| `sensenova` | `sensenova` | SenseNova | API key | [link](https://platform.sensenova.cn) | Get API key at platform.sensenova.cn |
|
||||
| `siliconflow` | `siliconflow` | SiliconFlow | API key | [link](https://cloud.siliconflow.com) | $1 free credits plus currently listed $0 models after identity verification; availability and limits may change |
|
||||
| `snowflake` | `snowflake` | Snowflake Cortex | API key, enterprise | [link](https://www.snowflake.com) | — |
|
||||
| `sparkdesk` | `sparkdesk` | SparkDesk | API key | [link](https://xinghuo.xfyun.cn) | Get API key at console.xfyun.cn |
|
||||
| `speka` | `speka` | Speka AI | API key, aggregator | [link](https://speka.me) | Free plan: $1 monthly usage, 10 RPM, one API key and access to open models and the playground; no card required. |
|
||||
| `stability-ai` | `stability` | Stability AI | API key, image | [link](https://stability.ai) | — |
|
||||
| `stepfun` | `stepfun` | StepFun | API key | [link](https://stepfun.com) | Get API key at platform.stepfun.com |
|
||||
| `sumopod` | `sumopod` | SumoPod | API key | [link](https://ai.sumopod.com) | Use your SumoPod API key (sk-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://ai.sumopod.com/v1. |
|
||||
| `suno` | `suno` | Suno | API key | [link](https://suno.ai) | Paste session cookie from suno.ai (Clerk auth) |
|
||||
| `synthetic` | `synthetic` | Synthetic | API key, aggregator | [link](https://synthetic.new) | — |
|
||||
| `tabitoken` | `tabitoken` | TabiToken | API key, aggregator | [link](https://tabitoken.com) | — |
|
||||
| `tencent` | `tencent` | Tencent Hunyuan | API key | [link](https://hunyuan.tencent.com) | Get API key at console.cloud.tencent.com |
|
||||
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
|
||||
| `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys |
|
||||
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
|
||||
| `token-kiosk` | `tk` | Token Kiosk | API key | [link](https://agent-router.gaib.ai) | Use your Token Kiosk API key in Authorization: Bearer <key>. Fully OpenAI-compatible gateway. API base URL: https://agent-router.gaib.ai/v1. |
|
||||
| `tokenreply` | `tokenreply` | TokenReply | API key, aggregator | [link](https://www.tokenreply.com) | Free-tagged models have model- and campaign-specific daily limits; no fixed global free quota is published. |
|
||||
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
|
||||
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
|
||||
| `typhoon` | `typhoon` | Typhoon | API key | [link](https://docs.opentyphoon.ai) | Free API key with a 5 req/s and 200 req/m rate limit. |
|
||||
| `udio` | `udio` | Udio | API key | [link](https://udio.com) | Paste session cookie from udio.com (Supabase auth) |
|
||||
| `uncloseai` | `unc` | UncloseAI | API key | [link](https://uncloseai.com) | No auth required. API accepts any non-empty string as key for identification. If older built-in models return 404, use Available Models → Import from /models or Auto-Sync; verified live model: solidrust/Hermes-3-Llama-3.1-8B-AWQ. |
|
||||
| `unorouter` | `unorouter` | UnoRouter | API key, aggregator | [link](https://unorouter.ai) | Models with the :free suffix do not debit balance; limit is 1 request/minute per free model per user. |
|
||||
| `upstage` | `upstage` | Upstage | API key | [link](https://www.upstage.ai) | — |
|
||||
| `v0-vercel` | `v0` | v0 (Vercel) | API key | [link](https://v0.dev) | — |
|
||||
| `venice` | `venice` | Venice.ai | API key | [link](https://venice.ai) | — |
|
||||
| `vercel-ai-gateway` | `vag` | Vercel AI Gateway | API key, aggregator | [link](https://vercel.com/docs/ai-gateway) | — |
|
||||
| `vertex` | `vertex` | Vertex AI | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide Service Account JSON or OAuth access_token |
|
||||
| `vertex-partner` | `vp` | Vertex AI Partners | API key, enterprise | [link](https://cloud.google.com/vertex-ai) | Provide the same Service Account JSON used for Vertex AI partner models. |
|
||||
| `void-ai` | `void-ai` | Void AI | API key, aggregator | [link](https://voidai.app) | The public model catalog marks some models with a free plan requirement, but access is conditional and no numeric quota is confirmed. |
|
||||
| `volcengine` | `volcengine` | Volcengine | API key | [link](https://www.volcengine.com) | — |
|
||||
| `voyage-ai` | `voyage` | Voyage AI | API key, embed/rerank | [link](https://www.voyageai.com) | Bearer API key for Voyage AI embeddings and rerank APIs. |
|
||||
| `wafer` | `wafer` | Wafer AI | API key | [link](https://wafer.ai) | — |
|
||||
| `wandb` | `wandb` | Weights & Biases Inference | API key | [link](https://wandb.ai) | — |
|
||||
| `watsonx` | `watsonx` | IBM watsonx.ai Gateway | API key, enterprise | [link](https://www.ibm.com/products/watsonx-ai) | Use your watsonx bearer token. Base URL can be https://<region>.ml.cloud.ibm.com/ml/gateway/v1/ or a self-managed /ml/gateway/v1 endpoint. |
|
||||
| `writer` | `writer` | Writer | API key | [link](https://dev.writer.com) | — |
|
||||
| `x5lab` | `x5lab` | X5Lab | API key | [link](https://x5lab.dev) | Use your X5Lab API key (x5-...) in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.x5lab.dev/v1. |
|
||||
| `xai` | `xai` | xAI (Grok) | API key | [link](https://x.ai) | Use an official xAI API key, or sign in with xAI OAuth. Grok Build JWT sessions remain a separate provider. |
|
||||
| `xiaomi-mimo` | `mimo` | Xiaomi MiMo | API key | [link](https://mimo.mi.com) | — |
|
||||
| `xiaomi-mimo-token-plan` | `mimotp` | Xiaomi MiMo Token Plan | API key | [link](https://mimo.mi.com) | — |
|
||||
| `yi` | `yi` | Yi (01.AI) | API key | [link](https://01.ai) | Get API key at platform.lingyiwanwu.com |
|
||||
| `yolo-auto` | `yolo-auto` | Yolo-Auto | API key, aggregator | [link](https://yolo-auto.com) | Free API access is request-limited and intended for testing; no numeric daily quota is published and free access is not promised indefinitely. |
|
||||
| `zai` | `zai` | Z.AI | API key | [link](https://open.bigmodel.cn) | — |
|
||||
| `zenmux` | `zm` | ZenMux | API key | [link](https://zenmux.ai) | Use your ZenMux API key in Authorization: Bearer <key>. ZenMux is fully OpenAI-compatible. Base URL: https://zenmux.ai/api/v1. |
|
||||
| `zerolimitai` | `zerolimitai` | ZeroLimitAI | API key, aggregator | [link](https://www.zerolimitai.com) | Temporary free trial is advertised, but official pages conflict between 3 and 7 days; a 100-calls/day claim is not treated as permanent. |
|
||||
| `zylo-api` | `zylo` | Zylo API | API key, aggregator | [link](https://zyloai.net) | Basic plan: 10 RPM, 7,200 requests/day and 200,000 tokens/day; limited to Basic text models. |
|
||||
|
||||
## Local Providers (14)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `comfyui` | `comfyui` | ComfyUI | Local | [link](https://github.com/comfyanonymous/ComfyUI) | No API key required. Configure the local ComfyUI base URL (default: http://localhost:8188). |
|
||||
| `docker-model-runner` | `dmr` | Docker Model Runner | Local, self-hosted | [link](https://docs.docker.com/ai/model-runner/) | API key optional. Configure the local Docker Model Runner OpenAI-compatible base URL (default: http://localhost:12434/v1). |
|
||||
| `lemonade` | `lemonade` | Lemonade Server | Local, self-hosted | [link](https://lemonade-server.ai) | API key optional. Configure the local Lemonade OpenAI-compatible base URL (default: http://localhost:13305/api/v1). |
|
||||
| `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. |
|
||||
| `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). |
|
||||
| `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). |
|
||||
| `mlx-gemma` | `mlx-gemma` | MLX Gemma 26B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11435. Requires uv and mlx-lm installed. Model: mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned (~15.9GB peak memory). |
|
||||
| `mlx-qwen` | `mlx-qwen` | MLX Qwen 3.8 27B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11436. Requires uv and mlx-lm installed. Model: maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw (~13.1GB peak memory). |
|
||||
| `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. |
|
||||
| `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). |
|
||||
| `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). |
|
||||
| `triton` | `triton` | NVIDIA Triton | Local, self-hosted | [link](https://developer.nvidia.com/triton-inference-server) | API key optional. Configure the Triton OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `vllm` | `vllm` | vLLM | Local, self-hosted | [link](https://github.com/vllm-project/vllm) | API key optional. Configure the local vLLM OpenAI-compatible base URL (default: http://localhost:8000/v1). |
|
||||
| `xinference` | `xinference` | XInference | Local, self-hosted | [link](https://inference.readthedocs.io) | API key optional. Configure the local XInference OpenAI-compatible base URL (default: http://localhost:9997/v1). |
|
||||
|
||||
## Search Providers (13)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `brave-search` | `brave-search` | Brave Search | Search | [link](https://brave.com/search/api) | Subscription token from Brave Search API dashboard |
|
||||
| `exa-search` | `exa-search` | Exa Search | Search | [link](https://exa.ai) | API key from dashboard.exa.ai |
|
||||
| `firecrawl` | `fc` | Firecrawl | Search | [link](https://firecrawl.dev) | API key from firecrawl.dev/app/api-keys (or set your self-hosted Firecrawl base URL) |
|
||||
| `google-pse-search` | `google-pse` | Google Programmable Search | Search | [link](https://developers.google.com/custom-search/v1/overview) | Requires a Google API key and your Programmable Search Engine ID (cx) |
|
||||
| `linkup-search` | `linkup` | Linkup Search | Search | [link](https://docs.linkup.so) | Bearer API key from the Linkup dashboard |
|
||||
| `ollama-search` | `ollama-search` | Ollama Search | Search | [link](https://ollama.com/settings/keys) | Same API key as Ollama Cloud (from ollama.com/settings/keys) |
|
||||
| `perplexity-search` | `pplx-search` | Perplexity Search | Search | [link](https://docs.perplexity.ai/guides/search-quickstart) | Same API key as Perplexity (pplx-...) |
|
||||
| `searchapi-search` | `searchapi` | SearchAPI | Search | [link](https://www.searchapi.io/docs/google) | API key from SearchAPI (query param or Bearer auth) |
|
||||
| `searxng-search` | `searxng` | SearXNG Search | Search | [link](https://docs.searxng.org) | API key is optional. Set your SearXNG base URL. Some instances may require a bearer token for access. |
|
||||
| `serper-search` | `serper-search` | Serper Search | Search | [link](https://serper.dev) | API key from serper.dev dashboard |
|
||||
| `tavily-search` | `tavily-search` | Tavily Search | Search | [link](https://tavily.com) | API key from app.tavily.com (format: tvly-...) |
|
||||
| `x-search` | `x_search` | X Search (Grok) | Search | [link](https://docs.x.ai/developers/tools/x-search) | SuperGrok OAuth (xai-oauth) or xAI API key. This is Grok X Search, not the X Developer MCP. |
|
||||
| `youcom-search` | `youcom-search` | You.com Search | Search | [link](https://you.com/business/api/) | X-API-Key from the You.com platform dashboard |
|
||||
|
||||
## Audio-only Providers (12)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `assemblyai` | `aai` | AssemblyAI | Audio | [link](https://assemblyai.com) | — |
|
||||
| `aws-polly` | `polly` | AWS Polly | Audio | [link](https://aws.amazon.com/polly/) | Use AWS Secret Access Key as API key; set providerSpecificData.accessKeyId and optional region. |
|
||||
| `cartesia` | `cartesia` | Cartesia | Audio | [link](https://cartesia.ai) | — |
|
||||
| `deepgram` | `dg` | Deepgram | Audio | [link](https://deepgram.com) | — |
|
||||
| `elevenlabs` | `el` | ElevenLabs | Audio | [link](https://elevenlabs.io) | — |
|
||||
| `fishaudio` | `fishaudio` | Fish Audio | Audio | [link](https://fish.audio) | — |
|
||||
| `gladia` | `gladia` | Gladia | Audio | [link](https://gladia.io) | — |
|
||||
| `inworld` | `inworld` | Inworld | Audio | [link](https://inworld.ai) | — |
|
||||
| `playht` | `playht` | PlayHT | Audio | [link](https://play.ht) | — |
|
||||
| `rev-ai` | `revai` | Rev AI | Audio | [link](https://www.rev.ai) | — |
|
||||
| `soniox` | `sx` | Soniox | Audio | [link](https://soniox.com) | — |
|
||||
| `speechmatics` | `sm` | Speechmatics | Audio | [link](https://www.speechmatics.com) | Free tier — 8 hours/month, no credit card required. Batch (async) mode only. |
|
||||
|
||||
## Upstream Proxy Providers (2)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `9router` | `nr` | 9router | Upstream proxy | [link](https://www.npmjs.com/package/9router) | — |
|
||||
| `cliproxyapi` | `cpa` | CLIProxyAPI | Upstream proxy | [link](https://github.com/router-for-me/CLIProxyAPI) | — |
|
||||
|
||||
## Cloud Agent Providers (3)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `codex-cloud` | `codex-cloud` | Codex Cloud | Cloud agent | [link](https://openai.com/codex) | OpenAI API key with Codex Cloud task access. |
|
||||
| `devin` | `devin` | Devin | Cloud agent | [link](https://devin.ai) | Devin API key for cloud agent sessions. |
|
||||
| `jules` | `jules` | Google Jules | Cloud agent | [link](https://jules.google) | Jules API key for creating and managing cloud coding tasks. |
|
||||
|
||||
## System Providers (1)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
| `auto` | `auto` | Auto (Zero-Config) | System | — | — |
|
||||
|
||||
## Sources of truth
|
||||
|
||||
- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)
|
||||
- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)
|
||||
- Executors: [`open-sse/executors/`](../../open-sse/executors/) (106 implementations)
|
||||
- Translators: [`open-sse/translator/`](../../open-sse/translator/)
|
||||
|
||||
## See Also
|
||||
|
||||
- [FREE_TIERS.md](./FREE_TIERS.md) — curated free-tier guide
|
||||
- [USER_GUIDE.md](../guides/USER_GUIDE.md) — provider setup walkthrough
|
||||
- [ARCHITECTURE.md](../architecture/ARCHITECTURE.md) — overall architecture
|
||||
49
README.md
49
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 346 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 346 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 350 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 350 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<tr>
|
||||
<td align="right"><b>⚙️ Features</b></td>
|
||||
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
|
||||
<td align="center"><a href="#-346-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-349-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI & MCP</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 346 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 346 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 57 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 350 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 350 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 346 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 350 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -559,7 +559,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **346-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **350-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
|
||||
@@ -612,7 +612,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
<b>+ also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
|
||||
</div>
|
||||
|
||||
<sub>📖 Per-tool setup for all 34 tools (26 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
<sub>📖 Per-tool setup for all 35 tools (26 CLI Code's + 9 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 346 AI Providers — 90+ Free
|
||||
## 🌐 349 AI Providers — 90+ Free
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **57 free forever**.
|
||||
> The most complete catalog of any open-source router: **350 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -821,7 +821,7 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
|
||||
<table>
|
||||
<tr><th align="left">Interface</th><th align="left">Endpoint / command</th><th align="left">Use it for</th></tr>
|
||||
<tr><td align="left" nowrap>🧰 <b>MCP (stdio)</b></td><td align="left" nowrap><code>omniroute --mcp</code></td><td align="left">Plug into Claude Desktop, Cursor, any MCP client</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>109 tools</b>, 33 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>110 tools</b>, 33 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>📡 <b>MCP (SSE)</b></td><td align="left" nowrap><code>/api/mcp/sse</code></td><td align="left">Streaming MCP transport</td></tr>
|
||||
<tr><td align="left" nowrap>🤝 <b>A2A</b></td><td align="left" nowrap><code>/.well-known/agent.json</code></td><td align="left">Agent-to-agent, <b>JSON-RPC 2.0</b> + SSE, 6 skills</td></tr>
|
||||
<tr><td align="left" nowrap>🌐 <b>REST API</b></td><td align="left" nowrap><code>/v1/*</code></td><td align="left">OpenAI-compatible — chat, embeddings, images, audio, OCR</td></tr>
|
||||
@@ -988,7 +988,21 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
|
||||
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
|
||||
`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).The image pins **`OMNIROUTE_MEMORY_MB=1024`**. That is enough for the dashboard and a light chat. **Coding agents** (`POST /v1/responses` from Claude Code, Codex, Grok, …) need a much larger V8 heap or the process `FATAL ERROR`s at ~12 GiB under two overlapping long contexts. Size the container above the heap (native buffers sit outside V8):
|
||||
|
||||
| Workload | Heap (`-e OMNIROUTE_MEMORY_MB`) | Container (`--memory`) |
|
||||
| ----------------------------------- | ------------------------------- | ---------------------- |
|
||||
| Dashboard / light chat | `1024` (image default) | ≥2 g |
|
||||
| One coding agent | `8192` | ≥10 g |
|
||||
| Two concurrent long `/v1/responses` | `10240`–`12288` | ≥12–16 g |
|
||||
|
||||
```bash
|
||||
docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
|
||||
-e OMNIROUTE_MEMORY_MB=8192 --memory=10g \
|
||||
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
Full table: [Docker Guide — runtime RAM](docs/guides/DOCKER_GUIDE.md#runtime-ram-for-coding-agents).
|
||||
|
||||
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
|
||||
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
|
||||
@@ -996,6 +1010,19 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
|
||||
> are **not supported for production**. See
|
||||
> [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
|
||||
|
||||
**🥟 Bun**
|
||||
|
||||
Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
|
||||
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
|
||||
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
|
||||
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).
|
||||
|
||||
```bash
|
||||
# Install and run with Bun
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
**🛠️ From source**
|
||||
|
||||
```bash
|
||||
@@ -1174,7 +1201,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 157 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 159 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
|
||||
@@ -30,20 +30,60 @@ export function register_combos(parent) {
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
tag.command("patch-api-combos-id-")
|
||||
.description("Update combo")
|
||||
tag.command("get-api-combos-id-")
|
||||
.description("Get combo by ID")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
tag.command("put-api-combos-id-")
|
||||
.description("Update combo")
|
||||
.requiredOption("--id <id>", "")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
let body;
|
||||
if (opts.body) {
|
||||
body = opts.body.startsWith("@")
|
||||
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
|
||||
: JSON.parse(opts.body);
|
||||
}
|
||||
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
tag.command("patch-api-combos-id-")
|
||||
.description("Update combo")
|
||||
.requiredOption("--id <id>", "")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
let body;
|
||||
if (opts.body) {
|
||||
body = opts.body.startsWith("@")
|
||||
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
|
||||
: JSON.parse(opts.body);
|
||||
}
|
||||
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
tag.command("delete-api-combos-id-")
|
||||
.description("Delete combo")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
|
||||
@@ -22,8 +22,15 @@ const VALID_FORMATS = new Set(["json", "env"]);
|
||||
const SECURE_FILE_MODE = 0o600;
|
||||
|
||||
export function registerAuthExport(program) {
|
||||
// #11226: `.command("auth export")` does NOT register a two-word command — commander
|
||||
// parses the bare word `export` as a required positional argument of `auth`, so the
|
||||
// action received (exportArgValue, options, command) while expecting (options, command)
|
||||
// and crashed with "cmd.optsWithGlobals is not a function". Register `export` as a
|
||||
// proper nested subcommand instead; the CLI surface stays `omniroute auth export`.
|
||||
program
|
||||
.command("auth export")
|
||||
.command("auth")
|
||||
.description(t("authExport.description"))
|
||||
.command("export")
|
||||
.description(t("authExport.description"))
|
||||
.option("--id <id>", t("authExport.idOpt"))
|
||||
.option("--format <format>", t("authExport.formatOpt"), "json")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { withRuntime } from "../runtime.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { resolveComboModels, collectModel } from "./comboModels.mjs";
|
||||
|
||||
const VALID_STRATEGIES = [
|
||||
"priority",
|
||||
@@ -125,10 +126,31 @@ export function registerCombo(program) {
|
||||
.choices(VALID_STRATEGIES)
|
||||
.default("priority")
|
||||
)
|
||||
.option(
|
||||
"--models <spec>",
|
||||
"Models for the combo: comma-separated provider/model entries, or a JSON array " +
|
||||
'(e.g. --models "openai/gpt-4o,anthropic/claude-3-opus" or ' +
|
||||
'--models \'[{"model":"gpt-4o","providerId":"openai"}]\')'
|
||||
)
|
||||
.option(
|
||||
"--model <spec>",
|
||||
"Add one model to the combo (provider/model or bare model id) — repeatable",
|
||||
collectModel,
|
||||
[]
|
||||
)
|
||||
.action(async (name, opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
let models;
|
||||
try {
|
||||
models = resolveComboModels(opts);
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
const exitCode = await runComboCreateCommand(name, opts.strategy, {
|
||||
...opts,
|
||||
models,
|
||||
output: globalOpts.output,
|
||||
});
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
@@ -284,12 +306,20 @@ export async function runComboCreateCommand(name, strategy = "priority", opts =
|
||||
return 1;
|
||||
}
|
||||
|
||||
const models = Array.isArray(opts.models) ? opts.models : [];
|
||||
if (!models.length) {
|
||||
console.error(
|
||||
"combo create requires at least one target. Pass --models <provider/model,...> and/or repeat --model <provider/model>."
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
return await withRuntime(async ({ kind, api, db }) => {
|
||||
if (kind === "http") {
|
||||
const res = await api("/api/combos", {
|
||||
method: "POST",
|
||||
body: { name, strategy, enabled: true, models: [], config: {} },
|
||||
body: { name, strategy, enabled: true, models, config: {} },
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
@@ -305,7 +335,7 @@ export async function runComboCreateCommand(name, strategy = "priority", opts =
|
||||
console.error(`Combo '${name}' already exists. Delete it first.`);
|
||||
return 1;
|
||||
}
|
||||
await db.combos.createCombo({ name, strategy, enabled: true, models: [], config: {} });
|
||||
await db.combos.createCombo({ name, strategy, enabled: true, models, config: {} });
|
||||
}
|
||||
|
||||
console.log(t("combo.created", { name }));
|
||||
|
||||
142
bin/cli/commands/comboModels.mjs
Normal file
142
bin/cli/commands/comboModels.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
// Parses the `--models` / `--model` options for `omniroute combo create` (#10954).
|
||||
//
|
||||
// Root cause of #10954: `combo create` only ever registered `--strategy`; the
|
||||
// HTTP body (POST /api/combos) and the local-db fallback (db.combos.createCombo)
|
||||
// both hardcoded `models: []`, so every combo created via the CLI came out
|
||||
// empty regardless of what the operator intended to route to.
|
||||
//
|
||||
// Accepted shapes mirror the server-side Zod union in
|
||||
// `src/shared/validation/schemas/combo.ts` (`comboModelEntry` /
|
||||
// `createComboSchema.models`) so a CLI-built payload never gets rejected by
|
||||
// the API that ultimately validates it:
|
||||
// - a plain string ("provider/model" or a bare model id) — the server's
|
||||
// `normalizeComboModels` (src/lib/combos/steps.ts) already splits the
|
||||
// leading "provider/" segment off a plain string, so passing the raw
|
||||
// token through is sufficient for the common case;
|
||||
// - a structured `{ kind?: "model", model, providerId?, provider?, ... }`
|
||||
// object;
|
||||
// - a structured `{ kind: "combo-ref", comboName, ... }` object (nested
|
||||
// combo reference).
|
||||
//
|
||||
// The CLI (bin/cli/**) ships as plain `.mjs` with relative-only imports — no
|
||||
// `@/` path aliases and no TS transpilation at runtime — so importing the
|
||||
// real Zod schema from `src/shared/validation/schemas/combo.ts` is not
|
||||
// viable here. This module instead validates the same minimal shape by hand
|
||||
// and stays a thin, independently testable unit.
|
||||
|
||||
/**
|
||||
* Validates one already-parsed combo model entry against the shape accepted
|
||||
* by `comboModelEntry` (string | model-step | combo-ref). Throws with a
|
||||
* 1-based, human-readable position when the entry does not match.
|
||||
*
|
||||
* @param {unknown} entry
|
||||
* @param {number} index
|
||||
* @returns {string | Record<string, unknown>}
|
||||
*/
|
||||
export function validateComboModelEntryShape(entry, index) {
|
||||
const position = index + 1;
|
||||
|
||||
if (typeof entry === "string") {
|
||||
const trimmed = entry.trim();
|
||||
if (trimmed.length === 0) {
|
||||
throw new Error(`--models entry #${position}: empty model string`);
|
||||
}
|
||||
if (trimmed.length > 300) {
|
||||
throw new Error(`--models entry #${position}: model string exceeds 300 characters`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
||||
throw new Error(`--models entry #${position}: must be a string or a JSON object`);
|
||||
}
|
||||
|
||||
const kind = entry.kind;
|
||||
|
||||
if (kind === "combo-ref") {
|
||||
if (typeof entry.comboName !== "string" || entry.comboName.trim().length === 0) {
|
||||
throw new Error(
|
||||
`--models entry #${position}: kind "combo-ref" requires a non-empty "comboName"`
|
||||
);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (kind !== undefined && kind !== "model") {
|
||||
throw new Error(`--models entry #${position}: unknown "kind" value ${JSON.stringify(kind)}`);
|
||||
}
|
||||
|
||||
if (typeof entry.model !== "string" || entry.model.trim().length === 0) {
|
||||
throw new Error(`--models entry #${position}: requires a non-empty "model"`);
|
||||
}
|
||||
if (entry.providerId !== undefined && typeof entry.providerId !== "string") {
|
||||
throw new Error(`--models entry #${position}: "providerId" must be a string`);
|
||||
}
|
||||
if (entry.provider !== undefined && typeof entry.provider !== "string") {
|
||||
throw new Error(`--models entry #${position}: "provider" must be a string`);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one `--models` spec — either a JSON array (`--models '[{"model":"gpt-4o"}]'`)
|
||||
* or a comma-separated list of provider/model tokens
|
||||
* (`--models 'openai/gpt-4o,anthropic/claude-3-opus'`) — into an array of
|
||||
* combo model entries.
|
||||
*
|
||||
* @param {string} spec
|
||||
* @returns {Array<string | Record<string, unknown>>}
|
||||
*/
|
||||
export function parseModelsSpec(spec) {
|
||||
const trimmed = String(spec ?? "").trim();
|
||||
if (trimmed.length === 0) return [];
|
||||
|
||||
if (trimmed.startsWith("[")) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (err) {
|
||||
throw new Error(`--models: invalid JSON array (${err.message})`);
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("--models: JSON value must be an array");
|
||||
}
|
||||
return parsed.map((entry, i) => validateComboModelEntryShape(entry, i));
|
||||
}
|
||||
|
||||
return trimmed
|
||||
.split(",")
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0)
|
||||
.map((token, i) => validateComboModelEntryShape(token, i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the final `models` array for `combo create` from Commander opts:
|
||||
* `--models <csv-or-json>` and/or repeatable `--model <spec>`.
|
||||
*
|
||||
* @param {{ models?: string, model?: string[] }} opts
|
||||
* @returns {Array<string | Record<string, unknown>>}
|
||||
*/
|
||||
export function resolveComboModels(opts = {}) {
|
||||
const result = [];
|
||||
|
||||
if (typeof opts.models === "string" && opts.models.trim().length > 0) {
|
||||
result.push(...parseModelsSpec(opts.models));
|
||||
}
|
||||
|
||||
if (Array.isArray(opts.model)) {
|
||||
opts.model.forEach((token, i) => {
|
||||
result.push(validateComboModelEntryShape(String(token).trim(), i));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Commander `collect`-style reducer for the repeatable `--model` option. */
|
||||
export function collectModel(value, previous) {
|
||||
previous.push(value);
|
||||
return previous;
|
||||
}
|
||||
@@ -228,20 +228,37 @@ async function runSocialFlow(def, opts) {
|
||||
|
||||
async function runDeviceFlow(def, opts) {
|
||||
const providerKey = resolveBackendKey(def.id);
|
||||
const startRes = await apiFetch(`/api/providers/${providerKey}/auth/start`, {
|
||||
...targetApiOptions(opts),
|
||||
method: "POST",
|
||||
});
|
||||
let startRes = await apiFetch(`/api/oauth/${providerKey}/device-code`, targetApiOptions(opts));
|
||||
if (!startRes.ok) {
|
||||
startRes = await apiFetch(`/api/providers/${providerKey}/auth/start`, {
|
||||
...targetApiOptions(opts),
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
if (!startRes.ok) {
|
||||
process.stderr.write(`Failed to start device flow: ${startRes.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const start = await startRes.json();
|
||||
process.stdout.write(
|
||||
`\nDevice code: ${start.userCode ?? start.user_code ?? ""}\nVisit: ${start.verificationUri ?? start.verification_uri}\n\n`
|
||||
);
|
||||
if (opts.browser !== false)
|
||||
await openBrowser(start.verificationUri ?? start.verification_uri ?? "");
|
||||
const userCode = start.userCode ?? start.user_code ?? "";
|
||||
const verificationUri =
|
||||
start.verificationUriComplete ??
|
||||
start.verification_uri_complete ??
|
||||
start.verificationUri ??
|
||||
start.verification_uri ??
|
||||
start.authUrl ??
|
||||
start.url ??
|
||||
"";
|
||||
|
||||
if (userCode) {
|
||||
process.stdout.write(`\nDevice code: ${userCode}\nVisit: ${verificationUri}\n\n`);
|
||||
} else if (verificationUri) {
|
||||
process.stdout.write(`\nVisit: ${verificationUri}\n\n`);
|
||||
} else {
|
||||
process.stdout.write(`\nAuthorization URL not available\n\n`);
|
||||
}
|
||||
|
||||
if (opts.browser !== false && verificationUri) await openBrowser(verificationUri);
|
||||
process.stderr.write("Waiting for device authorization...\n");
|
||||
const deadline = Date.now() + (opts.timeout ?? 300000);
|
||||
const intervalMs = (start.intervalMs ?? start.interval ?? 5) * 1000;
|
||||
@@ -302,7 +319,18 @@ export async function runOAuthStatus(opts, cmd) {
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json();
|
||||
const connections = (data.connections ?? data.providers ?? data.items ?? data).filter(
|
||||
const payload = data?.connections ?? data?.providers ?? data?.items ?? data;
|
||||
// #11236 (bug 5 residual): a 200 whose body is out of contract (no
|
||||
// connections/providers/items array — e.g. `{"status":"ok"}`) used to fall
|
||||
// through to `.filter` on a non-array and crash with a bare TypeError plus a
|
||||
// libuv teardown assertion on Windows. Coerce to an empty list with a
|
||||
// sanitized one-line warning instead of dumping a stack trace.
|
||||
if (!Array.isArray(payload)) {
|
||||
process.stderr.write(
|
||||
"Warning: unexpected response shape from /api/providers; showing no connections.\n"
|
||||
);
|
||||
}
|
||||
const connections = (Array.isArray(payload) ? payload : []).filter(
|
||||
(c) => c.authType === "oauth" || c.authType === "oauth2"
|
||||
);
|
||||
emit(connections, globalOpts, connectionSchema);
|
||||
|
||||
@@ -9,10 +9,13 @@ import { discoverPlugins } from "../plugins.mjs";
|
||||
// (instead of string-interpolating into `execSync`) prevents a malicious plugin
|
||||
// name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell.
|
||||
function runNpm(args) {
|
||||
const res = spawnSync("npm", args, { stdio: "inherit", shell: false });
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
const pm = isBun ? "bun" : "npm";
|
||||
const cmdArgs = isBun && args[0] === "install" ? ["add", ...args.slice(1)] : args;
|
||||
const res = spawnSync(pm, cmdArgs, { stdio: "inherit", shell: false });
|
||||
if (res.error) throw res.error;
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
throw new Error(`npm exited with code ${res.status}`);
|
||||
throw new Error(`${pm} exited with code ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
buildNodeHeapArgs,
|
||||
} from "../../../scripts/build/runtime-env.mjs";
|
||||
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
|
||||
import { startDetachedTray, validateTrayOptions } from "../tray/detachedTray.mjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const _pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "package.json"), "utf8"));
|
||||
@@ -42,7 +43,7 @@ function parsePort(value, fallback) {
|
||||
}
|
||||
|
||||
export function registerServe(program) {
|
||||
program
|
||||
const command = program
|
||||
.command("serve", { isDefault: true })
|
||||
.description(t("serve.description"))
|
||||
.option("--port <port>", t("serve.port"))
|
||||
@@ -51,7 +52,7 @@ export function registerServe(program) {
|
||||
.option("--log", t("serve.log"))
|
||||
.option("--no-recovery", t("serve.no_recovery"))
|
||||
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
|
||||
.option("--tray", t("serve.tray") || "Show system tray icon (desktop only)")
|
||||
.option("--tray", t("serve.tray") || "Start in the system tray (desktop only)")
|
||||
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
|
||||
.option(
|
||||
"--tls-cert <path>",
|
||||
@@ -66,6 +67,9 @@ export function registerServe(program) {
|
||||
.action(async (opts) => {
|
||||
await runServe(opts);
|
||||
});
|
||||
command.addOption(command.createOption("--tray-worker").hideHelp());
|
||||
command.addOption(command.createOption("--tray-ready-port <port>").hideHelp());
|
||||
command.addOption(command.createOption("--tray-ready-token <token>").hideHelp());
|
||||
}
|
||||
|
||||
/** Once-per-process guard so the Android/Termux cache hint is not spammed. */
|
||||
@@ -95,6 +99,32 @@ export function resetInstrumentationFailureHintForTests() {
|
||||
export async function runServe(opts = {}) {
|
||||
const startedAt = performance.now();
|
||||
|
||||
const trayOptionError = validateTrayOptions(opts);
|
||||
if (trayOptionError) throw new Error(trayOptionError);
|
||||
|
||||
if (opts.tray === true && opts.trayWorker !== true) {
|
||||
const port = parsePort(opts.port ?? process.env.PORT ?? "20128", 20128);
|
||||
const tlsCert = opts.tlsCert ?? process.env.OMNIROUTE_TLS_CERT;
|
||||
const tlsKey = opts.tlsKey ?? process.env.OMNIROUTE_TLS_KEY;
|
||||
urlScheme = resolveTlsOptions({
|
||||
...process.env,
|
||||
...(tlsCert ? { OMNIROUTE_TLS_CERT: tlsCert } : {}),
|
||||
...(tlsKey ? { OMNIROUTE_TLS_KEY: tlsKey } : {}),
|
||||
})
|
||||
? "https"
|
||||
: "http";
|
||||
const result = await startDetachedTray({
|
||||
cliPath: join(ROOT, "bin", "omniroute.mjs"),
|
||||
port,
|
||||
maxRestarts: opts.maxRestarts ?? 2,
|
||||
tlsCert,
|
||||
tlsKey,
|
||||
});
|
||||
console.log(`\x1b[32m✔ OmniRoute tray started in background\x1b[0m`);
|
||||
console.log(` \x1b[1mDashboard:\x1b[0m ${urlScheme}://localhost:${port}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Same prep as bin/omniroute.mjs — keep it here so a direct `runServe()` call
|
||||
// (tests / programmatic) still gets a writable Next.js cache dir before spawn.
|
||||
ensureAndroidCacheDir({ env: process.env });
|
||||
@@ -255,7 +285,8 @@ export async function runServe(opts = {}) {
|
||||
opts.log === true,
|
||||
opts.maxRestarts ?? 2,
|
||||
startedAt,
|
||||
useTray
|
||||
useTray,
|
||||
{ trayReadyPort: opts.trayReadyPort, trayReadyToken: opts.trayReadyToken }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -368,9 +399,11 @@ async function runWithSupervisor(
|
||||
showLog,
|
||||
maxRestarts,
|
||||
startedAt,
|
||||
useTray = false
|
||||
useTray = false,
|
||||
{ trayReadyPort, trayReadyToken } = {}
|
||||
) {
|
||||
if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1";
|
||||
writePidFile("supervisor", process.pid);
|
||||
|
||||
const supervisor = new ServerSupervisor({
|
||||
serverPath: serverJs,
|
||||
@@ -381,7 +414,7 @@ async function runWithSupervisor(
|
||||
if (detectMitmCrash(crashLog)) {
|
||||
try {
|
||||
const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
const { updateSettings } = await import(`${PROJECT_ROOT}/src/lib/db/settings.ts`);
|
||||
const { updateSettings } = await import(pathToFileURL(join(PROJECT_ROOT, "src/lib/db/settings.ts")).href);
|
||||
updateSettings({ mitmEnabled: false });
|
||||
} catch {}
|
||||
return "disable-mitm-and-retry";
|
||||
@@ -394,17 +427,38 @@ async function runWithSupervisor(
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
killTrayIfActive();
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
killTrayIfActive();
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
});
|
||||
|
||||
if (!showLog) {
|
||||
waitForServer(dashboardPort, 60000).then(async (up) => {
|
||||
if (up) {
|
||||
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
|
||||
if (useTray) {
|
||||
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
|
||||
if (!trayReady) {
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (trayReadyPort && trayReadyToken) {
|
||||
const { notifyTrayReady } = await import("../tray/detachedTray.mjs");
|
||||
try {
|
||||
await notifyTrayReady(parsePort(trayReadyPort, 0), trayReadyToken);
|
||||
} catch {
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReady(dashboardPort, apiPort, noOpen, startedAt);
|
||||
} else {
|
||||
reportReadinessTimeout(dashboardPort, supervisor);
|
||||
@@ -451,29 +505,30 @@ function killTrayIfActive() {
|
||||
async function maybeStartTray(port, apiPort, supervisor) {
|
||||
try {
|
||||
const { initTray, isTraySupported } = await import("../tray/index.mjs");
|
||||
if (!isTraySupported()) return;
|
||||
if (!isTraySupported()) return false;
|
||||
const { default: open } = await import("open").catch(() => ({ default: null }));
|
||||
const dashboardUrl = `${urlScheme}://localhost:${port}`;
|
||||
const tray = await initTray({
|
||||
port,
|
||||
onQuit: () => {
|
||||
killTrayIfActive();
|
||||
cleanupPidFile("supervisor");
|
||||
supervisor.stop();
|
||||
},
|
||||
onOpenDashboard: () => open?.(dashboardUrl),
|
||||
onShowLogs: () => {
|
||||
// In-place: open logs stream (best-effort)
|
||||
process.stdout.write(`[omniroute][tray] Logs at: ${dashboardUrl}/logs\n`);
|
||||
},
|
||||
onShowLogs: () => open?.(`${dashboardUrl}/dashboard/logs`),
|
||||
});
|
||||
if (tray) {
|
||||
const { killTray } = await import("../tray/index.mjs");
|
||||
_killTray = killTray;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
// tray is optional — do not fail the server, but surface why it failed so
|
||||
// "--tray shows nothing" is diagnosable instead of silent (#4605).
|
||||
process.stderr.write(`[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { createPrompt, printHeading, printInfo, printSuccess } from "../io.mjs";
|
||||
import { openOmniRouteDb } from "../sqlite.mjs";
|
||||
@@ -16,7 +16,7 @@ import { t } from "../i18n.mjs";
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
async function getListCliTools() {
|
||||
const { listCliTools } = await import(`${PROJECT_ROOT}/src/shared/constants/cliTools.ts`);
|
||||
const { listCliTools } = await import(pathToFileURL(resolve(PROJECT_ROOT, "src/shared/constants/cliTools.ts")).href);
|
||||
return listCliTools;
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
"log": "Show server logs inline",
|
||||
"no_recovery": "Disable auto-restart on crash (debugging mode)",
|
||||
"max_restarts": "Max crash restarts within 30s before giving up (default: 2)",
|
||||
"tray": "Show system tray icon (desktop only, opt-in)",
|
||||
"tray": "Start in the system tray (desktop only, opt-in)",
|
||||
"no_tray": "Disable system tray icon",
|
||||
"tls_cert": "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)"
|
||||
|
||||
@@ -10,6 +10,10 @@ const PROVIDER_TEST_CONFIGS = {
|
||||
format: "openai",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
model: "openai/gpt-4o-mini",
|
||||
// #11226: /models is public on OpenRouter (200 with any or no key) — probe the
|
||||
// authenticated key-info endpoint instead so a bad key fails the test here
|
||||
// instead of on the first real chat request.
|
||||
keyCheckPath: "/auth/key",
|
||||
},
|
||||
groq: {
|
||||
format: "openai",
|
||||
@@ -101,13 +105,19 @@ async function testOpenAILikeProvider(input, config) {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
const modelsRes = await fetchWithTimeout(joinUrl(config.baseUrl, "/models"), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
// Providers whose /models endpoint is public (e.g. OpenRouter) declare a
|
||||
// keyCheckPath pointing at an authenticated endpoint so the probe actually
|
||||
// exercises the key instead of the public catalog.
|
||||
const probeRes = await fetchWithTimeout(
|
||||
joinUrl(config.baseUrl, config.keyCheckPath || "/models"),
|
||||
{
|
||||
method: "GET",
|
||||
headers,
|
||||
}
|
||||
);
|
||||
|
||||
if (modelsRes.ok || modelsRes.status === 401 || modelsRes.status === 403) {
|
||||
return classifyResponse(modelsRes);
|
||||
if (probeRes.ok || probeRes.status === 401 || probeRes.status === 403) {
|
||||
return classifyResponse(probeRes);
|
||||
}
|
||||
|
||||
const chatRes = await fetchWithTimeout(joinUrl(config.baseUrl, "/chat/completions"), {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { apiFetch, isServerUp } from "./api.mjs";
|
||||
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
// Dynamic `import()` resolves its specifier as a URL, not as a filesystem path.
|
||||
// On Windows an absolute path starts with a drive letter, which the ESM loader
|
||||
// reads as the unsupported URL scheme `e:` and rejects. Pass a file:// URL.
|
||||
const projectFileUrl = (relPath) => pathToFileURL(resolve(PROJECT_ROOT, relPath)).href;
|
||||
|
||||
export class ServerOfflineError extends Error {
|
||||
constructor(message = "Server is offline and operation requires HTTP runtime") {
|
||||
super(message);
|
||||
@@ -22,8 +27,8 @@ function makeHttpContext(opts) {
|
||||
|
||||
async function importDbModules() {
|
||||
const [combos, recovery] = await Promise.all([
|
||||
import(`${PROJECT_ROOT}/src/lib/db/combos.ts`),
|
||||
import(`${PROJECT_ROOT}/src/lib/db/recovery.ts`),
|
||||
import(projectFileUrl("src/lib/db/combos.ts")),
|
||||
import(projectFileUrl("src/lib/db/recovery.ts")),
|
||||
]);
|
||||
return { combos, recovery };
|
||||
}
|
||||
|
||||
@@ -114,30 +114,30 @@ export function isBetterSqliteBinaryValid() {
|
||||
|
||||
export function npmInstallRuntime(pkgs, opts = {}) {
|
||||
const cwd = ensureRuntimeDir();
|
||||
// Persist to the runtime package.json (exact version) instead of --no-save so a later
|
||||
// install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the
|
||||
// same runtime dir) does not prune this package as "extraneous" — that pruning otherwise
|
||||
// reproduces "No SQLite driver available" after a tray install removes better-sqlite3.
|
||||
// npm 12+ defaults `allowScripts` to off, silently skipping lifecycle/install
|
||||
// scripts (e.g. better-sqlite3's node-gyp/prebuild-install rebuild) unless the
|
||||
// package has a matching `allowScripts` entry — and still exits 0, masking the
|
||||
// failure (#10713). The runtime dir is a CLI-owned, non-user package.json, so
|
||||
// explicitly allowing scripts for the packages we are installing here is safe.
|
||||
const npmArgs = [
|
||||
"install",
|
||||
...pkgs,
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefer-online",
|
||||
"--save-exact",
|
||||
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
|
||||
];
|
||||
// On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly
|
||||
// so we never set shell:true (which would propagate env and enable injection).
|
||||
const isWin = platform() === "win32";
|
||||
const [exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
|
||||
let exe, args, displayCmd;
|
||||
if (isBun) {
|
||||
const bunArgs = ["add", ...pkgs, "--trust"];
|
||||
[exe, args] = isWin ? ["cmd.exe", ["/c", "bun", ...bunArgs]] : ["bun", bunArgs];
|
||||
displayCmd = `bun ${bunArgs.join(" ")}`;
|
||||
} else {
|
||||
const npmArgs = [
|
||||
"install",
|
||||
...pkgs,
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--prefer-online",
|
||||
"--save-exact",
|
||||
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
|
||||
];
|
||||
[exe, args] = isWin ? ["cmd.exe", ["/c", "npm", ...npmArgs]] : ["npm", npmArgs];
|
||||
displayCmd = `npm ${npmArgs.join(" ")}`;
|
||||
}
|
||||
|
||||
if (!opts.silent) {
|
||||
process.stdout.write(`[omniroute][runtime] npm ${npmArgs.join(" ")}\n`);
|
||||
process.stdout.write(`[omniroute][runtime] ${displayCmd}\n`);
|
||||
}
|
||||
const res = spawnSync(exe, args, {
|
||||
cwd,
|
||||
|
||||
@@ -6,7 +6,9 @@ import { pathToFileURL } from "node:url";
|
||||
import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs";
|
||||
|
||||
const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime");
|
||||
const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.10.1";
|
||||
// Exported so the packaging coherence guard (tests/unit/pack-boot-runtime-paths.test.ts)
|
||||
// can assert this stays on the same major as optionalDependencies.better-sqlite3 (#11242).
|
||||
export const BETTER_SQLITE3_VERSION = "better-sqlite3@^13.0.2";
|
||||
|
||||
let resolvedCached = null;
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./
|
||||
|
||||
async function loadSqlite() {
|
||||
if (process.versions.bun) {
|
||||
return { Database: (await import("bun:sqlite")).Database };
|
||||
try {
|
||||
return { Database: (await import("bun:sqlite")).Database, driver: "bun:sqlite" };
|
||||
} catch (bunError) {
|
||||
// fall through to better-sqlite3 if bun:sqlite fails
|
||||
}
|
||||
}
|
||||
try {
|
||||
return { Database: (await import("better-sqlite3")).default };
|
||||
return { Database: (await import("better-sqlite3")).default, driver: "better-sqlite3" };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
@@ -86,12 +90,14 @@ export function normalizeBunSqliteParams(params) {
|
||||
|
||||
export function createSqliteNativeError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const isBun = Boolean(process.versions.bun);
|
||||
const rebuildCmd = isBun ? "bun add better-sqlite3 --trust" : "npm rebuild better-sqlite3";
|
||||
if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) {
|
||||
return new Error(
|
||||
"better-sqlite3 native binding is incompatible with this Node.js runtime. " +
|
||||
"Run `npm rebuild better-sqlite3` in the OmniRoute project and try again. " +
|
||||
"Or run: omniroute runtime repair " +
|
||||
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
|
||||
`better-sqlite3 native binding is incompatible with this runtime. ` +
|
||||
`Run \`${rebuildCmd}\` in the OmniRoute project and try again. ` +
|
||||
`Or run: omniroute runtime repair ` +
|
||||
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
|
||||
);
|
||||
}
|
||||
if (
|
||||
@@ -100,10 +106,9 @@ export function createSqliteNativeError(error) {
|
||||
message.includes("Cannot find module 'better-sqlite3'")
|
||||
) {
|
||||
return new Error(
|
||||
"better-sqlite3 native binding could not be found (no prebuilt addon for this platform). " +
|
||||
"This is common under `npx`, which runs a fresh, ephemeral install that never built the addon. " +
|
||||
"Run: omniroute runtime repair " +
|
||||
"(rebuilds into a user-writable runtime; works without a C++ toolchain)."
|
||||
`better-sqlite3 native binding could not be found (no prebuilt addon for this platform). ` +
|
||||
`Run: omniroute runtime repair ` +
|
||||
`(rebuilds into a user-writable runtime; works without a C++ toolchain).`
|
||||
);
|
||||
}
|
||||
return error;
|
||||
@@ -111,7 +116,7 @@ export function createSqliteNativeError(error) {
|
||||
|
||||
async function openSqliteDatabase(dbPath, options = {}) {
|
||||
const loaded = await loadSqlite();
|
||||
if (process.versions.bun) {
|
||||
if (loaded.driver === "bun:sqlite" || (process.versions.bun && !loaded.Database)) {
|
||||
if (options.fileMustExist && !fs.existsSync(dbPath)) {
|
||||
throw new Error(`SQLite file does not exist: ${dbPath}`);
|
||||
}
|
||||
|
||||
@@ -276,6 +276,10 @@ function isAgentSelfMac() {
|
||||
}
|
||||
}
|
||||
|
||||
function isDetachedTrayWorker() {
|
||||
return process.argv.includes("--tray-worker");
|
||||
}
|
||||
|
||||
function enableMac() {
|
||||
const plistDir = join(homedir(), "Library", "LaunchAgents");
|
||||
mkdirSync(plistDir, { recursive: true });
|
||||
@@ -300,7 +304,7 @@ function enableMac() {
|
||||
// If we're already the running agent, launchctl load/unload would SIGTERM us.
|
||||
// The plist is updated on disk and launchd already has us loaded under our own
|
||||
// PID — nothing more to do for the current session.
|
||||
if (isAgentSelfMac()) return existsSync(plistPath);
|
||||
if (isAgentSelfMac() || isDetachedTrayWorker()) return existsSync(plistPath);
|
||||
try {
|
||||
execSync("launchctl load -w " + JSON.stringify(plistPath), { stdio: "ignore" });
|
||||
} catch {}
|
||||
@@ -313,7 +317,7 @@ function disableMac() {
|
||||
// `launchctl unload` sends SIGTERM and a user clicking "Disable Autostart"
|
||||
// from the tray would lose the tray icon instead of just flipping the label.
|
||||
// Removing the plist file is enough to stop the agent at the next login.
|
||||
if (!isAgentSelfMac()) {
|
||||
if (!isAgentSelfMac() && !isDetachedTrayWorker()) {
|
||||
try {
|
||||
execSync("launchctl unload -w " + JSON.stringify(plistPath), { stdio: "ignore" });
|
||||
} catch {}
|
||||
|
||||
176
bin/cli/tray/detachedTray.mjs
Normal file
176
bin/cli/tray/detachedTray.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { randomBytes, timingSafeEqual } from "node:crypto";
|
||||
import { createServer, connect } from "node:net";
|
||||
|
||||
/** Builds arguments for the hidden process that owns the server and tray. */
|
||||
export function buildTrayWorkerArgs({ port, maxRestarts, readyPort, readyToken, tlsCert, tlsKey }) {
|
||||
const args = [
|
||||
"serve",
|
||||
"--tray",
|
||||
"--tray-worker",
|
||||
"--no-open",
|
||||
"--port",
|
||||
String(port),
|
||||
"--max-restarts",
|
||||
String(maxRestarts),
|
||||
"--tray-ready-port",
|
||||
String(readyPort),
|
||||
"--tray-ready-token",
|
||||
readyToken,
|
||||
];
|
||||
if (tlsCert) args.push("--tls-cert", tlsCert);
|
||||
if (tlsKey) args.push("--tls-key", tlsKey);
|
||||
return args;
|
||||
}
|
||||
|
||||
/** Builds the platform command that starts the hidden tray worker. */
|
||||
export function buildTrayLaunch({ platform, execPath, cliPath, workerArgs, label }) {
|
||||
if (platform === "darwin") {
|
||||
return {
|
||||
command: "launchctl",
|
||||
args: ["submit", "-l", label, "--", execPath, cliPath, ...workerArgs],
|
||||
options: { stdio: "ignore" },
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: execPath,
|
||||
args: [cliPath, ...workerArgs],
|
||||
options: { detached: true, stdio: "ignore", windowsHide: true },
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns an error for command modes that conflict with detached tray mode. */
|
||||
export function validateTrayOptions(opts) {
|
||||
if (opts.trayWorker && (!opts.trayReadyPort || !opts.trayReadyToken)) {
|
||||
return "tray worker requires readiness credentials";
|
||||
}
|
||||
if (!opts.tray || opts.trayWorker) return null;
|
||||
if (opts.daemon) return "--tray cannot use --daemon";
|
||||
if (opts.log) return "--tray cannot use --log";
|
||||
if (opts.noRecovery || opts.recovery === false) return "--tray cannot use --no-recovery";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Creates a token-protected loopback server for tray worker readiness. */
|
||||
export async function createTrayReadinessServer(token) {
|
||||
let markReady;
|
||||
const ready = new Promise((resolve) => {
|
||||
markReady = resolve;
|
||||
});
|
||||
const expected = Buffer.from(token);
|
||||
const server = createServer((socket) => {
|
||||
let data = "";
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
if (data.length > 256) socket.destroy();
|
||||
});
|
||||
socket.on("end", () => {
|
||||
const received = Buffer.from(data);
|
||||
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
|
||||
socket.end("ERROR");
|
||||
return;
|
||||
}
|
||||
socket.end("READY");
|
||||
markReady();
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
port: address.port,
|
||||
wait(timeoutMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error("Tray worker did not become ready")),
|
||||
timeoutMs
|
||||
);
|
||||
ready.then(() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
close() {
|
||||
server.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Notifies the parent process that the server and tray are ready. */
|
||||
export async function notifyTrayReady(port, token) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const socket = connect({ host: "127.0.0.1", port }, () => socket.end(token));
|
||||
let reply = "";
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("data", (chunk) => {
|
||||
reply += chunk;
|
||||
});
|
||||
socket.on("end", () => {
|
||||
if (reply === "READY") resolve();
|
||||
else reject(new Error("Tray readiness token was rejected"));
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** Starts a detached tray worker and waits until its server and tray are ready. */
|
||||
export async function startDetachedTray(
|
||||
{ cliPath, port, maxRestarts, tlsCert, tlsKey, timeoutMs = 60000 },
|
||||
{ platform = process.platform, spawnProcess = spawn } = {}
|
||||
) {
|
||||
const token = randomBytes(32).toString("hex");
|
||||
const readiness = await createTrayReadinessServer(token);
|
||||
const label = `com.omniroute.tray.${process.pid}.${Date.now()}`;
|
||||
const workerArgs = buildTrayWorkerArgs({
|
||||
port,
|
||||
maxRestarts,
|
||||
readyPort: readiness.port,
|
||||
readyToken: token,
|
||||
tlsCert,
|
||||
tlsKey,
|
||||
});
|
||||
const launch = buildTrayLaunch({
|
||||
platform,
|
||||
execPath: process.execPath,
|
||||
cliPath,
|
||||
workerArgs,
|
||||
label,
|
||||
});
|
||||
const child = spawnProcess(launch.command, launch.args, launch.options);
|
||||
const spawnFailure = new Promise((_, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) => {
|
||||
if (platform !== "darwin" || code !== 0) {
|
||||
reject(new Error(`Tray worker exited before readiness with code ${code ?? "unknown"}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
if (platform !== "darwin") child.unref?.();
|
||||
try {
|
||||
await Promise.race([readiness.wait(timeoutMs), spawnFailure]);
|
||||
return { platform, pid: child.pid, label: platform === "darwin" ? label : null };
|
||||
} catch (err) {
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
execFileSync("launchctl", ["bootout", `gui/${process.getuid()}/${label}`], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
} catch {}
|
||||
} else if (platform === "win32" && child.pid) {
|
||||
try {
|
||||
execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
||||
} catch {}
|
||||
} else if (child.pid) {
|
||||
try {
|
||||
process.kill(child.pid, "SIGTERM");
|
||||
} catch {}
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
readiness.close();
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,7 @@ export async function initSystrayUnix(
|
||||
}
|
||||
});
|
||||
|
||||
tray.ready().catch((err) => {
|
||||
process.stderr.write(`[omniroute][tray] systray2 failed: ${err?.message ?? String(err)}\n`);
|
||||
});
|
||||
await tray.ready();
|
||||
|
||||
return tray;
|
||||
}
|
||||
|
||||
@@ -94,10 +94,15 @@ export function ensureAndroidCacheDir(options = {}) {
|
||||
*/
|
||||
export function isFatalInstrumentationHookFailure(text) {
|
||||
if (!text) return false;
|
||||
return (
|
||||
/Unsupported platform:\s*android/i.test(text) ||
|
||||
/error occurred while loading instrumentation hook/i.test(text)
|
||||
);
|
||||
// Next.js wraps ANY throw inside instrumentation.register() with the generic
|
||||
// "An error occurred while loading instrumentation hook:" prefix, on every
|
||||
// platform (node_modules/next/dist/server/web/globals.js). That prefix alone
|
||||
// therefore cannot identify the Android/Termux cache-probe failure — a bare
|
||||
// generic instrumentation error on win32/desktop would be misreported as the
|
||||
// Android bug and hide the real cause. Only match when the text actually
|
||||
// carries the Android platform marker that Next's getCacheDirectory() emits.
|
||||
// #10028
|
||||
return /Unsupported platform:\s*android/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,18 @@ export function getSecureFloorForMajor(major) {
|
||||
}
|
||||
|
||||
export function getNodeRuntimeSupport(version = process.versions.node) {
|
||||
if (process.versions.bun) {
|
||||
return {
|
||||
nodeVersion: `bun-${process.versions.bun} (Node.js API ${version})`,
|
||||
nodeCompatible: true,
|
||||
reason: "supported-bun",
|
||||
supportedRange: SUPPORTED_NODE_RANGE + " || Bun >=1.1.0",
|
||||
supportedDisplay: SUPPORTED_NODE_DISPLAY + ", or Bun 1.1+",
|
||||
recommendedVersion: `v${RECOMMENDED_NODE_VERSION}`,
|
||||
minimumSecureVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseNodeVersion(version);
|
||||
const secureFloor = getSecureFloorForMajor(parsed.major);
|
||||
const nodeCompatible = secureFloor ? compareNodeVersions(parsed, secureFloor) >= 0 : false;
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import updateNotifier from "update-notifier";
|
||||
let updateNotifier = null;
|
||||
try {
|
||||
updateNotifier = (await import("update-notifier")).default;
|
||||
} catch {
|
||||
// update-notifier is optional in pruned standalone environments
|
||||
}
|
||||
import { isNativeBinaryCompatible } from "../scripts/build/native-binary-compat.mjs";
|
||||
import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs";
|
||||
import { getDefaultDataDir } from "./cli/data-dir.mjs";
|
||||
@@ -251,8 +256,9 @@ if (shouldProvisionStorageKey(process.argv)) {
|
||||
|
||||
// Register update notifier — checks npm once per 24h, notifies on exit via stderr.
|
||||
const _pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
const _notifier = updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 });
|
||||
const _notifier = updateNotifier ? updateNotifier({ pkg: _pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }) : null;
|
||||
process.on("exit", () => {
|
||||
if (!_notifier || !_notifier.update) return;
|
||||
if (process.env.OMNIROUTE_NO_UPDATE_NOTIFIER) return;
|
||||
if (process.env.CI) return;
|
||||
if (process.argv.includes("--quiet") || process.argv.includes("-q")) return;
|
||||
|
||||
1
changelog.d/features/10987-logfare-free-provider.md
Normal file
1
changelog.d/features/10987-logfare-free-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add Logfare as a free OpenAI-compatible provider — dashboard card with a Free badge and request-logging disclosure (every prompt/completion is logged for research; opt out at logfare.ai/consent), live model discovery from `https://logfare.ai/v1/models` (20 models, 11 chat-capable: kimi-k3, deepseek-v4-pro, glm-5.2, gpt-5.6-luna, minimax-m3…), full chat/streaming through the existing OpenAI-compatible path, the real Logfare logo on the card, and a listing in the free-tiers guide. ([#10987](https://github.com/diegosouzapw/OmniRoute/pull/10987))
|
||||
1
changelog.d/features/11104-operator-error-rules.md
Normal file
1
changelog.d/features/11104-operator-error-rules.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** let operators declare per-provider error rules through `settings.providerErrorRules` instead of patching the catalog — an operator-supplied rule for a provider is consulted before the built-in `providerRuleRegistry`, receives the raw error text, and has its declared scope/cooldown/reason actually honored end to end, for any provider (declaring the rule is the opt-in — no extra allowlist entry needed). Matches are plain case-insensitive substrings (never RegExp) and bounded to 50 rules to keep the hot path safe ([#11104](https://github.com/diegosouzapw/OmniRoute/pull/11104))
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(combo):** the shared per-request combo attempt budget is now operator-configurable via `maxGlobalAttempts` (combo config / `comboDefaults` cascade), instead of the hardcoded 30. Lower it to fail fast on a dead target pool, raise it for large combos; clamped to `[1, 200]` so an unbounded budget can never cause runaway background requests ([#11134](https://github.com/diegosouzapw/OmniRoute/issues/11134))
|
||||
1
changelog.d/features/11190-usage-command-json.md
Normal file
1
changelog.d/features/11190-usage-command-json.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(api):** `/api/usage/om-usage` gains a structured form — `?format=json` returns the key's own usage as `ApiKeyUsageLimitStatus` + `UsageSnapshot` instead of `text/plain`. This is the surface a UI (the OmniCopilot panel) consumes to show a key holder their daily/weekly spend and quota reset. The route is self-service (the caller's own key, gated by `allowUsageCommand`), not the management surface; refusals come back as a discriminated `{ "allowed": false, "error": … }` so a UI can tell "not allowed" apart from "allowed but nothing cached yet". The endpoint was previously undocumented in `API_REFERENCE.md`; it now has a section ([#11190](https://github.com/diegosouzapw/OmniRoute/pull/11190))
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(api):** `/api/usage/om-usage?format=json` now returns `providers[]` — every connection's quota snapshot, not just the single selected one — so a panel can render Codex / Claude / OpenCode side by side. The collector already gathered all of them; the single-pick `provider` field (kept) is a terminal presentation choice. Closes the per-connection gap from OmniCopilot #8 ([#11192](https://github.com/diegosouzapw/OmniRoute/pull/11192))
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(providers):** allow overriding the rate-limit queue wait timeout (`maxWaitMs`) per connection, alongside the existing `rpm`/`tpm`/`tpd`/`minTime`/`maxConcurrent` overrides — a single slow provider no longer has to lower the global wait budget for every other provider (#11251)
|
||||
1
changelog.d/features/effort-tiers-loop-learned-sets.md
Normal file
1
changelog.d/features/effort-tiers-loop-learned-sets.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(catalog):** surface runtime-learned `reasoning_effort` tiers in `/v1/models` `capabilities.effort_tiers` (learned set replaces synced metadata when present), map them to OpenCode `ModelV2.variants` in the OmniRoute plugin, and align dispatch `-<tier>` suffix validation to the effective (learned ?? synced) set — so the UI offers exactly the tiers the upstream accepts (e.g. `{low, high, max}` for `oc/x-preview-f-free`) and each advertised variant completes. Excludes codex/glm/kimi, which keep their own dedicated `-{effort}` suffix mechanism and never gain `effort_tiers` from this path (related to #7694, builds on #11232)
|
||||
1
changelog.d/features/unreleased-detached-cli-tray.md
Normal file
1
changelog.d/features/unreleased-detached-cli-tray.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(cli):** run `omniroute serve --tray` as a detached desktop process after server and tray readiness, with graphical login auto-start support.
|
||||
1
changelog.d/fixes/10028-windows-instrumentation-hook.md
Normal file
1
changelog.d/fixes/10028-windows-instrumentation-hook.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): stop diagnosing every Next.js instrumentation-hook failure as the Android/Termux cache bug — only the Android "Unsupported platform: android" signal now triggers the Android hint, so a win32/desktop instrumentation error surfaces its real cause instead of a useless `mkdir -p ~/.cache` (#10028)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** the five g4f.space sub-providers (Groq, Gemini, Pollinations, Ollama, NVIDIA) no longer advertise a free tier — a keyless `POST /v1/chat/completions` now returns `402 insufficient_credits` behind a proof-of-work "cake" wall (re-verified live 2026-08-22), so `hasFree` is `false` and the notes point at `g4f.dev/members.html`. The gateway still works with a member key, so its registry wiring and `authType: "optional"` are unchanged ([#10071](https://github.com/diegosouzapw/OmniRoute/issues/10071)) — thanks @chirag127
|
||||
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
1
changelog.d/fixes/10265-command-code-provider-api.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): route chat to the documented /provider/v1/chat/completions endpoint instead of the CLI-only /alpha/generate, which Command Code gates/blocks for external callers (#10265)
|
||||
1
changelog.d/fixes/10523-servicesupervisor-port-flake.md
Normal file
1
changelog.d/fixes/10523-servicesupervisor-port-flake.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(services): isolate probeBeforeSpawn adoption tests on distinct ports to stop the order-dependent flake (#10523)
|
||||
@@ -1 +1 @@
|
||||
- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Combos now drop incompatible continuation reasoning by default and can explicitly skip incompatible targets, while known providers no longer show redundant encrypted-reasoning controls. (#10550)
|
||||
- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Direct requests drop incompatible continuation reasoning by default; combos can explicitly skip incompatible targets without mutating the request. Known providers no longer show redundant encrypted-reasoning controls. (#10550, #10959)
|
||||
|
||||
1
changelog.d/fixes/10736-corrupt-rotate-fence.md
Normal file
1
changelog.d/fixes/10736-corrupt-rotate-fence.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** pause call-log rotation and record SQLITE_CORRUPT on `/api/db/health` instead of retrying writes against a malformed pager ([#10736](https://github.com/diegosouzapw/OmniRoute/issues/10736))
|
||||
1
changelog.d/fixes/10850-readyz-alias.md
Normal file
1
changelog.d/fixes/10850-readyz-alias.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(api):** alias `GET`/`HEAD` `/readyz` to `/healthz` so Kubernetes readiness probes do not 404 ([#10850](https://github.com/diegosouzapw/OmniRoute/issues/10850))
|
||||
1
changelog.d/fixes/10940-opencode-limit-output.md
Normal file
1
changelog.d/fixes/10940-opencode-limit-output.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): always emit limit.output in generated OpenCode config so schema validation passes for metadata-less models (#10940)
|
||||
1
changelog.d/fixes/10949-mixed-reasoning-plaintext.md
Normal file
1
changelog.d/fixes/10949-mixed-reasoning-plaintext.md
Normal file
@@ -0,0 +1 @@
|
||||
- Preserve explicit plaintext reasoning when a Responses reasoning item also carries opaque provider state (rare OpenCode Go `deepseek-v4-flash` responses). Mixed plaintext + opaque input is projected onto the target transport: plaintext targets keep portable text, opaque targets keep provider state. Opaque-only reasoning is dropped when the selected target cannot replay it, allowing cross-model conversations to continue. (#10949, #10959)
|
||||
1
changelog.d/fixes/10954-combo-create-models.md
Normal file
1
changelog.d/fixes/10954-combo-create-models.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): combo create accepts --models and no longer creates empty combos (#10954)
|
||||
1
changelog.d/fixes/10955-cli-ref-params.md
Normal file
1
changelog.d/fixes/10955-cli-ref-params.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): resolve $ref path params and add PATCH combos requestBody in generated API commands (#10955)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(sse): default single-target incompatible reasoning to drop for agentic replay — single-target requests to opaque reasoning targets now gracefully strip incompatible plaintext reasoning history instead of returning HTTP 400, matching combo default behavior while preserving operator and per-request overrides ([#10959](https://github.com/diegosouzapw/OmniRoute/issues/10959))
|
||||
2
changelog.d/fixes/10967-10966-combo-diag-recovery.md
Normal file
2
changelog.d/fixes/10967-10966-combo-diag-recovery.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- fix(sse): combo diagnostics no longer truncate `exhausted_connection` entries to a hardcoded `provider: "unknown"` with the provider prefix eaten by an 8-char slice — the real provider id is preserved and only the connection id is truncated (#10967)
|
||||
- fix(sse): combo terminal failures caused entirely by quota/account-balance exhaustion (including a durable HTTP 403 `insufficient_quota` / `AUTHZ_INSUFFICIENT_BALANCE`) now stamp a stable `quota_exhausted` diagnostics reason with a `switch-combo` recovery hint instead of the misleading default `retry` action (#10966)
|
||||
1
changelog.d/fixes/10976-skip-default-searxng.md
Normal file
1
changelog.d/fixes/10976-skip-default-searxng.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(search):** skip catalog-default SearXNG `http://localhost:8888/search` so Docker/K8s search does not ECONNREFUSED then 502 into the next provider ([#10976](https://github.com/diegosouzapw/OmniRoute/issues/10976))
|
||||
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
1
changelog.d/fixes/10986-reasoning-only-content.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)
|
||||
1
changelog.d/fixes/10988-release-v3850-quality-gates.md
Normal file
1
changelog.d/fixes/10988-release-v3850-quality-gates.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(ci):** clear inherited `release/v3.8.50` quality-gate reds on the X Search PR: drop the stale `copilot-m365-web.ts:330` public-creds allowlist, document six missing env vars, register four covering Stryker tap tests, prune leftover ESLint suppressions, replace the phantom `@/lib/db/connections` Utilization import with `getProviderConnectionById`, and fix open-sse/dashboard typecheck regressions in freebuff, browser-backed chat, auth, health matrix, and Monaco ([#10988](https://github.com/diegosouzapw/OmniRoute/pull/10988)).
|
||||
1
changelog.d/fixes/10988-release-v3850-unit-shards.md
Normal file
1
changelog.d/fixes/10988-release-v3850-unit-shards.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(ci):** clear remaining `release/v3.8.50` unit-shard reds on the X Search PR: pin `onnxruntime-node` to the transformers 1.24.3 copy, rebaseline OpenAPI coverage, sync goldens/i18n, honor eye-hidden no-auth models across provider aliases, await rejected-request call-log writes, absorb catalog event-loop shard contention in #9147, and align inherited tests with advisory context estimates, #10501 combo terminal-status aggregation, and current catalog/auth behavior ([#10988](https://github.com/diegosouzapw/OmniRoute/pull/10988)).
|
||||
1
changelog.d/fixes/10990-v0-vercel-web-static-catalog.md
Normal file
1
changelog.d/fixes/10990-v0-vercel-web-static-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Static model catalog for v0-vercel-web:** seed a static catalog for the v0-vercel-web web-cookie provider (v0-1.0-md, v0-1.5-lg, v0-1.5-md) so its dashboard "Available Models" / "Import from /models" UI serves a usable list instead of falling through to the route's 400 "does not support models listing" ([#10990](https://github.com/diegosouzapw/OmniRoute/issues/10990)).
|
||||
1
changelog.d/fixes/10997-blackbox-deprecation.md
Normal file
1
changelog.d/fixes/10997-blackbox-deprecation.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): mark the blackbox provider deprecated — api.blackbox.ai returns HTTP 404 on every path variant (sweep 2026-08-21), so the public inference surface is dead and the catalog entry now carries a deprecation notice. ([#10997](https://github.com/diegosouzapw/OmniRoute/issues/10997))
|
||||
1
changelog.d/fixes/11002-dify-key-validation.md
Normal file
1
changelog.d/fixes/11002-dify-key-validation.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): validate Dify keys against its native /v1/chat-messages endpoint (#11002)
|
||||
1
changelog.d/fixes/11008-account-rotation-eviction.md
Normal file
1
changelog.d/fixes/11008-account-rotation-eviction.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(accounts):** `markCooldown` now carries the failure origin (`transient` vs `terminal`) — transient 429/network only cools down, repeated terminal failures evict and are skipped by `pickAccount` until a success or operator clear ([#11008](https://github.com/diegosouzapw/OmniRoute/pull/11008)) — thanks @maxmad64bis
|
||||
1
changelog.d/fixes/11009-terminal-status-origin.md
Normal file
1
changelog.d/fixes/11009-terminal-status-origin.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** route terminal `testStatus` writes (`banned`, `deactivated`, `credits_exhausted`) through a single origin-aware passage — probe failures are recorded but never deactivate the connection ([#11009](https://github.com/diegosouzapw/OmniRoute/pull/11009)) — thanks @maxmad64bis
|
||||
1
changelog.d/fixes/11014-codex-drop-default-on.md
Normal file
1
changelog.d/fixes/11014-codex-drop-default-on.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(codex):** drop non-standard `codex.*` SSE events by default so OpenAI SDK / Codex CLI `/v1/responses` clients are not 502'd by `event: codex.rate_limits` ([#11014](https://github.com/diegosouzapw/OmniRoute/issues/11014)) — thanks @RaviTharuma
|
||||
1
changelog.d/fixes/11015-shutdown-track-sse.md
Normal file
1
changelog.d/fixes/11015-shutdown-track-sse.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(resilience):** count heavyweight `/v1` admission leases in the SIGTERM drain and send `Retry-After` on shutdown 503s so Recreate no longer looks like an empty 502 ([#11015](https://github.com/diegosouzapw/OmniRoute/issues/11015)) — thanks @RaviTharuma
|
||||
1
changelog.d/fixes/11016-cred-health-disable-log.md
Normal file
1
changelog.d/fixes/11016-cred-health-disable-log.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(startup):** log `Credential health scheduler disabled` when `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` is set instead of lying with `started` ([#11016](https://github.com/diegosouzapw/OmniRoute/issues/11016)) — thanks @RaviTharuma
|
||||
1
changelog.d/fixes/11017-rate-limit-docs.md
Normal file
1
changelog.d/fixes/11017-rate-limit-docs.md
Normal file
@@ -0,0 +1 @@
|
||||
- **docs(api-keys):** document that unset `DEFAULT_RATE_LIMIT_PER_DAY` is unlimited (#2289), not a hidden 1000/day cap ([#11017](https://github.com/diegosouzapw/OmniRoute/issues/11017)) — thanks @RaviTharuma
|
||||
1
changelog.d/fixes/11050-remove-ghost-webhook-events.md
Normal file
1
changelog.d/fixes/11050-remove-ghost-webhook-events.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(webhooks):** remove 3 declared-but-never-emitted events (`provider.error`, `provider.recovered`, `combo.switched`) from `WebhookEvent` — catalog now `request.completed | request.failed | quota.exceeded | test.ping`; `POST /api/webhooks` and `PUT /api/webhooks/[id]` reject ghost values with 400; OpenAPI webhook description updated across 43 locales ([11050](https://github.com/diegosouzapw/OmniRoute/pull/11050))
|
||||
1
changelog.d/fixes/11060-perplexity-filter.md
Normal file
1
changelog.d/fixes/11060-perplexity-filter.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): filter Perplexity model import to the Sonar family so Agent-API catalog ids stop surfacing as routable chat models (#11060)
|
||||
1
changelog.d/fixes/11085-claude-code-tool-name-casing.md
Normal file
1
changelog.d/fixes/11085-claude-code-tool-name-casing.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(claude):** restore canonical tool names (`bash` → `Bash`, `croncreate` → `CronCreate`) on non-streaming OpenAI→Claude conversion and through identity-echo alias maps, so Claude Code stops rejecting tool calls with "No such tool available" ([#11085](https://github.com/diegosouzapw/OmniRoute/pull/11085)) — thanks @linhdmn
|
||||
1
changelog.d/fixes/11089-chat-routing-synced-inventory.md
Normal file
1
changelog.d/fixes/11089-chat-routing-synced-inventory.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(resilience):** filter chat connection selection by each connection's *synced* model inventory on multi-host self-hosted providers (`ollama-local`, `lm-studio`, `vllm`, …), so a request for a model only one host advertises is pinned to that host instead of failing over onto a host that never had it ([#11089](https://github.com/diegosouzapw/OmniRoute/issues/11089))
|
||||
1
changelog.d/fixes/11095-termux-onnx.md
Normal file
1
changelog.d/fixes/11095-termux-onnx.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(install): make the ONNX dependency chain optional so Termux/Android installs succeed again (#11095)
|
||||
1
changelog.d/fixes/11101-reject-silent-validation.md
Normal file
1
changelog.d/fixes/11101-reject-silent-validation.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** Reject silent validation degradation on provider connection patch — unknown `rateLimitOverrides` keys (e.g. a typo'd `tpm`) and empty/non-numeric values now return `400` with the rejected key list instead of being silently dropped ([#11101](https://github.com/diegosouzapw/OmniRoute/pull/11101))
|
||||
1
changelog.d/fixes/11102-combo-suggestion-count.md
Normal file
1
changelog.d/fixes/11102-combo-suggestion-count.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Autopilot suggestion counter:** the combo health autopilot summary now reports `suggestionCount` (the real number of suggested actions across all issues) instead of conflating it with link counts, while keeping `actionableCount` as a deprecated alias for backward compatibility. The `run_combo_test` action now links to the dashboard with the combo id (`/dashboard/combos?test=<comboId>`) rather than the read-only API route, so operators can actually trigger a test from the UI ([#11102](https://github.com/diegosouzapw/OmniRoute/pull/11102)).
|
||||
1
changelog.d/fixes/11103-persist-config-audit-log.md
Normal file
1
changelog.d/fixes/11103-persist-config-audit-log.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Config audit persistence:** persist the configuration audit trail to SQLite (`config_audit_log`) instead of an in-memory buffer capped at 1000 volatile entries, and bound its growth with `cleanupConfigAudit()` driven by the `retention.configAudit` setting (default 30 days), wired into `runAutoCleanup` ([#11103](https://github.com/diegosouzapw/OmniRoute/pull/11103)).
|
||||
1
changelog.d/fixes/11109-stream-recovery-toolcall.md
Normal file
1
changelog.d/fixes/11109-stream-recovery-toolcall.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): resume mid-stream recovery after a _completed_ tool call — `finish_reason: "tool_calls"` is now tracked per-call instead of as a general terminal marker, so truncation of trailing prose after a fully-delivered tool call is recoverable while in-flight calls stay blocked ([#11109](https://github.com/diegosouzapw/OmniRoute/pull/11109))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** `reasoning_effort` now learns the accepted values from a provider's own 400/422 response and clamps to the highest one instead of forwarding an unsupported `xhigh`/`max` (or a hardcoded `"high"` fallback) — fixes custom OpenAI-compatible connections and registered providers with no reasoning metadata ([#11116](https://github.com/diegosouzapw/OmniRoute/pull/11116)) — thanks @maxmad64bis
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** parallel `function_call` items in a Responses API stream (e.g. several tool calls dispatched in the same turn) now each get a stable, distinct `index`/`id` when translated to Chat Completions streaming deltas, instead of colliding on index 0 and tripping strict stream parsers with `Expected 'id' to be a string.` ([#11144](https://github.com/diegosouzapw/OmniRoute/pull/11144))
|
||||
1
changelog.d/fixes/11149-opencode-go-flat-rate.md
Normal file
1
changelog.d/fixes/11149-opencode-go-flat-rate.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(analytics):** `opencode-go` is now classified as a flat-rate subscription, so cost analytics shows $0 for it instead of billing every call at the underlying model’s metered rate — it resells GLM, Kimi, Grok, DeepSeek, MiniMax, Qwen and GPT-5.x under one flat monthly fee, which made the overstatement large rather than marginal ([#11149](https://github.com/diegosouzapw/OmniRoute/pull/11149)) — thanks @electrumguy
|
||||
@@ -0,0 +1 @@
|
||||
- fix(dashboard): keep `open-sse/config/providerRegistry.ts` free of `node:net` so the provider detail client bundle builds again — the host classification moved to a platform-free `src/shared/network/privateHost.ts` with a pure-JS `isIP` equivalent, leaving the #11122 routing behaviour unchanged (#11154)
|
||||
1
changelog.d/fixes/11162-combo-create-requires-model.md
Normal file
1
changelog.d/fixes/11162-combo-create-requires-model.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Combo create:** creating a routing combo without any model is now refused (`400`) — the CLI requires `--models`/`--model` on `combo create`, matching the dashboard which already rejected empty combos.
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(resilience):** a missing-model `404` on a provider that declares `passthroughModels: true` in the shared registry (novita, uncloseai, orcarouter and 37 others) now locks out only that model instead of cooling the entire connection — `hasPerModelQuota()` previously read only the open-sse registry and the local/self-hosted families ([#11165](https://github.com/diegosouzapw/OmniRoute/pull/11165)) — thanks @yourspraveen
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(routing):** a custom `openai-compatible-*` / `anthropic-compatible-*` connection pointing at a keyless self-hosted backend (llama.cpp, Ollama, vLLM started without an API key) now stays in the `auto/*` candidate pool instead of being silently dropped by the credential gate — for those IDs "no credential" is the normal configuration, not an unconfigured connection ([#11180](https://github.com/diegosouzapw/OmniRoute/pull/11180)) — thanks @marcs7
|
||||
1
changelog.d/fixes/11181-lkgp-enabled-context.md
Normal file
1
changelog.d/fixes/11181-lkgp-enabled-context.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(routing):** the Routing tab's "last known good provider" toggle now actually takes effect — `lkgpEnabled` was persisted and the `lkgp` strategy guarded on it, but the setting was never forwarded into the `RoutingContext` built in `resolveAutoStrategyOrder()`, so `context.lkgpEnabled` was always `undefined` and the off-switch was unreachable ([#11181](https://github.com/diegosouzapw/OmniRoute/issues/11181))
|
||||
1
changelog.d/fixes/7764-quota-window-order.md
Normal file
1
changelog.d/fixes/7764-quota-window-order.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(usage):** keep session/weekly/monthly quota windows in chronological order on every provider card. The order is now derived from the quota keys themselves instead of a provider whitelist, so Claude, MiniMax, Z.ai and Command Code stop rendering the two bars in opposite positions across sibling accounts ([#7764](https://github.com/diegosouzapw/OmniRoute/issues/7764))
|
||||
1
changelog.d/fixes/8864-uncloseai-noauth.md
Normal file
1
changelog.d/fixes/8864-uncloseai-noauth.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): treat UncloseAI as a no-auth provider so the connect form no longer forces a fake API key (#8864)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(ssrf): make `getProviderOutboundGuard()` (used for search-provider connection validation, image generation and remote image fetch) honor the local-first default `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` the same way the chat validation guard already does, so a LAN-hosted SearXNG/Brave search provider works with only the LOCAL flag set instead of silently requiring `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS` ([#9123](https://github.com/diegosouzapw/OmniRoute/issues/9123)).
|
||||
1
changelog.d/fixes/9763-ratelimit-mintime-floor.md
Normal file
1
changelog.d/fixes/9763-ratelimit-mintime-floor.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(ratelimit):** respect operator `minTimeBetweenRequestsMs` floor when relaxing the limiter on headroom — the adaptive rate-limit learning no longer silently erases a configured minimum gap between requests when the upstream reports plenty of remaining capacity ([#9763](https://github.com/diegosouzapw/OmniRoute/issues/9763)).
|
||||
1
changelog.d/fixes/codex-appserver-hardening.md
Normal file
1
changelog.d/fixes/codex-appserver-hardening.md
Normal file
@@ -0,0 +1 @@
|
||||
- Hardened the Codex app-server transport after the post-merge security review of #11205: approval prompts from the app-server (its own command/file/permission execution — not the harness tool passthrough) are now auto-denied by default, with opt-in auto-approval via `providerSpecificData.codexAppServerAutoApprove` / `OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE`; the default codex sandbox changed from `danger-full-access` to `workspace-write` (override per connection or env); env-sourced capability tokens are now only sent to env-sourced URLs or operator-local hosts (loopback/RFC1918/link-local/ULA/localhost/single-label LAN names/*.local/*.ts.net/*.internal), so a connection's providerSpecificData URL can no longer exfiltrate the operator's env token; and the `/readyz` health probe no longer follows redirects while carrying the bearer token.
|
||||
1
changelog.d/fixes/codex-max-context-window.md
Normal file
1
changelog.d/fixes/codex-max-context-window.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(codex): prefer `max_context_window` over the `context_window` pricing tier as the usable input limit in discovery, and raise the static Codex OAuth catalog to the same usable window so the conservative discovery merge no longer caps live values at the 272K pricing tier
|
||||
1
changelog.d/fixes/command-code-effort-capabilities.md
Normal file
1
changelog.d/fixes/command-code-effort-capabilities.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(combo): resolve effort-suffixed command-code variants (e.g. `deepseek-v4-flash-max`) to their base model for capability lookups, so tool-bearing combo requests keep the declared priority order instead of reordering behind models with confirmed capabilities
|
||||
1
changelog.d/fixes/opencode-merge-provider-guard.md
Normal file
1
changelog.d/fixes/opencode-merge-provider-guard.md
Normal file
@@ -0,0 +1 @@
|
||||
- **OpenCode config merge:** stop `mergeOpenCodeConfig` splaying a malformed `provider` block into index keys. The root was already guarded against a non-object; the `provider` branch it spreads one level down was not, so an existing `"provider": ["a", "b"]` merged to `{"0": "a", "1": "b", …}`. Its sibling `mergeOpenCodeConfigText` already refuses the same input.
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(executors):** OpencodeExecutor rotates (or retries once on a single-account direct path) on upstream 400 empty-body rejections — malformed completion envelopes with no error field were propagated as success and killed client sessions. Bounded +1 attempt per request; body reads are conditioned on status 400 so successful/streaming responses are never buffered. 400s carrying an error field keep propagating immediately.
|
||||
1
changelog.d/fixes/release-v3850-basereds-tests-i18n.md
Normal file
1
changelog.d/fixes/release-v3850-basereds-tests-i18n.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(i18n): complete Vietnamese translations for recently added UI strings (#9985)
|
||||
3
changelog.d/fixes/release-v3850-basereds.md
Normal file
3
changelog.d/fixes/release-v3850-basereds.md
Normal file
@@ -0,0 +1,3 @@
|
||||
- fix(api): repair broken `@/lib/db/connections` import in the usage utilization route that failed the production build (#10939 follow-up)
|
||||
- chore(docs): regenerate PROVIDER_REFERENCE and refresh README diagram SVGs to the real provider count (347)
|
||||
- chore(lint): prune ESLint suppressions orphaned on the release branch
|
||||
@@ -0,0 +1 @@
|
||||
- fix(quality): register GrokBuildToolCard.tsx react-hooks/set-state-in-effect suppression (dropped in #10778's uncommitted fix)
|
||||
@@ -0,0 +1 @@
|
||||
- **test(db):** replace three empty `test.skip` placeholders in the critical DB-state suite with real assertions — `resetDbInstance` must swap the singleton while the on-disk row survives, the on-disk DB must open in WAL journal mode, and `db_meta` must hold the seeded `schema_version` — so a regression in any of those invariants can no longer pass as silently green ([#10906](https://github.com/diegosouzapw/OmniRoute/pull/10906))
|
||||
@@ -0,0 +1 @@
|
||||
- **docs(docker):** document runtime RAM for coding-agent `/v1/responses` (image default 1 GiB heap is dashboard-only; 8–12 GiB heap for agents) ([#10982](https://github.com/diegosouzapw/OmniRoute/issues/10982))
|
||||
1
changelog.d/maintenance/11024-n-instance-scale-out.md
Normal file
1
changelog.d/maintenance/11024-n-instance-scale-out.md
Normal file
@@ -0,0 +1 @@
|
||||
- **docs(docker):** document N independent `DATA_DIR`s as the supported large `/v1/responses` scale-out (one V8 heap ≠ host RAM; do not `replicas>1` on one SQLite file) ([#11024](https://github.com/diegosouzapw/OmniRoute/issues/11024)) — thanks @RaviTharuma
|
||||
1
changelog.d/maintenance/11038-filesize-baseline-fix.md
Normal file
1
changelog.d/maintenance/11038-filesize-baseline-fix.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(quality): rebaseline file-size for modelCapabilities.ts (1016->1072) drift from merged tip fixes (#11034 et al)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(quality): register `tests/unit/authz/oauth-autoimport-local-only.test.ts` in stryker `tap.testFiles` (residual of #11053)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user