mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
Merge remote-tracking branch 'origin/release/v3.8.51' into HEAD
# Conflicts: # README.md # config/quality/file-size-baseline.json # docs/diagrams/comparison-table.svg # docs/diagrams/free-tier-budget.svg # docs/diagrams/readme-hero.svg # docs/i18n/am/llm.txt # docs/i18n/ar/llm.txt # docs/i18n/az/llm.txt # docs/i18n/bg/llm.txt # docs/i18n/bn/llm.txt # docs/i18n/cs/llm.txt # docs/i18n/da/llm.txt # docs/i18n/de/llm.txt # docs/i18n/el/llm.txt # docs/i18n/es/llm.txt # docs/i18n/et/llm.txt # docs/i18n/fa/llm.txt # docs/i18n/fi/llm.txt # docs/i18n/fr/llm.txt # docs/i18n/ga/llm.txt # docs/i18n/gu/llm.txt # docs/i18n/ha/llm.txt # docs/i18n/he/llm.txt # docs/i18n/hi/llm.txt # docs/i18n/hr/llm.txt # docs/i18n/hu/llm.txt # docs/i18n/hy/llm.txt # docs/i18n/id/llm.txt # docs/i18n/ig/llm.txt # docs/i18n/it/llm.txt # docs/i18n/ja/llm.txt # docs/i18n/ka/llm.txt # docs/i18n/km/llm.txt # docs/i18n/kn/llm.txt # docs/i18n/ko/llm.txt # docs/i18n/lt/llm.txt # docs/i18n/lv/llm.txt # docs/i18n/ml/llm.txt # docs/i18n/mr/llm.txt # docs/i18n/ms/llm.txt # docs/i18n/mt/llm.txt # docs/i18n/my/llm.txt # docs/i18n/ne/llm.txt # docs/i18n/nl/llm.txt # docs/i18n/no/llm.txt # docs/i18n/or/llm.txt # docs/i18n/pa/llm.txt # docs/i18n/phi/llm.txt # docs/i18n/pl/llm.txt # docs/i18n/pt-BR/llm.txt # docs/i18n/pt/llm.txt # docs/i18n/ro/llm.txt # docs/i18n/ru/llm.txt # docs/i18n/si/llm.txt # docs/i18n/sk/llm.txt # docs/i18n/sl/llm.txt # docs/i18n/sr/llm.txt # docs/i18n/sv/llm.txt # docs/i18n/sw/llm.txt # docs/i18n/ta/llm.txt # docs/i18n/te/llm.txt # docs/i18n/th/llm.txt # docs/i18n/tr/llm.txt # docs/i18n/uk-UA/llm.txt # docs/i18n/ur/llm.txt # docs/i18n/uz/llm.txt # docs/i18n/vi/llm.txt # docs/i18n/yo/llm.txt # docs/i18n/zh-CN/llm.txt # docs/i18n/zh-TW/llm.txt # docs/reference/PROVIDER_REFERENCE.md # docs/screenshots/free-tier-budget-card.svg
This commit is contained in:
77
.env.example
77
.env.example
@@ -112,8 +112,9 @@ DISABLE_SQLITE_AUTO_BACKUP=false
|
||||
# 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:
|
||||
# quota store + warmup circuit breaker). 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
|
||||
@@ -737,6 +738,12 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
ENABLE_SOCKS5_PROXY=true
|
||||
NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
|
||||
# Opt-in feature flag (default off; a dashboard DB override wins over this value): proxy pools
|
||||
# and per-account rotation stop re-serving a member that just failed (TCP probe refused, or a
|
||||
# 429 received through it) for a period that doubles on each repeat, up to a cap. No proxy
|
||||
# status is written. "true" (or 1, yes) enables it; unset keeps plain selection.
|
||||
# PROXY_SKIP_RECENTLY_FAILED=false
|
||||
|
||||
# Standard proxy variables (lowercase variants also supported).
|
||||
# HTTP_PROXY=http://127.0.0.1:7890
|
||||
# HTTPS_PROXY=http://127.0.0.1:7890
|
||||
@@ -1034,6 +1041,12 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts.
|
||||
#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000
|
||||
|
||||
# Opt-in minimum output budget (tokens) for reasoning models (#10281 follow-up).
|
||||
# When set, a caller max_tokens in [256, floor) on a thinking-capable model is
|
||||
# raised to the floor so reasoning tokens cannot consume the whole budget
|
||||
# (zero-content finish_reason=length turns). Unset = never enlarge client budgets (#9507).
|
||||
#OMNIROUTE_REASONING_MIN_BUDGET=4096
|
||||
|
||||
# Spend write batcher cadence (ms) and buffer size before forced flush.
|
||||
# Used by: src/lib/spend/batchWriter.ts. Defaults: 60000 ms / 1000 entries.
|
||||
#OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000
|
||||
@@ -1149,12 +1162,34 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
#OMNIROUTE_WAL_GUARD_MAX_MB=256
|
||||
|
||||
# Minimum rows a cleanup must delete before the post-cleanup VACUUM runs. Default: 1000.
|
||||
# 0 always vacuums when rows were freed. Used by: src/lib/db/cleanup.ts.
|
||||
# 0 always vacuums when rows were freed. The post-cleanup VACUUM also runs when the
|
||||
# reclaimable-space threshold below is met, whichever comes first (either signal fires it).
|
||||
# Used by: src/lib/db/cleanup.ts::shouldVacuumAfterCleanup().
|
||||
#OMNIROUTE_VACUUM_MIN_DELETED_ROWS=1000
|
||||
|
||||
# Minimum reclaimable space (MB) that alone justifies a full-database VACUUM after a
|
||||
# cleanup, even when the row-count threshold above was not met (a handful of oversized
|
||||
# blob rows can free far more space than thousands of tiny rows). VACUUM is synchronous
|
||||
# and blocks the entire process. Default: 100. 0 always vacuums after any deletion.
|
||||
# Used by: src/lib/db/cleanup.ts::getVacuumMinReclaimableBytes().
|
||||
#OMNIROUTE_VACUUM_MIN_RECLAIMABLE_MB=100
|
||||
|
||||
# Explicit path to sql-wasm.wasm for the sql.js fallback adapter. Default: auto-detect.
|
||||
# Used by: src/lib/db/adapters/sqljsAdapter.ts.
|
||||
#OMNIROUTE_SQLJS_WASM_PATH=
|
||||
# Days a terminal (completed/failed/cancelled/expired) Batch API job's checkpoints,
|
||||
# referenced files, and row are kept by the automatic cleanup sweep. Default: 30
|
||||
# (matches OpenAI's own Batch API output retention window). Only takes effect once
|
||||
# BATCH_AND_FILE_AUTO_CLEANUP_ENABLED is turned on.
|
||||
# Used by: src/lib/db/cleanup.ts::getBatchRetentionDays().
|
||||
#OMNIROUTE_BATCH_RETENTION_DAYS=30
|
||||
|
||||
# Let the automatic cleanup sweep delete terminal Batch API jobs (and their
|
||||
# checkpoints) past OMNIROUTE_BATCH_RETENTION_DAYS, and clear the content of
|
||||
# uploaded files past their own expires_at. Off by default: every existing
|
||||
# install keeps this data exactly as before until an operator opts in.
|
||||
# Used by: src/lib/db/cleanup.ts (feature flag; see docs/reference/FEATURE_FLAGS.md).
|
||||
#BATCH_AND_FILE_AUTO_CLEANUP_ENABLED=false
|
||||
|
||||
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
|
||||
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
|
||||
@@ -1488,7 +1523,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
#
|
||||
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
|
||||
# If set, it becomes the default for FETCH_TIMEOUT_MS, STREAM_IDLE_TIMEOUT_MS,
|
||||
# and STREAM_READINESS_TIMEOUT_MS.
|
||||
# and STREAM_READINESS_TIMEOUT_MS. STREAM_ACTIVE_TIMEOUT_MS is independent.
|
||||
# The fine-grained variables below override their respective defaults only when set.
|
||||
|
||||
# ── Global shortcut ──
|
||||
@@ -1670,6 +1705,8 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# ── Stream idle detection ──
|
||||
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
|
||||
# # Extended-thinking models rarely pause >90s.
|
||||
# STREAM_ACTIVE_TIMEOUT_MS=1260000 # Max total active SSE lifetime (default: 21 min = the largest registered model timeoutMs + 1 min; 0 disables)
|
||||
# # Independent of REQUEST_TIMEOUT_MS and byte activity.
|
||||
# STREAM_READINESS_TIMEOUT_MS=80000 # Time to receive the first non-ping SSE event
|
||||
# STREAM_READINESS_MAX_TIMEOUT_MS=180000 # Cap for adaptive first-event extensions
|
||||
# # (large/tool-heavy/high-reasoning requests).
|
||||
@@ -1686,6 +1723,8 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# ── TLS client (wreq-js fingerprint proxy) ──
|
||||
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
|
||||
# TLS_FIRST_BYTE_WATCHDOG_MS=10000 # #12656: bounds time-to-first-byte on the wreq body (0 disables)
|
||||
# OPENCODE_RESPONSES_STALL_ROTATION=false # #13484 feature flag (Settings → Feature Flags wins): rotate once when a streamed Responses reply stalls before its first byte
|
||||
# RESPONSES_FIRST_BYTE_TIMEOUT_MS=15000 # #13484: OpenCode Responses first-byte window, only used when the OPENCODE_RESPONSES_STALL_ROTATION flag is on (0 disables)
|
||||
|
||||
# ── API Bridge (/v1 proxy server) ──
|
||||
# API_BRIDGE_PROXY_TIMEOUT_MS=600000 # Proxy hop timeout (default: 10min)
|
||||
@@ -2231,6 +2270,11 @@ APP_LOG_TO_FILE=true
|
||||
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
|
||||
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
|
||||
# PROXY_HEALTH_AUTO_DEACTIVATE=false
|
||||
# Opt-in feature flag (default off; a dashboard DB override wins over this value): show,
|
||||
# under a proxy pool in the dashboard, how many observed egress IPs served its members over
|
||||
# the last 24 h and how many connections used them (read-only, computed from the proxy log,
|
||||
# never used for routing). "true" (or 1, yes) enables it.
|
||||
# PROXY_POOL_EGRESS_OBSERVATION=false
|
||||
|
||||
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
|
||||
# directly when proxy reachability pre-checks fail. Default: false.
|
||||
@@ -2344,6 +2388,10 @@ APP_LOG_TO_FILE=true
|
||||
# Cursor stream idle timeout (ms). Default: 300000 (5 min).
|
||||
# Used by: open-sse/executors/cursor.ts.
|
||||
# CURSOR_STREAM_TIMEOUT_MS=300000
|
||||
# Grace window (ms) after a composer kv_after_text soft terminator when bytes remain
|
||||
# buffered — gives a trailing exec_mcp tool call time to complete its frame. 2s covers
|
||||
# every exec_mcp-behind-kv ordering observed live.
|
||||
# CURSOR_KV_GRACE_MS=2000
|
||||
|
||||
# Cursor tool-commit directive toggle. Default-on: when a request declares
|
||||
# tools, a directive is prepended so composer-2.5 reliably issues tool calls
|
||||
@@ -2351,6 +2399,13 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/executors/cursor.ts.
|
||||
# CURSOR_TOOL_DIRECTIVE=1
|
||||
|
||||
# Operator-defined system prompt text appended to the system message AFTER
|
||||
# translation (post-translation injection), so it reaches codex/Responses and
|
||||
# /v1/messages paths. Also used as the directive prefix stripped from echoed
|
||||
# system preamble blocks. Leave unset to disable.
|
||||
# Used by: open-sse/translator/request/claude-to-openai.ts, open-sse/translator/response/openai-to-claude.ts.
|
||||
# OMNIROUTE_SYSTEM_INSTRUCTION_APPEND=
|
||||
|
||||
# Per-image fetch timeout (ms) for remote image_url vision input. Default: 15000.
|
||||
# Used by: open-sse/utils/cursorImages.ts.
|
||||
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
|
||||
@@ -2735,6 +2790,13 @@ APP_LOG_TO_FILE=true
|
||||
# tokens (accessToken / refreshToken / providerSpecificData). Default OFF —
|
||||
# only non-credential metadata is synced. See docs/security/SOCKET_DEV_FINDINGS.md §5.
|
||||
# OMNIROUTE_CLOUD_SYNC_SECRETS=false
|
||||
#
|
||||
# Set to "true" to reject an UNSIGNED Cloud sync response when no local secret
|
||||
# is configured (#13679). Default OFF keeps v3.8.x back-compat for peers that
|
||||
# have not rotated in a shared secret yet; v3.9 flips the default to enforced.
|
||||
# A signature that IS present is always verified, and always rejected when
|
||||
# OMNIROUTE_CLOUD_SYNC_SECRET is unset, regardless of this flag.
|
||||
# OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=false
|
||||
|
||||
# ─── Zed import legacy compat (v3.8.6) ──────────────────────────────────────
|
||||
# Set to "true" to fall back to the v3.8.5 one-step "import everything from
|
||||
@@ -3030,6 +3092,13 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
|
||||
# CHROME_PATH=/usr/bin/chromium
|
||||
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
# CDP_PROXY_TOKEN required by docker/chatgpt-web-codex-browser/cdp-proxy.mjs (#13679):
|
||||
# when set, every request to the CDP proxy sidecar must present it as an
|
||||
# `X-Omni-Cdp-Token` header. Left unset, the proxy keeps forwarding requests
|
||||
# unauthenticated (network isolation via docker-compose.yml's dedicated
|
||||
# `chatgpt-web-codex-net` is the default mitigation). Generate with:
|
||||
# `openssl rand -hex 32`
|
||||
# CDP_PROXY_TOKEN=
|
||||
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
|
||||
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex v2
|
||||
|
||||
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -453,8 +453,11 @@ jobs:
|
||||
# One FS inventory of src/app/api for both anti-hallucination directions.
|
||||
- name: API docs refs (openapi + prose → routes)
|
||||
run: npm run check:api-docs-refs
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
# Blocking since the 2026-09 docs re-sync: a core doc edited without `npm run i18n:run
|
||||
# --files=<doc>` (or `--adopt` for a mechanical edit) leaves 65 stale mirrors behind;
|
||||
# the run only retranslates the `## ` sections whose text changed, so it is cheap.
|
||||
- name: i18n docs drift (sources changed since their translation)
|
||||
run: node scripts/i18n/check-translation-drift.mjs
|
||||
|
||||
docs-lint:
|
||||
name: Docs Lint (prose — advisory)
|
||||
@@ -511,9 +514,11 @@ jobs:
|
||||
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
||||
# Real-translation ratchet: a leaf copied verbatim from en.json passes key
|
||||
# parity above but is still English to the user (es shipped 55% English).
|
||||
# Advisory in PR-0; flipped to blocking once the backlog is retranslated (PR-4).
|
||||
- name: i18n real-translation ratio (advisory)
|
||||
run: node scripts/i18n/check-translation-ratio.mjs --warn
|
||||
# Blocking since PR-4 retranslated the verbatim-English backlog: the share of
|
||||
# untranslated leaves per locale may only fall (ratchet baseline in
|
||||
# config/quality/i18n-translation-baseline.json; `npm run i18n:check-ratio:update`).
|
||||
- name: i18n real-translation ratio
|
||||
run: node scripts/i18n/check-translation-ratio.mjs
|
||||
# #8463: a rewritten English value used to leave its 39 translations behind
|
||||
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
|
||||
# Key parity above cannot see it — a stale translation counts as covered.
|
||||
@@ -531,6 +536,20 @@ jobs:
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: node scripts/i18n/check-new-key-coverage.mjs
|
||||
# Absolute complement of the two gates above: every locale must carry exactly the key
|
||||
# set of en.json, whatever the age of the key. A locale batch is generated from the
|
||||
# en.json of the day the branch is cut and translates for days while the base keeps
|
||||
# adding keys — the batch PR adds no key itself, so the new-key gate stays silent and
|
||||
# 43 absent keys out of ~13,000 still read 99.7 % coverage. Incident 2026-09-15:
|
||||
# batch 1 (#13044) landed 43 keys short in nine locales, batch 2 (#13660) 10 keys short
|
||||
# in eight. Fix is `sync-ui-keys --locale=<codes> --translate-markers`.
|
||||
- name: i18n key completeness (every locale carries every en.json key)
|
||||
run: node scripts/i18n/check-key-completeness.mjs
|
||||
# Same gate for the CLI catalogs (bin/cli/locales). check:cli-i18n only compares
|
||||
# pt-BR / zh-CN / zh-TW; 38 locales shipped with 1–24 of 830 keys for months
|
||||
# (audit 2026-09-16) and the CLI silently fell back to English for them.
|
||||
- name: i18n key completeness (CLI catalogs)
|
||||
run: node scripts/i18n/check-key-completeness.mjs --catalog=cli
|
||||
|
||||
# #8038: cheap glossary/protected-terms consistency gate —
|
||||
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
|
||||
|
||||
69
.github/workflows/docker-publish.yml
vendored
69
.github/workflows/docker-publish.yml
vendored
@@ -411,16 +411,12 @@ jobs:
|
||||
path: /tmp/digests/bun-web
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create Docker Hub manifest
|
||||
- name: Create Docker Hub version manifests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
fi
|
||||
local refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
@@ -433,7 +429,7 @@ jobs:
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
docker buildx imagetools create -t "${image}:${VERSION}${suffix}" "${refs[@]}"
|
||||
}
|
||||
|
||||
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
|
||||
@@ -441,16 +437,12 @@ jobs:
|
||||
create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
|
||||
create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
|
||||
|
||||
- name: Create GHCR manifest
|
||||
- name: Create GHCR version manifests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
fi
|
||||
local refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
@@ -463,7 +455,7 @@ jobs:
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
docker buildx imagetools create -t "${image}:${VERSION}${suffix}" "${refs[@]}"
|
||||
}
|
||||
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
|
||||
@@ -471,6 +463,59 @@ jobs:
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
|
||||
|
||||
- name: Smoke-test published Docker image
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
container="omniroute-smoke-${VERSION//[^a-zA-Z0-9_.-]/-}"
|
||||
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
|
||||
docker run --detach --name "$container" "${IMAGE_NAME}:${VERSION}"
|
||||
for attempt in $(seq 1 30); do
|
||||
status="$(docker inspect --format '{{.State.Health.Status}}' "$container")"
|
||||
if [ "$status" = "healthy" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$status" = "unhealthy" ] || [ "$(docker inspect --format '{{.State.Status}}' "$container")" = "exited" ]; then
|
||||
docker logs "$container"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
docker logs "$container"
|
||||
exit 1
|
||||
|
||||
- name: Promote Docker Hub latest tags
|
||||
if: needs.prepare.outputs.promote_latest == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
promote_tag() {
|
||||
local suffix="$1"
|
||||
docker buildx imagetools create -t "${IMAGE_NAME}:latest${suffix}" "${IMAGE_NAME}:${VERSION}${suffix}"
|
||||
}
|
||||
promote_tag ""
|
||||
promote_tag "-web"
|
||||
for suffix in -bun -web-bun; do
|
||||
if docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}${suffix}" >/dev/null 2>&1; then
|
||||
promote_tag "$suffix"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Promote GHCR latest tags
|
||||
if: needs.prepare.outputs.promote_latest == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
promote_tag() {
|
||||
local suffix="$1"
|
||||
docker buildx imagetools create -t "${GHCR_IMAGE_NAME}:latest${suffix}" "${GHCR_IMAGE_NAME}:${VERSION}${suffix}"
|
||||
}
|
||||
promote_tag ""
|
||||
promote_tag "-web"
|
||||
for suffix in -bun -web-bun; do
|
||||
if docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${VERSION}${suffix}" >/dev/null 2>&1; then
|
||||
promote_tag "$suffix"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Inspect image
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
run: |
|
||||
|
||||
@@ -97,4 +97,11 @@
|
||||
# credential; the generic-api-key rule flags the long hyphenated string.
|
||||
'''omniroute-cheaperinference-sponsor-banner-dismissed-v\d+''',
|
||||
'''SunbreakWebUI1''',
|
||||
# Uzbek dashboard catalog (#13727, src/i18n/messages/uz.json `outputTokenDesc`):
|
||||
# "Yakunlash/javob tokenlari" = "completion/response tokens". The rule reads the
|
||||
# `...TokenDesc` key as a token assignment and the translated words as its value.
|
||||
'''Yakunlash/javob''',
|
||||
# Feature-flag id from #13439 (src/shared/constants/featureFlagDefinitions.ts):
|
||||
# `key: "PROTECTED_PRIORITY_INFRA_502_ENABLED"` is a flag name, not a credential.
|
||||
'''PROTECTED_PRIORITY_INFRA_502_ENABLED''',
|
||||
]
|
||||
|
||||
33356
.i18n-state.json
33356
.i18n-state.json
File diff suppressed because it is too large
Load Diff
@@ -56,8 +56,14 @@ explicitly:
|
||||
}
|
||||
```
|
||||
|
||||
The token can also come from the `OMNIROUTE_MANAGEMENT_API_KEY` environment
|
||||
variable (the option wins when both are set). Resolution order:
|
||||
`managementReadToken` option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then the
|
||||
`apiKey` fallback.
|
||||
|
||||
Left unset, `managementReadToken` falls back to `apiKey` for backwards
|
||||
compatibility. When a gateway rejects that fallback, the catalog still
|
||||
compatibility, and the plugin warns once at startup that the fallback is
|
||||
active. When a gateway rejects that fallback, the catalog still
|
||||
publishes — but with raw model ids instead of display names, no canonical
|
||||
alias dedupe, no pricing and no combos. The plugin warns once per endpoint
|
||||
when this happens, naming the endpoint and the consequence, so the degraded
|
||||
@@ -65,25 +71,25 @@ catalog is never a mystery.
|
||||
|
||||
## Options
|
||||
|
||||
| Key | Default | Notes |
|
||||
| -------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
|
||||
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
|
||||
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
|
||||
| `managementReadToken` | falls back to `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
|
||||
| `displayName` | `"OmniRoute"` | Provider display name |
|
||||
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
|
||||
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
|
||||
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
|
||||
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
|
||||
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
|
||||
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
|
||||
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
|
||||
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
|
||||
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
|
||||
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
|
||||
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
|
||||
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
|
||||
| Key | Default | Notes |
|
||||
| -------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
|
||||
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
|
||||
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
|
||||
| `managementReadToken` | option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
|
||||
| `displayName` | `"OmniRoute"` | Provider display name |
|
||||
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
|
||||
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
|
||||
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
|
||||
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
|
||||
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
|
||||
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
|
||||
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
|
||||
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
|
||||
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
|
||||
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
|
||||
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
|
||||
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
|
||||
|
||||
## Tool calling on Gemini models
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir } from "node:os";
|
||||
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import type {
|
||||
OmniRouteEnrichmentEntry,
|
||||
@@ -81,6 +81,12 @@ interface DiskSnapshotV2 {
|
||||
*/
|
||||
const MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
// Suffix for the temp file each write publishes via rename. Monotone per
|
||||
// process: two writes for one provider (for example across a credential
|
||||
// rotation) must not share a temp name. Built after the empty-models and
|
||||
// size-cap guards, so only real attempts consume a value.
|
||||
let snapshotWriteCounter = 0;
|
||||
|
||||
function trimTrailingSlashes(value: string): string {
|
||||
let i = value.length;
|
||||
while (i > 0 && value.charCodeAt(i - 1) === 0x2f) i -= 1;
|
||||
@@ -133,7 +139,7 @@ export async function readDiskSnapshot(
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed.v !== "number" ||
|
||||
parsed.v < SNAPSHOT_FORMAT_VERSION ||
|
||||
parsed.v !== SNAPSHOT_FORMAT_VERSION ||
|
||||
typeof parsed.identityFingerprint !== "string" ||
|
||||
parsed.identityFingerprint !== identityFingerprint
|
||||
) {
|
||||
@@ -179,8 +185,14 @@ export async function readDiskSnapshot(
|
||||
export async function writeDiskSnapshot(
|
||||
providerId: string,
|
||||
snapshot: CatalogSnapshot,
|
||||
identityFingerprint: string
|
||||
identityFingerprint: string,
|
||||
logger?: { warn: (message: string) => void }
|
||||
): Promise<void> {
|
||||
// Monotone per-process suffix: two writes for one provider (for example
|
||||
// across a credential rotation) must not share a temp name. Declared here
|
||||
// so the catch below can clean it up; assigned after the guards so only
|
||||
// real attempts consume a counter value.
|
||||
let tmp = "";
|
||||
try {
|
||||
if (snapshot.models.length === 0) return;
|
||||
const file = diskSnapshotPath(providerId);
|
||||
@@ -196,14 +208,33 @@ export async function writeDiskSnapshot(
|
||||
writtenAt: Date.now(),
|
||||
};
|
||||
let payload = JSON.stringify(envelope);
|
||||
if (payload.length > MAX_SNAPSHOT_BYTES && envelope.enrichment !== undefined) {
|
||||
if (
|
||||
Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES &&
|
||||
envelope.enrichment !== undefined
|
||||
) {
|
||||
delete envelope.enrichment;
|
||||
payload = JSON.stringify(envelope);
|
||||
}
|
||||
if (payload.length > MAX_SNAPSHOT_BYTES) return;
|
||||
await writeFile(file, payload, { encoding: "utf8", mode: 0o600 });
|
||||
} catch {
|
||||
if (Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES) {
|
||||
logger?.warn(
|
||||
`[omniroute-v2] snapshot for ${providerId} exceeds the size cap, skipping disk write`
|
||||
);
|
||||
return;
|
||||
}
|
||||
tmp = `${file}.${process.pid}.${snapshotWriteCounter++}`;
|
||||
await writeFile(tmp, payload, { encoding: "utf8", mode: 0o600 });
|
||||
await rename(tmp, file);
|
||||
} catch (err) {
|
||||
// Best-effort: callers already hold the in-memory entry.
|
||||
logger?.warn(
|
||||
`[omniroute-v2] snapshot write failed for ${providerId}: ` +
|
||||
`${err instanceof Error ? err.message : String(err)}, keeping the in-memory entry`
|
||||
);
|
||||
try {
|
||||
await unlink(tmp);
|
||||
} catch {
|
||||
// Ignore: the temp file may not exist (mkdir failed first).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,14 @@ import { assertContext } from "./compat.js";
|
||||
import { type ApiKeyOrigin, resolveApiKey, warnIfMissing } from "./credentials.js";
|
||||
import { createSourceErrorReporter } from "./enrichment-report.js";
|
||||
import { sanitizeToolSchemasFor } from "./gemini-language.js";
|
||||
import { PLUGIN_ID, parsePluginOptions, resolveTimeouts, type PluginOptions } from "./options.js";
|
||||
import {
|
||||
MANAGEMENT_TOKEN_ENV_VAR,
|
||||
PLUGIN_ID,
|
||||
parsePluginOptions,
|
||||
resolveManagementReadToken,
|
||||
resolveTimeouts,
|
||||
type PluginOptions,
|
||||
} from "./options.js";
|
||||
|
||||
/**
|
||||
* A fetch result that says whether it succeeded. Returning a bare `[]` on
|
||||
@@ -61,7 +68,7 @@ function toResolvedOptions(parsed: PluginOptions): ResolvedOptions {
|
||||
providerId: parsed.providerId,
|
||||
baseURL: parsed.baseURL,
|
||||
apiKey: parsed.apiKey ?? process.env.OMNIROUTE_API_KEY ?? "",
|
||||
managementReadToken: parsed.managementReadToken,
|
||||
managementReadToken: resolveManagementReadToken(parsed.managementReadToken),
|
||||
timeoutMs: parsed.timeoutMs,
|
||||
timeouts: parsed.timeouts,
|
||||
logLevel: parsed.logLevel,
|
||||
@@ -93,6 +100,16 @@ export default define({
|
||||
resolved.logLevel = parsed.logLevel;
|
||||
resolved.startupDebug = parsed.startupDebug;
|
||||
log.info(`[omniroute-v2] init providerId=${X}`);
|
||||
// The inference key stands in below when no management token is set, and
|
||||
// gateways usually reject that stand-in with 401/403. Say so once here,
|
||||
// before any fetch, instead of letting the refusal surface per endpoint.
|
||||
if (resolved.managementReadToken === undefined) {
|
||||
log.warn(
|
||||
`[omniroute-v2] no management token configured: management endpoints (/api/*) will reuse the inference key, ` +
|
||||
`which gateways usually reject with 401/403. Set "managementReadToken" in the plugin options ` +
|
||||
`or export ${MANAGEMENT_TOKEN_ENV_VAR}.`
|
||||
);
|
||||
}
|
||||
|
||||
// v1 parity port: in-memory TTL + disk snapshot. The memory key
|
||||
// `baseURL::sha256(creds)` isolates credential tuples (prod vs
|
||||
@@ -297,7 +314,7 @@ export default define({
|
||||
};
|
||||
if (models.length > 0) {
|
||||
state.entries.set(cacheKey, snapshot);
|
||||
await writeDiskSnapshot(X, snapshot, identityFingerprint);
|
||||
await writeDiskSnapshot(X, snapshot, identityFingerprint, log);
|
||||
}
|
||||
void optional.then(
|
||||
(parts) => upgradeWithOptional(snapshot, parts),
|
||||
@@ -344,7 +361,7 @@ export default define({
|
||||
if (unchanged) return;
|
||||
state.entries.set(cacheKey, upgraded);
|
||||
if (upgraded.models.length > 0) {
|
||||
await writeDiskSnapshot(X, upgraded, identityFingerprint);
|
||||
await writeDiskSnapshot(X, upgraded, identityFingerprint, log);
|
||||
}
|
||||
// Reload only when the optional tier actually moved: the catalog
|
||||
// fingerprint covers ids alone, so without this the host would rebuild
|
||||
|
||||
@@ -61,6 +61,21 @@ const pluginOptionsSchema = z
|
||||
|
||||
export type PluginOptions = z.infer<typeof pluginOptionsSchema>;
|
||||
|
||||
/** Environment source for the management token (option wins over this). */
|
||||
export const MANAGEMENT_TOKEN_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
|
||||
|
||||
/**
|
||||
* Resolve the management token: a non-empty option wins, then a non-empty
|
||||
* environment value, else absent. Empty counts as absent on both inputs, the
|
||||
* same rule the inference key follows; no trimming, the token is opaque.
|
||||
*/
|
||||
export function resolveManagementReadToken(optionValue: string | undefined): string | undefined {
|
||||
if (optionValue !== undefined && optionValue.length > 0) return optionValue;
|
||||
const fromEnv = process.env[MANAGEMENT_TOKEN_ENV_VAR];
|
||||
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Per-endpoint timeout defaults (v1 parity). `timeoutMs` is the global fallback. */
|
||||
export const DEFAULT_TIMEOUT_MS = 10_000 as const;
|
||||
/** Auto-combos keep the v1 5s budget; the field is resolved now for the P3 port. */
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
diskSnapshotPath,
|
||||
readDiskSnapshot,
|
||||
writeDiskSnapshot,
|
||||
type CatalogSnapshot,
|
||||
} from "../src/cache.js";
|
||||
|
||||
function isolateDisk(): { dir: string; restore: () => void } {
|
||||
const dir = mkdtempSync(join(tmpdir(), "omniroute-disk-atomic-"));
|
||||
const prev = process.env.OPENCODE_DATA_DIR;
|
||||
process.env.OPENCODE_DATA_DIR = dir;
|
||||
return {
|
||||
dir,
|
||||
restore: () => {
|
||||
if (prev === undefined) delete process.env.OPENCODE_DATA_DIR;
|
||||
else process.env.OPENCODE_DATA_DIR = prev;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeSnapshot(models: string[] = ["m-a"]): CatalogSnapshot {
|
||||
return {
|
||||
models: models.map((id) => ({ id })),
|
||||
combos: [],
|
||||
autoCombos: [],
|
||||
providers: [],
|
||||
fetchedAt: Date.now(),
|
||||
} as unknown as CatalogSnapshot;
|
||||
}
|
||||
|
||||
function makeLogger() {
|
||||
const messages: string[] = [];
|
||||
return {
|
||||
messages,
|
||||
logger: { warn: (message: string) => void messages.push(message) },
|
||||
};
|
||||
}
|
||||
|
||||
// Entries next to the destination other than the destination itself: any
|
||||
// leftover temp file after a successful write shows up here.
|
||||
function strayEntries(file: string): string[] {
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = readdirSync(dirname(file));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return entries.filter((entry) => entry !== file.split("/").pop());
|
||||
}
|
||||
|
||||
// The writer names its temp file `${file}.${pid}.${counter}` with a
|
||||
// module-monotone counter starting at 0, built after the empty-models and
|
||||
// size-cap guards (an over-cap call consumes no counter value). Tests in this
|
||||
// file run sequentially in one process, so the attempt table below predicts
|
||||
// every temp path exactly:
|
||||
// over-cap: no counter use | failed write A: 0, failed write B: 1 |
|
||||
// interrupted overwrite A: 2, interrupted overwrite B: 3 | mkdir failure: 4 |
|
||||
// truncated read: 5 | success: 6 | permissions: 7 | round-trip: 8, 9.
|
||||
function predictedTmp(file: string, counter: number): string {
|
||||
return `${file}.${process.pid}.${counter}`;
|
||||
}
|
||||
|
||||
describe("disk snapshot atomic write, strict version, traced give-ups", () => {
|
||||
it("ignores a newer snapshot version without throwing", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
const file = diskSnapshotPath("t1-future");
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
// A writer from the future persists version 3; this reader must
|
||||
// treat it as "no snapshot" instead of trusting unknown data.
|
||||
writeFileSync(
|
||||
file,
|
||||
JSON.stringify({
|
||||
v: 3,
|
||||
identityFingerprint: "fp-1",
|
||||
models: [{ id: "m-future" }],
|
||||
combos: [],
|
||||
writtenAt: Date.now(),
|
||||
})
|
||||
);
|
||||
const back = await readDiskSnapshot("t1-future", "fp-1");
|
||||
assert.equal(back, undefined);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("traces an over-cap write and leaves no destination behind", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
const { messages, logger } = makeLogger();
|
||||
const bigId = `huge-${"x".repeat(33 * 1024 * 1024)}`;
|
||||
await writeDiskSnapshot("t2-cap", makeSnapshot([bigId]), "fp-1", logger);
|
||||
const file = diskSnapshotPath("t2-cap");
|
||||
assert.equal(existsSync(file), false);
|
||||
assert.deepEqual(strayEntries(file), []);
|
||||
assert.match(messages.join("\n"), /exceeds|too large|size cap/i);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("a failed write leaves no destination behind and is traced", async () => {
|
||||
const disk = isolateDisk();
|
||||
const file = diskSnapshotPath("t3a-fail");
|
||||
const blocker = predictedTmp(file, 1);
|
||||
try {
|
||||
const { messages, logger } = makeLogger();
|
||||
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-before"]), "fp-1", logger);
|
||||
// Plant a directory at the next temp path: the write fails with
|
||||
// EISDIR before any rename, deterministically, on every platform.
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
mkdirSync(blocker, { recursive: true });
|
||||
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-after"]), "fp-1", logger);
|
||||
assert.equal(existsSync(file), true);
|
||||
const back = await readDiskSnapshot("t3a-fail", "fp-1");
|
||||
assert.deepEqual(
|
||||
(back?.models ?? []).map((entry) => entry.id),
|
||||
["m-before"]
|
||||
);
|
||||
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
|
||||
} finally {
|
||||
rmSync(blocker, { recursive: true, force: true });
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("an interrupted overwrite keeps the previous snapshot", async () => {
|
||||
const disk = isolateDisk();
|
||||
const file = diskSnapshotPath("t3b-keep");
|
||||
const blocker = predictedTmp(file, 3);
|
||||
try {
|
||||
const { messages, logger } = makeLogger();
|
||||
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-before"]), "fp-1", logger);
|
||||
const before = readFileSync(file, "utf8");
|
||||
mkdirSync(blocker, { recursive: true });
|
||||
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-after"]), "fp-1", logger);
|
||||
assert.equal(readFileSync(file, "utf8"), before);
|
||||
const back = await readDiskSnapshot("t3b-keep", "fp-1");
|
||||
assert.deepEqual(
|
||||
(back?.models ?? []).map((entry) => entry.id),
|
||||
["m-before"]
|
||||
);
|
||||
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
|
||||
} finally {
|
||||
rmSync(blocker, { recursive: true, force: true });
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("a mkdir failure is traced and writes nothing", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
const { messages, logger } = makeLogger();
|
||||
// A file planted at the plugins path makes mkdir fail
|
||||
// deterministically (EEXIST on mkdir, ENOTDIR on direct writeFile).
|
||||
writeFileSync(join(disk.dir, "plugins"), "blocker");
|
||||
await writeDiskSnapshot("t3b-bis", makeSnapshot(["m-a"]), "fp-1", logger);
|
||||
assert.equal(existsSync(diskSnapshotPath("t3b-bis")), false);
|
||||
assert.match(messages.join("\n"), /failed|EEXIST|ENOTDIR|error/i);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("a truncated file reads as no snapshot without throwing", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
const { logger } = makeLogger();
|
||||
await writeDiskSnapshot("t4-truncated", makeSnapshot(["m-a"]), "fp-1", logger);
|
||||
const file = diskSnapshotPath("t4-truncated");
|
||||
const full = readFileSync(file, "utf8");
|
||||
writeFileSync(file, full.slice(0, Math.floor(full.length / 2)));
|
||||
const back = await readDiskSnapshot("t4-truncated", "fp-1");
|
||||
assert.equal(back, undefined);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("a successful write leaves no entry but the destination", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
await writeDiskSnapshot("t5-clean", makeSnapshot(["m-a"]), "fp-1");
|
||||
const file = diskSnapshotPath("t5-clean");
|
||||
assert.deepEqual(strayEntries(file), []);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("the replaced snapshot stays owner-only", async (t) => {
|
||||
if (process.platform === "win32") {
|
||||
t.skip("file mode semantics are POSIX-only");
|
||||
return;
|
||||
}
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
await writeDiskSnapshot("t6-mode", makeSnapshot(["m-a"]), "fp-1");
|
||||
const file = diskSnapshotPath("t6-mode");
|
||||
assert.equal((statSync(file).mode & 0o077) === 0, true);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips a valid snapshot with and without a logger", async () => {
|
||||
const disk = isolateDisk();
|
||||
try {
|
||||
const { logger } = makeLogger();
|
||||
const snapshot = makeSnapshot(["m-a"]);
|
||||
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1");
|
||||
const plain = await readDiskSnapshot("t7-roundtrip", "fp-1");
|
||||
assert.deepEqual(
|
||||
(plain?.models ?? []).map((entry) => entry.id),
|
||||
["m-a"]
|
||||
);
|
||||
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1", logger);
|
||||
const logged = await readDiskSnapshot("t7-roundtrip", "fp-1", logger);
|
||||
assert.deepEqual(
|
||||
(logged?.models ?? []).map((entry) => entry.id),
|
||||
["m-a"]
|
||||
);
|
||||
} finally {
|
||||
disk.restore();
|
||||
rmSync(disk.dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
371
@omniroute/opencode-plugin-v2/tests/management-token-env.test.ts
Normal file
371
@omniroute/opencode-plugin-v2/tests/management-token-env.test.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import plugin from "../src/index.js";
|
||||
import { publishCatalog } from "../src/catalog.js";
|
||||
import type { CatalogDraft } from "@opencode-ai/plugin/v2/promise";
|
||||
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types";
|
||||
|
||||
const MODELS_URL = "https://gw.example.com/v1/models";
|
||||
const COMBOS_URL = "https://gw.example.com/api/combos";
|
||||
const PRICING_MODELS_URL = "https://gw.example.com/api/pricing/models";
|
||||
|
||||
const MGMT_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
|
||||
const INFERENCE_ENV_VAR = "OMNIROUTE_API_KEY";
|
||||
|
||||
function okJson(body: unknown) {
|
||||
return { ok: true, status: 200, statusText: "OK", json: async () => body };
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
seen: Map<string, string>;
|
||||
warns: string[];
|
||||
restore: () => void;
|
||||
}
|
||||
|
||||
function installHarness(combos: unknown[]): Harness {
|
||||
const seen = new Map<string, string>();
|
||||
const warns: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
const origWarn = console.warn;
|
||||
const origLog = console.log;
|
||||
const origError = console.error;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
warns.push(String(args[0]));
|
||||
};
|
||||
console.log = () => {};
|
||||
console.error = (...args: unknown[]) => {
|
||||
warns.push(String(args[0]));
|
||||
};
|
||||
globalThis.fetch = (async (url: unknown, init?: { headers?: Record<string, string> }) => {
|
||||
const href = String(url);
|
||||
seen.set(href, String(init?.headers?.Authorization ?? ""));
|
||||
if (href.includes("/api/combos/auto")) return okJson({ combos: [] });
|
||||
if (href.includes("/api/pricing/models")) {
|
||||
return okJson({
|
||||
providers: {
|
||||
demo: {
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
models: [{ id: "team-combo", name: "Team Combo" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (href.includes("/api/pricing")) return okJson({});
|
||||
if (href.includes("/api/free-tier/summary")) return okJson({ perModel: [] });
|
||||
if (href.includes("/api/combos")) return okJson({ combos });
|
||||
return okJson({ data: [{ id: "m1" }] });
|
||||
}) as typeof fetch;
|
||||
return {
|
||||
seen,
|
||||
warns,
|
||||
restore() {
|
||||
globalThis.fetch = origFetch;
|
||||
console.warn = origWarn;
|
||||
console.log = origLog;
|
||||
console.error = origError;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withIsolatedEnv<T>(
|
||||
mgmt: string | undefined,
|
||||
inference: string | undefined,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const prevMgmt = process.env[MGMT_ENV_VAR];
|
||||
const prevInference = process.env[INFERENCE_ENV_VAR];
|
||||
// Like tests/management-token.test.ts:176-180: a fresh OPENCODE_DATA_DIR
|
||||
// per case keeps the real disk snapshot out of the run, so a filtered 'it'
|
||||
// never gets a warm snapshot served without fetch.
|
||||
const prevDataDir = process.env.OPENCODE_DATA_DIR;
|
||||
process.env.OPENCODE_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-mgmt-env-"));
|
||||
if (mgmt === undefined) delete process.env[MGMT_ENV_VAR];
|
||||
else process.env[MGMT_ENV_VAR] = mgmt;
|
||||
if (inference === undefined) delete process.env[INFERENCE_ENV_VAR];
|
||||
else process.env[INFERENCE_ENV_VAR] = inference;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (prevDataDir === undefined) delete process.env.OPENCODE_DATA_DIR;
|
||||
else process.env.OPENCODE_DATA_DIR = prevDataDir;
|
||||
if (prevMgmt === undefined) delete process.env[MGMT_ENV_VAR];
|
||||
else process.env[MGMT_ENV_VAR] = prevMgmt;
|
||||
if (prevInference === undefined) delete process.env[INFERENCE_ENV_VAR];
|
||||
else process.env[INFERENCE_ENV_VAR] = prevInference;
|
||||
}
|
||||
}
|
||||
|
||||
function setupHarness(options: Record<string, unknown>) {
|
||||
const catalogCallbacks: Array<(draft: unknown) => Promise<void>> = [];
|
||||
const ctx = {
|
||||
options,
|
||||
catalog: {
|
||||
transform: (cb: (draft: unknown) => Promise<void>) => {
|
||||
catalogCallbacks.push(cb);
|
||||
return Promise.resolve({ dispose: async () => {} });
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
transform: () => Promise.resolve({ dispose: async () => {} }),
|
||||
},
|
||||
};
|
||||
return { catalogCallbacks, ctx };
|
||||
}
|
||||
|
||||
function stubDraft() {
|
||||
const published = new Map<string, Record<string, unknown>>();
|
||||
const draft = {
|
||||
provider: { update: (_id: string, fn: (p: Record<string, unknown>) => void) => fn({}) },
|
||||
model: {
|
||||
update: (pid: string, mid: string, fn: (m: Record<string, unknown>) => void) => {
|
||||
const key = pid + "/" + mid;
|
||||
let entry = published.get(key);
|
||||
if (entry === undefined) {
|
||||
entry = { id: mid, providerID: pid };
|
||||
published.set(key, entry);
|
||||
}
|
||||
fn(entry);
|
||||
},
|
||||
},
|
||||
};
|
||||
return { draft, published };
|
||||
}
|
||||
|
||||
function fallbackWarns(warns: string[]): string[] {
|
||||
return warns.filter((w) => w.includes("managementReadToken"));
|
||||
}
|
||||
|
||||
async function runSetup(ctx: unknown): Promise<void> {
|
||||
await (plugin as unknown as { setup: (ctx: unknown) => Promise<void> }).setup(ctx);
|
||||
}
|
||||
|
||||
describe("plugin-v2 management token environment source", () => {
|
||||
it("uses the managementReadToken option for /api/* while models keep apiKey", async () => {
|
||||
await withIsolatedEnv(undefined, undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the management token from the environment when the option is absent", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
|
||||
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the option over the environment", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the inference key with a single early warning when neither is set", async () => {
|
||||
await withIsolatedEnv(undefined, undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
const atSetup = fallbackWarns(h.warns);
|
||||
assert.equal(
|
||||
atSetup.length,
|
||||
1,
|
||||
`expected exactly one early fallback warning, got: ${JSON.stringify(h.warns)}`
|
||||
);
|
||||
assert.match(atSetup[0] ?? "", /managementReadToken/);
|
||||
assert.match(atSetup[0] ?? "", new RegExp(MGMT_ENV_VAR));
|
||||
assert.ok(!(atSetup[0] ?? "").includes("chat-key"), "warning must not leak the key");
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
|
||||
assert.equal(
|
||||
fallbackWarns(h.warns).length,
|
||||
1,
|
||||
"the fallback warning stays a single setup-time notice"
|
||||
);
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an empty option as absent so the environment wins", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an empty environment value as absent so the option wins", async () => {
|
||||
await withIsolatedEnv("", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back with a warning when both the option and the environment are empty", async () => {
|
||||
await withIsolatedEnv("", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.equal(fallbackWarns(h.warns).length, 1);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("enriches the catalog from the environment token alone", async () => {
|
||||
const providers = new Map<string, ProviderV2Info>();
|
||||
const models = new Map<string, ModelV2Info>();
|
||||
const draft = {
|
||||
provider: {
|
||||
list: () => [],
|
||||
get: (id: string) => providers.get(id) as never,
|
||||
update: (id: string, fn: (p: ProviderV2Info) => void) => {
|
||||
const p = (providers.get(id) ?? { id }) as ProviderV2Info;
|
||||
fn(p);
|
||||
providers.set(id, p);
|
||||
},
|
||||
remove: () => {},
|
||||
},
|
||||
model: {
|
||||
get: () => undefined,
|
||||
update: (pid: string, mid: string, fn: (m: ModelV2Info) => void) => {
|
||||
const k = pid + "/" + mid;
|
||||
const m = (models.get(k) ?? { id: mid, providerID: pid }) as ModelV2Info;
|
||||
fn(m);
|
||||
models.set(k, m);
|
||||
},
|
||||
remove: () => {},
|
||||
default: { get: () => undefined, set: () => {} },
|
||||
},
|
||||
} as unknown as CatalogDraft;
|
||||
let seenCombos = "";
|
||||
let seenPricing = "";
|
||||
const res = await withIsolatedEnv("mgmt-env-token", undefined, async () =>
|
||||
publishCatalog(
|
||||
draft,
|
||||
{
|
||||
providerId: "omniroute",
|
||||
baseURL: "https://gw.example.com",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: process.env[MGMT_ENV_VAR],
|
||||
timeoutMs: 1000,
|
||||
modelCacheTtlMs: 300000,
|
||||
usableOnly: false,
|
||||
},
|
||||
{
|
||||
fetcher: async () => [{ id: "m1" }],
|
||||
combosFetcher: async (_base, token) => {
|
||||
seenCombos = token;
|
||||
return [{ id: "team-combo", models: [{ kind: "model", model: "m1" }] }];
|
||||
},
|
||||
enrichmentFetcher: async (_base, token) => {
|
||||
seenPricing = token;
|
||||
// The process env is the source under test: the resolver output
|
||||
// flows in through the option above, so report success only when
|
||||
// the flow under test actually carried it.
|
||||
if (token !== "mgmt-env-token") return new Map();
|
||||
return new Map([["team-combo", { name: "Team Combo" }]]);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
assert.deepEqual(res, { models: 1, combos: 1, autoCombos: 0 });
|
||||
assert.equal(seenCombos, "mgmt-env-token");
|
||||
assert.equal(seenPricing, "mgmt-env-token");
|
||||
const entry = models.get("omniroute/team-combo");
|
||||
assert.ok(entry, "expected the combo entry in the published catalog");
|
||||
assert.equal(entry?.name, "Team Combo");
|
||||
});
|
||||
});
|
||||
@@ -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/telemetry.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 tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.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/telemetry.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 tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts tests/issue-13000-cold-start-combo-limit.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -4631,9 +4631,21 @@ export function buildStaticProviderEntry(
|
||||
.map((m) => m.max_output_tokens)
|
||||
.filter((v): v is number => typeof v === "number" && v > 0);
|
||||
|
||||
if (contextValues.length > 0 && outputValues.length > 0) {
|
||||
// Prefer the server-computed aggregate (accounts for explicit
|
||||
// context_length overrides and members outside memberEntries, e.g.
|
||||
// not yet resolved in /v1/models) over the raw Math.min(member)
|
||||
// lower bound. Mirrors mapComboToModelV2's limit.context logic
|
||||
// (#13000) so the static catalog and the dynamic hook agree.
|
||||
const preferredContext =
|
||||
typeof combo.computed_context_length === "number" && combo.computed_context_length > 0
|
||||
? combo.computed_context_length
|
||||
: contextValues.length > 0
|
||||
? Math.min(...contextValues)
|
||||
: undefined;
|
||||
|
||||
if (preferredContext !== undefined && outputValues.length > 0) {
|
||||
entry.limit = {
|
||||
context: Math.min(...contextValues),
|
||||
context: preferredContext,
|
||||
output: Math.min(...outputValues),
|
||||
};
|
||||
}
|
||||
@@ -5511,6 +5523,32 @@ export function createOmniRouteConfigHook(
|
||||
|
||||
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||
|
||||
// Snapshot backfill for computed_context_length: a live /api/combos
|
||||
// response can come back without this field (server hasn't finished
|
||||
// recomputing it yet, e.g. just after a restart) even though the
|
||||
// combo's members and identity are otherwise unchanged. When that
|
||||
// happens, prefer the last-known-good value from the warm disk
|
||||
// snapshot over the Math.min(member) fallback in
|
||||
// mapComboToModelV2() — never overwrite any other combo field
|
||||
// (models/name/etc.) with stale data, only this one derived number.
|
||||
if (warmSnapshot) {
|
||||
const snapshotComboById = new Map(warmSnapshot.rawCombos.map((c) => [c.id, c]));
|
||||
for (const combo of localRawCombos) {
|
||||
const hasLive =
|
||||
typeof combo.computed_context_length === "number" &&
|
||||
combo.computed_context_length > 0;
|
||||
if (hasLive) continue;
|
||||
const stale = snapshotComboById.get(combo.id);
|
||||
if (
|
||||
stale &&
|
||||
typeof stale.computed_context_length === "number" &&
|
||||
stale.computed_context_length > 0
|
||||
) {
|
||||
combo.computed_context_length = stale.computed_context_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||
// live fetch returned no models AND features.diskCache !== false,
|
||||
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||
@@ -5518,9 +5556,17 @@ export function createOmniRouteConfigHook(
|
||||
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
|
||||
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
|
||||
if (snapshot && snapshot.rawModels.length > 0) {
|
||||
// Report snapshot age like the warm-startup path already does:
|
||||
// "stale" alone reads as a transient blip, so a week-old catalog
|
||||
// is indistinguishable from a five-minute-old one.
|
||||
const snapshotAge = snapshot.writtenAt;
|
||||
const snapshotAgeLabel =
|
||||
typeof snapshotAge === "number"
|
||||
? `${Math.round((Date.now() - snapshotAge) / 3_600_000)}h`
|
||||
: "unknown";
|
||||
logAt(
|
||||
"warn",
|
||||
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
|
||||
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models, age ${snapshotAgeLabel})`
|
||||
);
|
||||
localRawModels = snapshot.rawModels;
|
||||
localRawCombos = snapshot.rawCombos;
|
||||
|
||||
@@ -481,10 +481,7 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
|
||||
];
|
||||
assert.ok(entry);
|
||||
const ids = Object.keys(entry.models).sort();
|
||||
assert.deepEqual(ids, [
|
||||
"claude-sonnet-4-6",
|
||||
"gemini-3-flash",
|
||||
]);
|
||||
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
|
||||
assert.equal(entry.models["claude-tier"], undefined, "no combo entry");
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
|
||||
@@ -1041,11 +1038,7 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
|
||||
];
|
||||
assert.ok(entry);
|
||||
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained"
|
||||
);
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
|
||||
});
|
||||
|
||||
test("config: enrichment fetcher throws → soft-fail (warn + raw-id static catalog)", async () => {
|
||||
@@ -1068,11 +1061,7 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry, "static block still published on enrichment failure");
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id retained"
|
||||
);
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
|
||||
assert.equal(enrichmentFetcher.callCount(), 1);
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
|
||||
@@ -1270,10 +1259,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
||||
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(
|
||||
entry.models["claude-sonnet-4-6"],
|
||||
"stale snapshot hydrated into static block"
|
||||
);
|
||||
assert.ok(entry.models["claude-sonnet-4-6"], "stale snapshot hydrated into static block");
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
"Claude Sonnet 4.6 (cached)",
|
||||
@@ -1281,14 +1267,95 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
|
||||
);
|
||||
assert.equal(writes, 0, "disk write skipped when live fetch failed");
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("using stale disk cache") ||
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
logger.entries.some(
|
||||
(e) =>
|
||||
String(e[0]).includes("using stale disk cache") ||
|
||||
String(e[0]).includes("warm startup from disk snapshot")
|
||||
),
|
||||
"disk-cache hydration breadcrumb emitted"
|
||||
);
|
||||
});
|
||||
|
||||
// The stale-fallback branch (`modelsFetchThrew && wantDiskCache && !warmSnapshot`)
|
||||
// only runs when the warm-startup read found nothing — a snapshot can appear on
|
||||
// disk between that first read and the live fetch failing (e.g. another OC
|
||||
// process instance wrote one concurrently). A stateful reader simulates that:
|
||||
// empty on the warm-startup read, populated by the time the fallback re-reads.
|
||||
function emptyThenSnapshotReader(
|
||||
snapshot: Omit<
|
||||
Awaited<ReturnType<typeof import("../src/index.js").defaultDiskSnapshotReader>> & object,
|
||||
never
|
||||
>
|
||||
): typeof import("../src/index.js").defaultDiskSnapshotReader {
|
||||
let calls = 0;
|
||||
return (async () => {
|
||||
calls++;
|
||||
return calls === 1 ? undefined : snapshot;
|
||||
}) as typeof import("../src/index.js").defaultDiskSnapshotReader;
|
||||
}
|
||||
|
||||
test("config: stale-fallback warning reports the disk snapshot age in hours", async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
|
||||
});
|
||||
const fetcher = throwingModelsFetcher();
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const writtenAt = Date.now() - 2 * 3_600_000; // 2h old
|
||||
const diskSnapshotReader = emptyThenSnapshotReader({
|
||||
rawModels: [MODEL_CLAUDE],
|
||||
rawCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
writtenAt,
|
||||
});
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: true } },
|
||||
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
|
||||
);
|
||||
|
||||
await hook(makeInput());
|
||||
|
||||
assert.ok(
|
||||
logger.entries.some((e) => String(e[0]).includes("using stale disk cache (1 models, age 2h)")),
|
||||
"stale-fallback warning includes the computed snapshot age"
|
||||
);
|
||||
});
|
||||
|
||||
test('config: stale-fallback warning falls back to "unknown" age without writtenAt', async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
|
||||
});
|
||||
const fetcher = throwingModelsFetcher();
|
||||
const combosFetcher = stubCombosFetcher([]);
|
||||
const logger = captureWarn();
|
||||
|
||||
const diskSnapshotReader = emptyThenSnapshotReader({
|
||||
rawModels: [MODEL_CLAUDE],
|
||||
rawCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
});
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", features: { diskCache: true } },
|
||||
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
|
||||
);
|
||||
|
||||
await hook(makeInput());
|
||||
|
||||
assert.ok(
|
||||
logger.entries.some((e) =>
|
||||
String(e[0]).includes("using stale disk cache (1 models, age unknown)")
|
||||
),
|
||||
'stale-fallback warning falls back to "unknown" when writtenAt is absent'
|
||||
);
|
||||
});
|
||||
|
||||
test("config: cached rawEnrichment from earlier provider hook is reused (no refetch)", async () => {
|
||||
const readAuthJson = stubReadAuthJson({
|
||||
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
|
||||
@@ -1376,10 +1443,7 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.ok(entry);
|
||||
assert.equal(
|
||||
entry.models["claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
assert.equal(entry.models["gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
|
||||
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
|
||||
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
|
||||
@@ -1495,10 +1559,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(
|
||||
entryA.models["claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
|
||||
// Second invocation (cache hit) — name must still be single-suffixed.
|
||||
const inputB = makeInput();
|
||||
@@ -1506,10 +1567,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
|
||||
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
assert.equal(
|
||||
entryB.models["claude-sonnet-4-6"].name,
|
||||
"Claude - Claude Sonnet 4.6"
|
||||
);
|
||||
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Repro for #13000: combo context limits fall back to Math.min(member)
|
||||
* instead of using computed_context_length after cold start — no disk
|
||||
* snapshot fallback.
|
||||
*
|
||||
* Scenario (mirrors the report): a warm disk snapshot holds the combo with
|
||||
* its correct server-computed `computed_context_length` (245000, from all 6
|
||||
* members). After a restart, the live refresh's combos fetch returns the
|
||||
* SAME combo but without `computed_context_length` (e.g. the value hasn't
|
||||
* propagated yet), and the live models fetch only resolves 2 of the 6
|
||||
* members (the rest not yet in /v1/models). The background refresh then
|
||||
* republishes the provider block built from this degraded live data,
|
||||
* downgrading a previously-known-good 245000 limit to Math.min(163840,
|
||||
* 1_000_000) = 163840 — exactly the member-minimum described in the issue.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
} from "../src/index.js";
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
function stubReadAuthJson(value: Record<string, unknown>): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function makeInput(): Config {
|
||||
return { provider: {} } as unknown as Config;
|
||||
}
|
||||
|
||||
// The two members resolvable in the degraded live /v1/models response.
|
||||
const MEMBER_DEEPSEEK: OmniRouteRawModelEntry = {
|
||||
id: "deepseek-v4-pro",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 163_840,
|
||||
max_output_tokens: 64_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MEMBER_GLM: OmniRouteRawModelEntry = {
|
||||
id: "glm-5.2",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 16_384,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
// The other member that IS present once the server is fully warm.
|
||||
const MEMBER_GLM_53_HIGH: OmniRouteRawModelEntry = {
|
||||
id: "GLM-5.3-high",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 245_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_MODELS: OmniRouteRawCombo["models"] = [
|
||||
{ kind: "model", model: "deepseek-v4-pro", weight: 25 },
|
||||
{ kind: "model", model: "glm-5.2", weight: 25 },
|
||||
{ kind: "model", model: "GLM-5.3-high", weight: 50 },
|
||||
];
|
||||
|
||||
test("issue #13000: warm combo limit (245000) survives a degraded post-restart refresh instead of downgrading to Math.min(member)", async () => {
|
||||
const warmSnapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MEMBER_DEEPSEEK, MEMBER_GLM, MEMBER_GLM_53_HIGH],
|
||||
rawCombos: [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
computed_context_length: 245_000,
|
||||
},
|
||||
],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
|
||||
const combosFetcher: OmniRouteCombosFetcher = async () => [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
// computed_context_length intentionally omitted.
|
||||
},
|
||||
];
|
||||
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
|
||||
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
|
||||
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
|
||||
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => warmSnapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Let the detached background refresh (degraded live data) complete and
|
||||
// republish the block.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
const comboModelAfter = entryAfter.models["orchestrator"];
|
||||
assert.ok(comboModelAfter, "combo model still published after refresh");
|
||||
|
||||
assert.equal(
|
||||
comboModelAfter.limit.context,
|
||||
245_000,
|
||||
`expected the combo limit to stay at the known-good 245000, but got ${comboModelAfter.limit.context} ` +
|
||||
`(Math.min(member) fallback — the exact bug described in #13000)`
|
||||
);
|
||||
});
|
||||
|
||||
test("issue #13000 (control): no warm snapshot exists — Math.min(member) fallback is still used (expected, documented behavior)", async () => {
|
||||
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
|
||||
const combosFetcher: OmniRouteCombosFetcher = async () => [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
// computed_context_length intentionally omitted.
|
||||
},
|
||||
];
|
||||
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
|
||||
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
|
||||
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
|
||||
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
|
||||
|
||||
// No prior snapshot on disk.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
const comboModelAfter = entryAfter.models["orchestrator"];
|
||||
assert.ok(comboModelAfter, "combo model published on cold first run");
|
||||
|
||||
// No snapshot to backfill from — Math.min(163840, 1_000_000) = 163840.
|
||||
assert.equal(
|
||||
comboModelAfter.limit.context,
|
||||
163_840,
|
||||
"pure cold start with no snapshot must keep using the Math.min(member) fallback"
|
||||
);
|
||||
});
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 359 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 360 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -56,7 +56,7 @@ 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 (175 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (176 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 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 |
|
||||
@@ -664,7 +664,7 @@ focused checks, and use a Conventional Commit message (for example, `docs: slim
|
||||
## Environment
|
||||
|
||||
- **Runtime**: Node.js ≥22.22.2 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
|
||||
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.4.0` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
|
||||
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.4.2` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
|
||||
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
|
||||
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
|
||||
- **Default port**: 20128 (API + dashboard on same port)
|
||||
|
||||
327
CHANGELOG.md
327
CHANGELOG.md
@@ -92,31 +92,31 @@
|
||||
|
||||
## [3.8.51] — TBD
|
||||
|
||||
_Living section — reconciled 2026-09-07 from all cycle commits (`release/v3.8.50` tip `091589089c` → `release/v3.8.51` tip `d6f315018a`, 696 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
|
||||
_Living section — reconciled 2026-09-15 from all cycle commits (`091589089c` → `c0f92ec98a`, 916 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
|
||||
|
||||
### 📊 Release by the numbers
|
||||
|
||||
| | |
|
||||
| --- | ---: |
|
||||
| 👥 People who contributed | **120** |
|
||||
| 📝 Commits in the cycle | **696** |
|
||||
| 🔀 Pull requests referenced | **672** |
|
||||
| 📋 Changelog entries | **685** |
|
||||
| 🙌 Contributors credited in entries | **118** |
|
||||
| 👥 People who contributed | **123** |
|
||||
| 📝 Commits in the cycle | **916** |
|
||||
| 🔀 Pull requests referenced | **892** |
|
||||
| 📋 Changelog entries | **919** |
|
||||
| 🙌 Contributors credited in entries | **121** |
|
||||
| 🤖 Automated dependency commits | 18 |
|
||||
|
||||
**Entries by type**
|
||||
|
||||
| Type | Count |
|
||||
| --- | ---: |
|
||||
| 🐛 Fixes | 403 |
|
||||
| ✨ Features | 118 |
|
||||
| 🧹 Chore | 72 |
|
||||
| 📚 Docs | 36 |
|
||||
| 🧪 Tests | 29 |
|
||||
| 🐛 Fixes | 592 |
|
||||
| ✨ Features | 131 |
|
||||
| 🧹 Chore | 93 |
|
||||
| 📚 Docs | 42 |
|
||||
| 🧪 Tests | 32 |
|
||||
| ♻️ Refactor | 8 |
|
||||
| 🏗️ Build | 7 |
|
||||
| ⚡ Performance | 7 |
|
||||
| ♻️ Refactor | 7 |
|
||||
| 🏗️ Build | 6 |
|
||||
| ⚙️ CI | 2 |
|
||||
| ⏪ Reverts | 2 |
|
||||
| 📦 Dependencies | 2 |
|
||||
@@ -124,35 +124,35 @@ _Living section — reconciled 2026-09-07 from all cycle commits (`release/v3.8.
|
||||
|
||||
### 🏆 Top 25 contributors this cycle
|
||||
|
||||
_By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
|
||||
_By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
|
||||
|
||||
| # | Contributor | Commits |
|
||||
| ---: | --- | ---: |
|
||||
| 🥇 | diegosouzapw | 240 |
|
||||
| 🥈 | Paco Cartones (@pacocartones) | 58 |
|
||||
| 🥉 | Bob.Hou (@HouMinXi) | 57 |
|
||||
| 4 | Dizzle (@maxmad64bis) | 27 |
|
||||
| 5 | Webman (@jonlwheat2-gif) | 22 |
|
||||
| 6 | Ravi Tharuma (@RaviTharuma) | 21 |
|
||||
| 7 | backryun | 17 |
|
||||
| 8 | Markus Hartung (@hartmark) | 17 |
|
||||
| 9 | Nguyen Thanh Dat (@ntdatt812) | 16 |
|
||||
| 10 | Rafa Martins (@rafacpti23) | 10 |
|
||||
| 11 | Syed Raheemuddin (@raheemuddin786) | 10 |
|
||||
| 12 | Koosha Paridehpour (@KooshaPari) | 9 |
|
||||
| 13 | Nguyễn Viết Tuấn (@TheDemonTuan) | 8 |
|
||||
| 14 | MumuTW | 7 |
|
||||
| 15 | Paijo (@oyi77) | 7 |
|
||||
| 16 | Tobias Andersen (@turbolego) | 6 |
|
||||
| 17 | Bl0ck (@Bl0ck154) | 5 |
|
||||
| 18 | SHANMUGAPRIYAN (@geek007git) | 5 |
|
||||
| 19 | KaspaPulse | 5 |
|
||||
| 20 | Abhishek Sharma (@abhisheksharma2411) | 4 |
|
||||
| 21 | Abhishek Divekar (@adivekar-utexas) | 4 |
|
||||
| 22 | Andrew B. (@AndrianBalanescu) | 4 |
|
||||
| 23 | NoxzRCW | 4 |
|
||||
| 24 | opensource-elearning | 4 |
|
||||
| 25 | Alvin T. Veroy (@alvinveroy) | 3 |
|
||||
| 🥇 | diegosouzapw | 323 |
|
||||
| 🥈 | Bob.Hou (@HouMinXi) | 79 |
|
||||
| 🥉 | Paco Cartones (@pacocartones) | 77 |
|
||||
| 4 | Dizzle (@maxmad64bis) | 49 |
|
||||
| 5 | Koosha Paridehpour (@KooshaPari) | 43 |
|
||||
| 6 | Nguyen Thanh Dat (@ntdatt812) | 31 |
|
||||
| 7 | Ravi Tharuma (@RaviTharuma) | 26 |
|
||||
| 8 | Markus Hartung (@hartmark) | 22 |
|
||||
| 9 | Webman (@jonlwheat2-gif) | 22 |
|
||||
| 10 | backryun | 17 |
|
||||
| 11 | anhtahaylove | 15 |
|
||||
| 12 | Rafa Martins (@rafacpti23) | 10 |
|
||||
| 13 | Syed Raheemuddin (@raheemuddin786) | 10 |
|
||||
| 14 | Nguyễn Viết Tuấn (@TheDemonTuan) | 8 |
|
||||
| 15 | MumuTW | 7 |
|
||||
| 16 | Paijo (@oyi77) | 7 |
|
||||
| 17 | Tobias Andersen (@turbolego) | 6 |
|
||||
| 18 | Bl0ck (@Bl0ck154) | 5 |
|
||||
| 19 | SHANMUGAPRIYAN (@geek007git) | 5 |
|
||||
| 20 | KaspaPulse | 5 |
|
||||
| 21 | Abhishek Sharma (@abhisheksharma2411) | 4 |
|
||||
| 22 | Abhishek Divekar (@adivekar-utexas) | 4 |
|
||||
| 23 | Andrew B. (@AndrianBalanescu) | 4 |
|
||||
| 24 | NoxzRCW | 4 |
|
||||
| 25 | opensource-elearning | 4 |
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -278,6 +278,19 @@ _By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), au
|
||||
- **feat(dashboard):** link a log entry's Conversation Context to its owning conversation ([#12646](https://github.com/diegosouzapw/OmniRoute/pull/12646)) — thanks @hartmark
|
||||
- **feat(catalog):** eligibility-gated free-tier bucket ([#12669](https://github.com/diegosouzapw/OmniRoute/pull/12669))
|
||||
- **feat(db):** fail-closed previous_response_id continuation for redacted video turns (#12150 P2b) ([#12707](https://github.com/diegosouzapw/OmniRoute/pull/12707))
|
||||
- **feat(gamification): pay the documented `streak_bonus` and `badge_unlock` XP rewards.** `XP_REWARDS` listed both rewards but the award pipeline never paid them: the private reward table in `events.ts` omitted them, `updateStreak()` did not report when a streak extended, and badge unlocks carried no XP. Every request that extends a daily streak now pays `streak_bonus × streak length` once per UTC day (guarded by a same-day `xp_audit_log` check), and every badge unlocked through the pipeline pays `badge_unlock` once per badge (guarded by the `user_badges` primary key; `unlockBadge()` now reports whether it inserted). Bonus XP flows through the same `addXp` + level sync + global/weekly/monthly leaderboard path as action XP, so level-ups and rankings include it. The Radar supporter recognition unlock stays XP-free. (#12522 — thanks @pacocartones)
|
||||
- **feat(providers):** advertise a `free-tier` capability in the provider plugin manifest for every provider with documented free models, so sidecars and dashboards can filter free-capable providers without reading the quota catalog ([#12786](https://github.com/diegosouzapw/OmniRoute/pull/12786)) — thanks @maxmad64bis
|
||||
- **feat(providers):** Added EURouter as an OpenAI-compatible API-key gateway (`https://api.eurouter.ai/v1`), with live model discovery via `passthroughModels`. Its copy states that models are served by third-party upstreams listed per model, so an EU-based router is not read as EU data residency for inference. ([#13025](https://github.com/diegosouzapw/OmniRoute/pull/13025)) — thanks @ntdatt812
|
||||
- **feat(providers):** Added GreenPT as an OpenAI-compatible API-key provider (`https://api.greenpt.ai/v1`), with live model discovery via `passthroughModels`. No free-inference badge: the published docs describe a free API subscription billed per token, not a free tier. ([#13024](https://github.com/diegosouzapw/OmniRoute/pull/13024)) — thanks @ntdatt812
|
||||
- **feat(i18n):** 9 new locales — Greek (`el`), Croatian (`hr`), Serbian (`sr`), Lithuanian (`lt`), Estonian (`et`), Latvian (`lv`), Slovenian (`sl`), Maltese (`mt`), Irish (`ga`) — across the dashboard, docs mirrors, CLI, README and the site; OmniRoute now ships every official EU language (51 locales). (#13044)
|
||||
- **feat(i18n):** 8 new locales — Kannada (`kn`), Malayalam (`ml`), Odia (`or`), Punjabi (`pa`), Nepali (`ne`), Sinhala (`si`), Burmese (`my`), Khmer (`km`) — across the dashboard, docs mirrors, CLI, README and the site (59 locales). The translator now restores the ICU literal escape around angle placeholders and splits oversized docs sections before translating. (#0000) ([#13660](https://github.com/diegosouzapw/OmniRoute/pull/13660))
|
||||
- feat(providers): **list Agnes 3.0 Flash as the current free chat model, drop retired 1.5 Flash, add Image 2.0/2.5 Flash plus Video 2.5/2.5 Flash, and discover the live `/v1/models` catalog (including the CN host `api.agnes-ai.cn`).** `agnes-1.5-flash` now forwards to `agnes-3.0-flash`. Video 2.5 polls `GET /v1/videos/{id}` (not the V2.0 `/agnesapi` contract). Live `/v1/models` (2026-09-09) no longer serves 1.5; the wiki marks it deprecated. 3.0 Flash is 512K context / 65,536 max output, same window as 2.5. CN-region keys use the existing per-connection base-URL field, default stays `apihub.agnes-ai.com`. ([#13120](https://github.com/diegosouzapw/OmniRoute/pull/13120)) — thanks @HouMinXi
|
||||
- **feat(sse):** Codex and OpenAI catalogs list GPT-6 Astra with effort aliases (`-low` through `-ultra`); Codex CLI identity pins `@openai/codex@0.153.4` in lockstep with the image ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) — thanks @HouMinXi
|
||||
- **feat(dashboard):** the orchestration History tab gained a "Compare runs" mode — toggling it turns each grid cell into a 2-item selection queue (a 3rd click drops the oldest pick), and picking two cells opens a side-by-side comparison panel instead of the usual detail drawer. The panel fetches both runs' detail the same way the drawer does (falling back to persisted history once a run leaves the live TTL window) and shows, per side: identity/source/state, start time, a signed `right - left` delta for duration/cost/event count, the event timeline aligned by index, and any memory hits. One side's fetch failing never blocks the other, and a delta is only ever computed when both sides have a finite value — otherwise it renders "—", never `NaN`. Comparing two runs from different sources or skills still works; a banner marks the deltas as informational rather than hiding them, since the two runs aren't a strict apples-to-apples pair. ([#12677](https://github.com/diegosouzapw/OmniRoute/pull/12677))
|
||||
- **feat(dashboard):** badge a conversation that never reached a clean stop ([#12717](https://github.com/diegosouzapw/OmniRoute/pull/12717)) — thanks @hartmark
|
||||
- **feat(release):** reconcile-changelog tool + version-anchored fragment aggregation ([#12987](https://github.com/diegosouzapw/OmniRoute/pull/12987))
|
||||
- **feat(mcp):** carry context_length through MCP list_models_catalog projection (#12776) ([#13406](https://github.com/diegosouzapw/OmniRoute/pull/13406)) — thanks @KooshaPari
|
||||
- **feat(cli):** make startup readiness budget configurable (#13369) ([#13433](https://github.com/diegosouzapw/OmniRoute/pull/13433)) — thanks @KooshaPari
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -694,6 +707,194 @@ _By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), au
|
||||
- **fix(i18n):** quote <name> placeholder in OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES description (#12505) ([#12769](https://github.com/diegosouzapw/OmniRoute/pull/12769)) — thanks @KooshaPari
|
||||
- **fix(api/pricing):** surface validation error message as string, not raw object (#12494) ([#12771](https://github.com/diegosouzapw/OmniRoute/pull/12771)) — thanks @KooshaPari
|
||||
- **fix(sse):** re-enable prompt compression for native Codex passthrough ([#12834](https://github.com/diegosouzapw/OmniRoute/pull/12834)) — thanks @thomasmaerz
|
||||
- **fix(dashboard):** model health tests for a provider node set to the Responses API now call `/v1/responses` with a Responses-shaped body instead of `/v1/chat/completions` — those models were reported as `Provider returned HTTP 200 but no text content` even though the same model answered normally through `/v1/responses` ([#13070](https://github.com/diegosouzapw/OmniRoute/issues/13070)) ([#13087](https://github.com/diegosouzapw/OmniRoute/pull/13087)) — thanks @ntdatt812
|
||||
- fix(routing): stop a round-robin combo's "opencode" targets from collapsing onto the opencode-zen connection (#11912) ([#13283](https://github.com/diegosouzapw/OmniRoute/pull/13283))
|
||||
- fix(routing): stop the reactive-compaction debug log from lying when compression is globally disabled (#11977) ([#13276](https://github.com/diegosouzapw/OmniRoute/pull/13276))
|
||||
- fix(electron): relativize standalone-bundle symlink targets so Stage 8 manifest verification stops failing on Windows (#11979) ([#13251](https://github.com/diegosouzapw/OmniRoute/pull/13251))
|
||||
- fix(dashboard): surface a visible error when Compression Studio's combined preview run fails (#12061) ([#13288](https://github.com/diegosouzapw/OmniRoute/pull/13288))
|
||||
- fix(dashboard): make the compression "Effective pipeline" preview honor the active profile and warn when the master switch is off (#12063) ([#13287](https://github.com/diegosouzapw/OmniRoute/pull/13287))
|
||||
- fix(providers): scope TinyCMS Web signer's DOM shims to each call instead of leaking them for the process lifetime, and surface a clean HTTP status on a non-JSON interception-toggles error (#12072) ([#13254](https://github.com/diegosouzapw/OmniRoute/pull/13254))
|
||||
- fix(guardrails): stop Vision Bridge from re-selecting a model locked after a 404 (#12111) ([#13259](https://github.com/diegosouzapw/OmniRoute/pull/13259))
|
||||
- fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129) ([#13278](https://github.com/diegosouzapw/OmniRoute/pull/13278))
|
||||
- fix(providers): minimax-m3 now collapses manual thinking.type:"enabled" to adaptive, preventing upstream 400 (2013) (#12132) ([#13249](https://github.com/diegosouzapw/OmniRoute/pull/13249))
|
||||
- fix(db): scope model visibility overrides by modality so hiding a Chat model no longer hides an identically-ID'd Image/Embeddings/etc. model (#12172) ([#13258](https://github.com/diegosouzapw/OmniRoute/pull/13258))
|
||||
- fix(db): scope local-provider apiKey dedup to matching base URL so LM Studio/Ollama-style connections support multiple accounts (#12173) ([#13250](https://github.com/diegosouzapw/OmniRoute/pull/13250))
|
||||
- fix(providers): refresh Trae's stale Referer/Origin and forward user timezone so imported connections stop failing with 401 (#12190) ([#13255](https://github.com/diegosouzapw/OmniRoute/pull/13255))
|
||||
- fix(providers): route opencode-go/gpt-5.6-luna to /responses instead of /chat/completions (#12196) ([#13275](https://github.com/diegosouzapw/OmniRoute/pull/13275))
|
||||
- fix(dashboard): allow deleting the last extra-upstream-header row even when invalid (#12251) ([#13252](https://github.com/diegosouzapw/OmniRoute/pull/13252))
|
||||
- **fix(i18n):** translate pre-existing `__MISSING__:` keys for `combo.sort`, `requestLogger.detail` expand/collapse, `common.profile`, and `settings.resilienceCredentialHealth*` across 39 locales ([#12272](https://github.com/diegosouzapw/OmniRoute/issues/12272)) ([#13034](https://github.com/diegosouzapw/OmniRoute/pull/13034)) — thanks @HouMinXi
|
||||
- fix(cli): run the Node.js runtime compatibility guard before the heavy `tsx/esm` + Commander import chain so an unsupported runtime gets a clear message instead of a raw `Invalid regular expression flags` crash (#12296) ([#13253](https://github.com/diegosouzapw/OmniRoute/pull/13253))
|
||||
- fix(dashboard): refresh the providers list after deleting a compatible provider node (#12298) ([#13256](https://github.com/diegosouzapw/OmniRoute/pull/13256))
|
||||
- fix(usage): fail closed on API-key budget enforcement when a provider's `auto` routing alias has no pricing row, instead of silently counting it as $0 (#12341) ([#13257](https://github.com/diegosouzapw/OmniRoute/pull/13257))
|
||||
- **fix(providers):** include the submitted Agnes video model when polling by `video_id` ([#12356](https://github.com/diegosouzapw/OmniRoute/pull/12356)) — thanks @pacocartones
|
||||
- **fix(routing):** custom OpenAI-compatible nodes now honor the saved Chat/Responses API type after edits instead of letting the node's original ID prefix override the live connection setting ([#11884](https://github.com/diegosouzapw/OmniRoute/issues/11884)). ([#12358](https://github.com/diegosouzapw/OmniRoute/pull/12358)) — thanks @pacocartones
|
||||
- fix(sse): surface an error instead of a silent empty 200 when a Claude stream closes with zero bytes (#12398) ([#13285](https://github.com/diegosouzapw/OmniRoute/pull/13285))
|
||||
- fix(oauth): warn before the dead localhost:8080 redirect in antigravity/gemini `oauth start` (#12413) ([#13265](https://github.com/diegosouzapw/OmniRoute/pull/13265))
|
||||
- fix(providers): stop devin-cli spawn error from double-closing the SSE controller (#12517) ([#13261](https://github.com/diegosouzapw/OmniRoute/pull/13261))
|
||||
- **fix(rerank):** clamp Voyage `top_k` to the documents actually sent after empty-string filtering, and honor `return_documents: false` in the NVIDIA response adapter (#12523 — thanks @pacocartones)
|
||||
- **fix(audio):** `/v1/audio/translations` now resolves combo names the way `/v1/audio/transcriptions` already does, so a combo that `GET /v1/models` advertises is fanned out to its targets instead of being rejected with `400 Invalid translation model: <combo>. Use format: provider/model`; literal `provider/model` ids and unknown bare names behave as before (#12536 — thanks @pacocartones)
|
||||
- **fix(gemini):** strip the JSON-Schema-2020-12 `prefixItems` keyword from Gemini tool schemas at every nesting level, so Claude Code tool definitions no longer fail with `400 Unknown name "prefixItems"` on Gemini models (#12540 — thanks @pacocartones)
|
||||
- **fix(api-manager):** Expose an accessible loading status while API keys are fetched instead of an empty accessibility tree (#12541 — thanks @pacocartones)
|
||||
- **fix(video):** Clamp `estimateJpegFrameBytes` at zero for padding-only payloads and build the three encode-side frame data URIs from `JPEG_FRAME_DATA_URI_PREFIX` instead of a repeated literal (#12543 — thanks @pacocartones)
|
||||
- **fix(devin):** accept Windows `DEVIN_AGENTIC_HOME` sandbox paths (`C:\...\.sandbox\...`) in the isolated-home check so the Devin Claude Bridge no longer fails closed on Windows ([#12405](https://github.com/diegosouzapw/OmniRoute/issues/12405)) (#12545 — thanks @pacocartones)
|
||||
- **fix(db):** Add `conversation_turn_nodes` and orphaned `agentic_conversations` to the auto-cleanup cycle under the existing `retention.callLogs` window, so identity nodes whose call-log content has already been purged no longer accumulate without bound in `storage.sqlite` (#12548 — thanks @pacocartones)
|
||||
- **fix(i18n):** Wrap the `~/.claude/profiles/<name>/settings.json` placeholder in ICU single quotes in the `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` feature-flag description across all 42 locales and the TypeScript default, so next-intl no longer fails with `INVALID_MESSAGE: UNCLOSED_TAG` and the Feature Flags card shows the description instead of the raw key (#12549 — thanks @pacocartones)
|
||||
- **fix(orchestration):** `updateCloudAgentTask` now publishes the task's real `status` on `agent.task.updated` when an update only touches `result`, `activities` or `error`, instead of the fabricated `"updated"` state, and stays silent when no row matched the id (#12550 — thanks @pacocartones)
|
||||
- **fix(i18n):** the home "Recent Requests" panel and the Provider Topology legend are now translated instead of rendering English copies on non-English dashboards; the legend reads its own `home.topologyLegend*` labels with consistent casing rather than borrowing the memory-settings "Recent" and analytics "Error" strings (#12551 — thanks @pacocartones).
|
||||
- **docs(reference):** bring the `FEATURE_FLAGS.md` catalog back to 1:1 with `featureFlagDefinitions.ts` — 20 missing flags added, the two `*_BLOCK_THRESHOLD` env-only knobs moved out of the flag tables, category/total counts and the Live WS port corrected, guarded by a static test (#12552 — thanks @pacocartones)
|
||||
- **fix(i18n):** backfill missing `usage.kiloPass*` strings in 39 locales and restore `featureFlagOmnirouteDisableThinkingLevelVariantsDescription` in `pt.json` ([#12561](https://github.com/diegosouzapw/OmniRoute/issues/12561)) ([#13027](https://github.com/diegosouzapw/OmniRoute/pull/13027)) — thanks @HouMinXi
|
||||
- fix(docker): default docker-compose app ports (dashboard/API/live-WS) to loopback instead of `0.0.0.0`, closing the anonymous `/v1` LAN/WAN exposure gap left open by `REQUIRE_API_KEY=false` (#12568) ([#13240](https://github.com/diegosouzapw/OmniRoute/pull/13240))
|
||||
- fix(api): close DNS-rebinding SSRF gap in webhook outbound-URL guard (#12569) ([#13243](https://github.com/diegosouzapw/OmniRoute/pull/13243))
|
||||
- fix(docker): require a per-session token on the VNC browser CDP bridge and isolate it on a dedicated Docker network (#12571) ([#13241](https://github.com/diegosouzapw/OmniRoute/pull/13241))
|
||||
- fix(open-sse): write Adobe Firefly session tokens and cookie jars with 0700/0600 permissions instead of the process umask (#12572) ([#13242](https://github.com/diegosouzapw/OmniRoute/pull/13242))
|
||||
- fix(api): remove hardcoded wildcard CORS in openai-to-gemini-sse.ts so the centralized fail-closed CORS gate is the sole source of `Access-Control-Allow-Origin` (#12573) ([#13239](https://github.com/diegosouzapw/OmniRoute/pull/13239))
|
||||
- fix(api): enforce API key policy (budget/rate-limit/schedule/endpoint scoping) on the ElevenLabs speech-to-text, text-to-speech and voices proxy routes (#12574) ([#13244](https://github.com/diegosouzapw/OmniRoute/pull/13244))
|
||||
- fix(sse): cap HuggingChat NDJSON body size and bound the read loop with the fetch timeout so a stalled or hostile upstream cannot buffer unbounded memory (#12577) ([#13282](https://github.com/diegosouzapw/OmniRoute/pull/13282))
|
||||
- fix(docker): scope the cliproxyapi/qdrant/bifrost sidecars to loopback by default and forward `CLIPROXYAPI_MANAGEMENT_KEY` into the cliproxyapi container so its management API is not left both unauthenticated and LAN-published (#12578) ([#13240](https://github.com/diegosouzapw/OmniRoute/pull/13240))
|
||||
- fix(api): create DB export temp paths with `fs.mkdtempSync` instead of predictable timestamps (#12579) ([#13237](https://github.com/diegosouzapw/OmniRoute/pull/13237))
|
||||
- Cline 401 bodies that say "re-authenticate your Cline account" classify as a refreshable OAuth token, not a terminal expired key. The cooling panel no longer labels every cooldown as a 429; it shows the recorded last error instead. (#12594) ([#13060](https://github.com/diegosouzapw/OmniRoute/pull/13060)) — thanks @HouMinXi
|
||||
- **fix(catalog):** degrade unknown combo targets instead of dropping LCD modalities, and persist OpenRouter `architecture.input_modalities` into the capability snapshot ([#12613](https://github.com/diegosouzapw/OmniRoute/issues/12613)) ([#13042](https://github.com/diegosouzapw/OmniRoute/pull/13042)) — thanks @HouMinXi
|
||||
- fix(providers): send `x-api-key` instead of `Authorization: Bearer` for OpenCode Zen's `/v1/responses` endpoint (Muse Spark Contributor models), fixing a 401 on OmniRoute's auth header (#12633) ([#13247](https://github.com/diegosouzapw/OmniRoute/pull/13247))
|
||||
- fix(sse): surface the actionable "Auggie CLI not found" message when the shell reports a missing `auggie` binary via exit code instead of a spawn error (#12645) ([#13263](https://github.com/diegosouzapw/OmniRoute/pull/13263))
|
||||
- **fix(config):** Persist deletions of built-in background-degradation entries — when a stored settings record exists its `degradationMap` is now authoritative instead of being merged under the defaults, so an entry the user removed in the dashboard no longer reappears on the next apply or restart ([#12424](https://github.com/diegosouzapw/OmniRoute/issues/12424)) ([#12647](https://github.com/diegosouzapw/OmniRoute/pull/12647)) — thanks @pacocartones
|
||||
- **fix(gamification):** action-count badge milestones (First Token, Token Consumer, Token Machine, Token Whale, and the token-sharing tier) are now backed by a durable `xp_action_counts` counter incremented in `addXp()`, instead of a live `COUNT(*)` over `xp_audit_log`. The audit log is pruned by `retention.xpAuditLog` (default 30 days), so on a default install those "lifetime" milestones were really "actions in the last 30 days" and unlocked badges could stop unlocking once old rows aged out. `getActionCount()` and `checkActionCountBadges()` now read the same durable source, and a migration backfills existing totals from the surviving audit rows ([#12546](https://github.com/diegosouzapw/OmniRoute/issues/12546)) ([#12651](https://github.com/diegosouzapw/OmniRoute/pull/12651)) — thanks @pacocartones
|
||||
- **fix(images):** `/v1/images/edits` now iterates a combo's targets the same way `/v1/images/generations` does (#9239) instead of flattening a bare combo to its first target. A combo whose first target is not edit-capable — or lacks credentials — now falls through to a later edit-capable target rather than hard-erroring, and missing credentials are skipped (not a hard `401`) to match the generations path. The per-target skip/terminal classification is extracted into a shared `runImageComboTargets` loop, so generations behavior is unchanged ([#12547](https://github.com/diegosouzapw/OmniRoute/issues/12547)). ([#12653](https://github.com/diegosouzapw/OmniRoute/pull/12653)) — thanks @pacocartones
|
||||
- fix(sse): add first-byte watchdog to the TLS-fingerprint transport so a stalled wreq body falls back instead of hanging for minutes (#12656) ([#13272](https://github.com/diegosouzapw/OmniRoute/pull/13272))
|
||||
- fix(sse): exempt tiny-budget reasoning probes from combo quality failure and surface persisted-cooldown skips in ALL_TARGETS_SKIPPED diagnostics (#12659) ([#13269](https://github.com/diegosouzapw/OmniRoute/pull/13269))
|
||||
- fix(models): declare the real ~1M contextLength for OpenCode Zen's Muse Spark 1.2 models instead of falling back to the 200000 provider default (#12681) ([#13247](https://github.com/diegosouzapw/OmniRoute/pull/13247))
|
||||
- fix(oauth): align codebuddy-cn OAuth User-Agent with the chat/usage CLI version to avoid WAF false positives (#12702) ([#13264](https://github.com/diegosouzapw/OmniRoute/pull/13264))
|
||||
- fix(dashboard): surface an authentication-required banner instead of silently blanking database settings for a guest session (#12709) ([#13270](https://github.com/diegosouzapw/OmniRoute/pull/13270))
|
||||
- **fix(resilience):** stop unbounded queue that hangs 6min until Aborted — gate, provider slot, and Bottleneck queue now share a per-connection `maxWaitMs` budget; `fail-closed` on exhaust (503); execution backstop `executionMaxWaitMs` overridable per connection with upstream clamp ([#12715](https://github.com/diegosouzapw/OmniRoute/pull/12715)) — thanks @maxmad64bis (with thanks to @Tushar49 for surfacing the slow-provider need in #12635)
|
||||
- **fix(auto-combo):** every mode pack now carries `quality` and `reliability` weights (`quality` 0.02, 0.03 in `quality-first`; `reliability` 0.03, 0.04 in `reliability-first`), so selecting a pack no longer silences either signal; `DEFAULT_WEIGHTS` is unchanged ([#12731](https://github.com/diegosouzapw/OmniRoute/pull/12731)) — thanks @maxmad64bis
|
||||
- **fix(sse):** the provider execution pipeline keeps the upstream error `code`/`type` again — both were lost when the non-streaming leg was extracted, so a config-class `422` (Antigravity missing project) degraded into a generic account cooldown instead of reaching the gate that recognises it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(db):** a legacy `call_logs` table no longer aborts startup — the provider-stats index is created after the column healing runs, not before it, so an install predating `request_type` opens instead of failing with `no such column` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(catalog):** a malformed row in the operator-writable custom-models blob no longer kills every `auto/*` pool ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(tests):** realign the guards that the same day's merges left asserting the old behavior — the provider-count total after GreenPT and EURouter, the GLM stream buffer slot that `#12925` turned into a declared parameter, and the injection scan bound that `#13104` reshaped from a head-only window into head-plus-tail ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- fix(cache): fold tool_choice/tools/response_format into the semantic cache signature so a cached tool_calls response can no longer be replayed for a request whose tool policy forbids it (#12734) ([#13267](https://github.com/diegosouzapw/OmniRoute/pull/13267))
|
||||
- **fix(models):** stop treating provider-supplied `isFree:true`/`:free`/`0/0` flags as trusted unless the provider has a documented free tier — only the shipped free-tier catalog decides otherwise; fetched `isFree:true` trusted only for a free-tier provider, custom `isFree:true` via a trusted path ([#12744](https://github.com/diegosouzapw/OmniRoute/pull/12744)) — thanks @maxmad64bis
|
||||
- fix(memory): authenticate the internal /v1/rerank loopback call so memory reranking no longer silently degrades to unranked order when REQUIRE_API_KEY=true (#12745) ([#13268](https://github.com/diegosouzapw/OmniRoute/pull/13268))
|
||||
- fix(sse): parse Ollama Cloud's current usage markup (`$X of $Y used` aria-label, nested width style) (#12749) ([#13260](https://github.com/diegosouzapw/OmniRoute/pull/13260))
|
||||
- fix(cli): setup-opencode no longer sends an active context's management token to `/v1/models` when `--api-key`/`OMNIROUTE_API_KEY` is supplied — an explicit flag or the env var now always outranks the context's token, and the flag itself is no longer swallowed by the parent program's global `--api-key` option (#12783) ([#13246](https://github.com/diegosouzapw/OmniRoute/pull/13246))
|
||||
- fix(sse): register Arcee AI in the executor provider registry so requests reach api.arcee.ai instead of silently falling back to OpenAI (#12784) ([#13277](https://github.com/diegosouzapw/OmniRoute/pull/13277))
|
||||
- **fix(quota):** share one upstream quota read when identical saturation checks arrive at the same time, so traffic bursts don't multiply provider API calls ([#12787](https://github.com/diegosouzapw/OmniRoute/pull/12787)) — thanks @maxmad64bis
|
||||
- **fix(db):** ignore expired or invalid rate-limit cooldown writes so a stale timestamp can't lock a connection that should be usable — clearing still works as before ([#12788](https://github.com/diegosouzapw/OmniRoute/pull/12788)) — thanks @maxmad64bis
|
||||
- **fix(providers):** single source for provider order with `xao` ranking alongside `xai-oauth` ([#12790](https://github.com/diegosouzapw/OmniRoute/pull/12790)) — thanks @maxmad64bis
|
||||
- **fix(routing):** off-table models check the free-model catalog before inheriting premium prices, latency bootstraps from the observed pool median, async tiers read live database pricing with a 90-day freshness gate, and the tier cache invalidates on every pricing write ([#12792](https://github.com/diegosouzapw/OmniRoute/pull/12792)) — thanks @maxmad64bis
|
||||
- **fix(auto-combo):** snapshot scoring uses the observed per-connection account tier and per-model quality instead of neutral constants, and circuit-open providers rank lower at build time ([#12794](https://github.com/diegosouzapw/OmniRoute/pull/12794)) — thanks @maxmad64bis
|
||||
- **fix(pool):** empty `auto/*` pools now say why they are empty, and the models listing uses the same paid check as routing ([#12795](https://github.com/diegosouzapw/OmniRoute/pull/12795)) — thanks @maxmad64bis
|
||||
- fix(routing): recognize CLIProxyAPI's 'unknown provider for model' 400 as fallback-worthy (#12800) ([#13284](https://github.com/diegosouzapw/OmniRoute/pull/13284))
|
||||
- **fix(sse):** translate-mode streams now emit the estimated token counts as a trailing usage-only chunk before `[DONE]` when the upstream stays silent, so metered chat clients see totals instead of nothing ([#12828](https://github.com/diegosouzapw/OmniRoute/pull/12828)) — thanks @maxmad64bis
|
||||
- **fix(db):** provider stats stay truthful on empty and legacy databases: fallback counts default to `0` instead of `null`, latency averages read `null` (not `0`) when no durations were recorded, failures logged before the error-type column existed group under `pre_migration` instead of `unclassified`, and per-provider queries use two new composite indexes ([#12832](https://github.com/diegosouzapw/OmniRoute/pull/12832)) — thanks @maxmad64bis
|
||||
- fix(nvidia): fail open when a synced model catalog goes stale instead of gating forever (#12849) ([#13248](https://github.com/diegosouzapw/OmniRoute/pull/13248))
|
||||
- **fix(db):** WAL maintenance lives in its own module: a `TRUNCATE` checkpoint that hits a busy database now warns with its streak and retries once via `PASSIVE` instead of logging success, and closing-time checkpoints no longer report success on builds without a database file, with the busy totals visible in the authenticated monitoring health payload ([#12853](https://github.com/diegosouzapw/OmniRoute/pull/12853)) — thanks @maxmad64bis
|
||||
- **fix(health):** quota with no snapshots reads empty instead of a contradicting 0% ([#12857](https://github.com/diegosouzapw/OmniRoute/pull/12857)) — thanks @maxmad64bis
|
||||
- fix(a2a): accept the dashboard's own session cookie on /a2a so "Run message/send" no longer fails with "Unauthorized: missing or invalid API key" (#12888) ([#13271](https://github.com/diegosouzapw/OmniRoute/pull/13271))
|
||||
- **fix(logs):** the Logs grid's in-memory filter pass no longer discards rows the SQL query already matched — selecting an API key from the dropdown (which sends the key's id) returns its calls again, the Combo tab shows every combo instead of only those whose name contains a "1", and the model filter and search cover the same columns as the query ([#12896](https://github.com/diegosouzapw/OmniRoute/pull/12896)) — fixes [#12873](https://github.com/diegosouzapw/OmniRoute/issues/12873) — thanks @ntdatt812
|
||||
- **fix(a2a):** `/api/a2a/status` now builds the agent card from the request that asked for it, so a gateway reached at a non-localhost host no longer advertises `http://localhost:20128` as its A2A URL ([#12918](https://github.com/diegosouzapw/OmniRoute/pull/12918)). — thanks @ntdatt812
|
||||
- **fix(compression):** progressive aging now appends its `[COMPRESSED:aging:…]` annotation after a turn's `tool_result` blocks instead of in front of them, so Anthropic no longer rejects aged conversations with "`tool_use` ids were found without `tool_result` blocks immediately after" ([#12920](https://github.com/diegosouzapw/OmniRoute/pull/12920)). — thanks @ntdatt812
|
||||
- **fix(bedrock):** model import now resolves context limits for every vendor prefix instead of only `anthropic.*`, so `global.openai.gpt-5.6-*` no longer imports with a null `inputTokenLimit` and gets rejected pre-flight at the 200k default ([#12921](https://github.com/diegosouzapw/OmniRoute/pull/12921)). — thanks @ntdatt812
|
||||
- **fix(stream):** the 64 KB stream buffer GLM asks for is honoured instead of dropped, and the type error it caused no longer fails the API Route Typecheck gate on every open PR ([#12925](https://github.com/diegosouzapw/OmniRoute/pull/12925)) — thanks @ntdatt812
|
||||
- **fix(guardrails):** mask PII inside a `tool_result`'s nested content array, which the masker walked past while redacting its sibling block ([#12930](https://github.com/diegosouzapw/OmniRoute/pull/12930)) — thanks @ntdatt812
|
||||
- **fix(dashboard):** the Radar catalog table no longer leaves absent data unexplained — empty limits, unknown context windows, and unreported capabilities each explain themselves on hover, and a new check keeps it that way ([#12937](https://github.com/diegosouzapw/OmniRoute/pull/12937)) — thanks @maxmad64bis
|
||||
- **fix(sse):** geo-blocked opencode requests rotate to the next account proxy instead of failing, so one refused egress no longer aborts the whole chain ([#12941](https://github.com/diegosouzapw/OmniRoute/pull/12941)) — thanks @maxmad64bis
|
||||
- **fix(codex):** dashboard "clear cooldown" and the CAS recovery path now drop nested `codexScopeRateLimitedUntil` maps in the same write that nulls `rate_limited_until`, and fresh quota snapshots with headroom lift fallback-sourced scope cooldowns parked by quota preflight ([#12817](https://github.com/diegosouzapw/OmniRoute/issues/12817), [#12860](https://github.com/diegosouzapw/OmniRoute/issues/12860), [#12951](https://github.com/diegosouzapw/OmniRoute/pull/12951)) The dashboard reset-credit button (`consumeCodexResetCredit`) now exercises that same snapshot path after a successful redeem, so a filled quota bar is enough to unpark a leftover fallback Codex child without another manual clear. — thanks @HouMinXi
|
||||
- **fix(db):** resolve `sql-wasm.wasm` across global npm install and hoisted layouts, ensuring OmniRoute can boot cleanly on Node 24 when native `better-sqlite3` is uncompiled. ([#13035](https://github.com/diegosouzapw/OmniRoute/pull/13035)) — thanks @HouMinXi
|
||||
- **fix(api):** `DELETE /v1/batches/delete-completed` now sweeps only the calling API key's own completed batches (batches with no owner stay out of a key-scoped sweep on purpose), with an explicit instance-wide mode reserved for authenticated dashboard sessions, a 401 for a presented key that is unknown, revoked, deactivated, banned or expired (never falling through to the session branch), audit logging of every sweep, a sanitized 500 on failure, an owner-scoped file half (a key-scoped sweep never nulls a file another tenant owns) and an atomic sweep — chunked in 200-batch transactions in instance mode — so a mid-way error never leaves a batch pointing at a nulled file (GHSA-wvxc-jp3v-5mg5) ([#12969](https://github.com/diegosouzapw/OmniRoute/pull/12969))
|
||||
- **fix(combo):** quota-weighted routing stops drawing on an out-of-credit connection — a 402 now invalidates the stored quota snapshot instead of leaving its stale remaining percentage in place, and a snapshot older than 10 minutes no longer counts as confident headroom for the primary pool ([#12972](https://github.com/diegosouzapw/OmniRoute/pull/12972)) ([#13006](https://github.com/diegosouzapw/OmniRoute/pull/13006)) — thanks @HouMinXi
|
||||
- **fix(vision):** Custom Models with "Vision capable" checked no longer have image requests swapped to `glm/glm-4.6v` when the client sends the advertised alias (`vllm/path/...`) or the bare path-shaped id — Vision Bridge now matches the stored override for all three id forms ([#12758](https://github.com/diegosouzapw/OmniRoute/issues/12758)) ([#12974](https://github.com/diegosouzapw/OmniRoute/pull/12974)) — thanks @HouMinXi
|
||||
- **fix(sse):** transient opencode upstream failures rotate to the next account proxy instead of failing, so one flapping egress no longer aborts the whole chain ([#12975](https://github.com/diegosouzapw/OmniRoute/pull/12975)) — thanks @maxmad64bis
|
||||
- **fix(azure):** Deployments from GPT-6 onward now send `max_completion_tokens` instead of `max_tokens`, which Azure rejects with HTTP 400. The rule matched a literal `gpt-5`, so each new generation arrived broken; it now matches the generation range, while `gpt-35-turbo` still keeps `max_tokens`. ([#13007](https://github.com/diegosouzapw/OmniRoute/pull/13007)) — thanks @ntdatt812
|
||||
- **fix(db):** add an opt-in self-restart circuit for sustained critical memory pressure, gate post-cleanup VACUUM behind a minimum freed-rows threshold, and checkpoint the SQLite WAL every 5 minutes with a size guard that escalates to TRUNCATE, so a growing WAL can no longer stall the event loop into a full outage. ([#13011](https://github.com/diegosouzapw/OmniRoute/pull/13011)) — thanks @HouMinXi
|
||||
- **fix(auth):** an explicit connection pin may probe a quota-disabled row once and re-enable it on success ([#12874](https://github.com/diegosouzapw/OmniRoute/issues/12874)) ([#13017](https://github.com/diegosouzapw/OmniRoute/pull/13017)) — thanks @HouMinXi
|
||||
- **fix(api):** thread `X-OmniRoute-Fallback-Attempts` through combo chat completions so streaming and non-streaming responses report how many prior legs were attempted ([#13038](https://github.com/diegosouzapw/OmniRoute/pull/13038)) — thanks @HouMinXi
|
||||
- Restore provider failure classification and credential refresh on non-streaming requests: classify non-2xx failures to lock models on per-model quota exhaustion, update connection rate limits from headers and body, and pass credential refresh handlers to pipeline execution so 401 tokens can be refreshed and retried (#13043). ([#13069](https://github.com/diegosouzapw/OmniRoute/pull/13069)) — thanks @HouMinXi
|
||||
- **fix(responses):** wrap forced-non-streaming web_search fallback JSON as Responses SSE so Codex still sees `response.completed` ([#13050](https://github.com/diegosouzapw/OmniRoute/pull/13050)) — thanks @HouMinXi
|
||||
- **fix(antigravity):** Preserve upstream thought-token usage in normalized completion and reasoning token counts ([#13055](https://github.com/diegosouzapw/OmniRoute/pull/13055)) — thanks @pacocartones
|
||||
- Fix a concurrency-slot leak in the DeepSeek PoW solver: a worker that failed to spawn (for example a missing worker script) never released its slot, so `MAX_CONCURRENT_WORKERS` failures disabled the solver until restart. ([#13097](https://github.com/diegosouzapw/OmniRoute/pull/13097)) — thanks @anhtahaylove
|
||||
- **fix(acp):** bound the ACP session output buffers — `stdoutBuffer` and `stderrBuffer` now cap at 1 MiB keeping the most recent output behind a visible `[...output truncated...]` marker, and `stderrBuffer` is reset per prompt instead of accumulating for the lifetime of the session. ([#13100](https://github.com/diegosouzapw/OmniRoute/pull/13100)) — thanks @anhtahaylove
|
||||
- **fix(acp):** release the `stdout`/`exit` listeners and the idle timer that a `sendPrompt` timeout used to leave attached to the `acpManager` singleton, and drop sessions that exited on their own from the session map instead of keeping them forever. ([#13096](https://github.com/diegosouzapw/OmniRoute/pull/13096)) — thanks @anhtahaylove
|
||||
- **fix(security):** the prompt-injection and PII scanners now read the text a `tool_result` block carries on `content` (string or nested block list), in messages and in system blocks, so tool output is judged by the same rules as user text ([#13101](https://github.com/diegosouzapw/OmniRoute/pull/13101)) — thanks @ntdatt812
|
||||
- **fix(gamification):** close the badge notification SSE stream when the request signal is already aborted before the stream starts — a client that disconnects while the route is still awaiting auth used to leave both the 2s unlock poll and the 15s heartbeat running for the lifetime of the process. ([#13106](https://github.com/diegosouzapw/OmniRoute/pull/13106)) — thanks @anhtahaylove
|
||||
- **fix(security):** the prompt-injection scan now spends its 16 KB budget on both ends of the request instead of the first 16 KB only, so `system`, `instructions`, `query`, `documents` and the newest turns are no longer hidden behind one long message ([#13104](https://github.com/diegosouzapw/OmniRoute/pull/13104)) — thanks @ntdatt812
|
||||
- **fix(dashboard):** expose the Volcano Ark console cookie on quota scraping and unwrap connect-error objects so the dashboard shows the upstream message ([#13107](https://github.com/diegosouzapw/OmniRoute/pull/13107)) — thanks @HouMinXi
|
||||
- **fix(db):** release the `beforeExit`/`SIGINT`/`SIGTERM` handlers when a `node:sqlite` adapter closes, so a closed adapter and its database handle are no longer pinned to `process` for the lifetime of the run — the same treatment #7494 gave the sql.js adapter. ([#13109](https://github.com/diegosouzapw/OmniRoute/pull/13109)) — thanks @anhtahaylove
|
||||
- **fix(translator):** `contentSchema` and `unevaluatedItems` are now treated as subschema positions by the tool-schema sanitizer, so a truncation placeholder in either is replaced with a permissive schema instead of being forwarded as a string ([#13110](https://github.com/diegosouzapw/OmniRoute/pull/13110)) — thanks @ntdatt812
|
||||
- **fix(cli-helper):** clear the `createLogStream` timeout on the abort path — `stop()` aborts the in-flight fetch and returned through the `signal.aborted` branch, which skipped `clearTimeout` and left an armed timer per stopped stream. The stream reader is now also cancelled when the read loop exits early. ([#13114](https://github.com/diegosouzapw/OmniRoute/pull/13114)) — thanks @anhtahaylove
|
||||
- **fix(docs):** drop TLS-impersonation, MITM-decrypt, supply-chain attestation, and XOR-mask writeups from the public `/docs` catalog and Docker image. Files stay in git for engineers; operators who need them open the repo, not the website. ([#13136](https://github.com/diegosouzapw/OmniRoute/pull/13136)) — thanks @HouMinXi
|
||||
- **fix(combo):** parse numeric-epoch `rate_limited_until` in the combo cooldown read path ([#13141](https://github.com/diegosouzapw/OmniRoute/pull/13141)) — thanks @maxmad64bis
|
||||
- **fix(opencode):** both OpenCode plugins now reject a gateway address typed without `http://` at configuration time, instead of publishing every model with an api url no client can call, and the v2 plugin no longer publishes a model card whose api url is blank or relative ([#13142](https://github.com/diegosouzapw/OmniRoute/pull/13142)) — thanks @maxmad64bis
|
||||
- **fix(providers):** lock opencode model on upstream 400 model-unavailable ([#13146](https://github.com/diegosouzapw/OmniRoute/pull/13146)) — thanks @maxmad64bis
|
||||
- **fix(logging):** keep the provider exchange rather than the raw client bodies when a call log exceeds its size budget, and show that recovered payload in the request-detail panel instead of replacing it with the stored response body ([#13147](https://github.com/diegosouzapw/OmniRoute/pull/13147)) — thanks @maxmad64bis
|
||||
- **fix(traffic-inspector):** the WebSocket route no longer leaks a traffic-buffer subscriber and a 30s ping timer when the client socket is already closed at handler time — listeners are attached before any resource is acquired, a destroyed socket bails out early, and the ping interval stops on a dead socket where `write()` never throws ([#13155](https://github.com/diegosouzapw/OmniRoute/pull/13155)) — thanks @anhtahaylove
|
||||
- **fix(telegram):** bound the per-user API key cache in the Telegram chat proxy so a burst of distinct chat ids can no longer grow the process heap without limit ([#13165](https://github.com/diegosouzapw/OmniRoute/issues/13165)) ([#13166](https://github.com/diegosouzapw/OmniRoute/pull/13166)) — thanks @anhtahaylove
|
||||
- **fix(stream):** cancel the upstream response body when the JSON-to-SSE sniff unwinds on a body timeout, so a stalled upstream no longer pins the connection ([#13169](https://github.com/diegosouzapw/OmniRoute/issues/13169)) ([#13171](https://github.com/diegosouzapw/OmniRoute/pull/13171)) — thanks @anhtahaylove
|
||||
- **fix(telegram):** authenticate webhook deliveries with Telegram's `secret_token` so an unauthenticated caller can no longer mint API keys or spend upstream quota ([#13172](https://github.com/diegosouzapw/OmniRoute/issues/13172)) ([#13175](https://github.com/diegosouzapw/OmniRoute/pull/13175)) — thanks @anhtahaylove
|
||||
- **fix(models):** give discoverable Gemini 3.8 Flash ids their own 65536 output spec so Antigravity no longer clamps them to 16384 ([#13195](https://github.com/diegosouzapw/OmniRoute/pull/13195)) — thanks @HouMinXi
|
||||
- **fix(dashboard):** leftover catalog-removed provider rows (gemini-cli) can be listed and purged from the providers page ([#13067](https://github.com/diegosouzapw/OmniRoute/issues/13067)) ([#13197](https://github.com/diegosouzapw/OmniRoute/pull/13197)) — thanks @HouMinXi
|
||||
- **fix(auth):** a dashboard session now requires the `authenticated: true` claim that login, OIDC and the session refresh already emit — a JWT merely signed with `JWT_SECRET` (for example the Cursor CLI passthrough token, which any API-key holder can obtain) no longer verifies as the `auth_token` cookie on any route, the WebSocket handshake or the live server; existing sessions keep working ([#13298](https://github.com/diegosouzapw/OmniRoute/issues/13298)) ([#13375](https://github.com/diegosouzapw/OmniRoute/pull/13375))
|
||||
- **fix(api):** `DELETE /v1/batches/delete-completed` now applies the caller's API-key policy (endpoint allowlist, schedule, usage cap, rate limit) like every other `/v1` route, commits the key-scoped sweep per 200-batch chunk instead of holding one write lock for the whole sweep, refuses to loop without progress, and rejects a scope that names both a key and `allTenants` ([#13684](https://github.com/diegosouzapw/OmniRoute/pull/13684))
|
||||
- Restore the API-route typecheck gate: the non-streaming leg result lost its discriminated-union narrowing after the server-owned tool loop reassignment, producing 13 new TS2339 diagnostics in `chatCore.ts`. ([#12963](https://github.com/diegosouzapw/OmniRoute/pull/12963))
|
||||
- Restore four non-streaming behaviours the server-owned tool loop refactor dropped: client aborts map to 499 with the fixed `Request aborted` message, an aborted request no longer logs a synthetic `clientResponse`, Claude prompt-cache telemetry is recorded again, and a body that cannot be canonicalized no longer throws when the tool loop is off. ([#12990](https://github.com/diegosouzapw/OmniRoute/pull/12990))
|
||||
- **fix(skills):** The CLI registry parser now reads positionals declared with `.addArgument()`, not only those written inline in `.command()`. `tunnel create [type]` was being published as `tunnel create`, so the agent-skills sync gate reported drift on every branch and regenerating would have deleted the argument. ([#13009](https://github.com/diegosouzapw/OmniRoute/pull/13009)) — thanks @ntdatt812
|
||||
- **fix(combos):** dashboard combo test uses a short prompt, serial probes, and a 60s timeout so reasoning models and rate-limited free pools do not fail the health check ([#13001](https://github.com/diegosouzapw/OmniRoute/pull/13001)) — thanks @HouMinXi
|
||||
- fix(compression): terminate idle worker threads on eviction so long-running instances stop leaking OS threads and MessagePorts ([#13091](https://github.com/diegosouzapw/OmniRoute/pull/13091)) — thanks @anhtahaylove
|
||||
- Redact Google API keys of any length in error bodies: the pattern required exactly 39 characters, so shorter or longer `AIza…` credentials passed through unredacted. ([#12964](https://github.com/diegosouzapw/OmniRoute/pull/12964))
|
||||
- **fix(grok-cli):** a 402 "Grok Build usage balance exhausted" parks that Grok login as out of credit (Grok Build CLI, grok.com cookie, and xAI OAuth share the weekly pool). Combo routing then tries the next login instead of locking the model for every account in the pool ([#13061](https://github.com/diegosouzapw/OmniRoute/pull/13061)) — thanks @HouMinXi
|
||||
- fix(compression): spawn the LLMLingua worker with a file URL object so compression actually runs instead of silently failing open on Node ([#13093](https://github.com/diegosouzapw/OmniRoute/pull/13093)) — thanks @anhtahaylove
|
||||
- **fix(test):** run the local `test` and `test:unit` scripts at concurrency 4 so a full-suite run no longer exhausts the machine's commit charge and kills unrelated processes ([#13187](https://github.com/diegosouzapw/OmniRoute/pull/13187)) — thanks @anhtahaylove
|
||||
- **fix(monitoring):** `GET /api/monitoring/health` `credentialHealth` now includes a bounded `failedConnections` list (`connectionId`, `status`, sanitized `lastError`) when the probe-cache gauge `failed>0`, plus `source: probe-cache` and a cheap `staleDbNonOkCount` for sticky SQLite `test_status` on active rows. Documents that the live gauge is not `provider_connections.test_status`. ([#12876](https://github.com/diegosouzapw/OmniRoute/pull/12876)) — thanks @RaviTharuma
|
||||
- **fix(resilience):** an openai-compatible multi-upstream gateway no longer marks the whole connection `credits_exhausted` when a single upstream (or the `/models` / representative-model health probe) returns 402. Quota failures stay model-scoped; true connection-wide auth failures are unchanged. Single-credential 402 key disable ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239)) is preserved. ([#12875](https://github.com/diegosouzapw/OmniRoute/pull/12875)) — thanks @RaviTharuma
|
||||
- **fix(dashboard):** nine fixes on the Orchestration Canvas, all diagnosed in the Phase 2 reviews. A source that fails now keeps the timestamp of its FIRST failure instead of being re-stamped every poll — the stale line said "since the last poll" no matter how long the source had been down, and the churn also defeated the snapshot's stable identity (it serializes the source list), so the canvas re-rendered on every tick while anything was broken. A source that HAD data and only then started failing is flagged too: previously only a source with no node at all got the warning, so a source that went down mid-session kept a healthy-looking node forever. The rest are pointwise: clicking a filter chip cancels the pending search debounce (left armed, it fired ~300ms later and silently reverted the chip); the search input carries an accessible name; `?state=running, failed` parses like the unpadded form instead of dropping the padded value; the CSV toggle helper is defined once in `model/urlParams.ts` rather than twice; the agents tab tells "nothing running" apart from "the filter matched nothing", offering a clear-filters button instead of setup links that would be wrong advice there; edges stop emitting SMIL particles above 40 simultaneously active edges, keeping the colored stroke; and the drawer's error banner clears when a retried action succeeds. ([#12983](https://github.com/diegosouzapw/OmniRoute/pull/12983))
|
||||
- **fix(i18n):** the nine locales added with the EU-language batch (Greek, Estonian, Irish, Croatian, Lithuanian, Latvian, Maltese, Slovenian, Serbian) were missing the eleven Orchestration Canvas keys that Phase 3 introduced, so the compare-runs panel and the "no runs match these filters" empty state fell back to English in those languages (`deepMergeFallback` substitutes English for an absent key, so nothing rendered blank — it rendered untranslated). The coverage gate does not catch this: it enforces an 80% floor per locale, and eleven missing keys out of ~13,000 leaves coverage at 99.9%. Translated for real in each language, calibrated against the wording each file already uses for "run", "filter" and "skill". ([#13294](https://github.com/diegosouzapw/OmniRoute/pull/13294))
|
||||
- **fix(dashboard):** five follow-ups from the Phase 2 Orchestration Canvas review. The History drawer now shows the memory section for runs that already left the live TTL window: the persisted `memory_hits` event is parsed into `metadata.memoryHits` with the same defensive validation the drawer applies, and — because that event is observability rather than a state transition — it no longer leaks into the timeline, where it had been inheriting the task's state and rendering as a duplicate transition. The A2A memory recall runs against its own 1.5s deadline instead of inheriting the memory backend's 30s one; overshooting degrades exactly like any other recall failure (no hits, task proceeds), and the timer is cleared on both paths. Repeating a Conductor run carries its `requirements.cli`/`requirements.model` forward, so the new run is pinned to the same runner profile and model rather than drifting to whatever the fleet picks. A successful repeat from the Agents tab now focuses the run it created, instead of leaving the operator on the finished one. And the auth test for `POST /api/conductor/tasks` moved into the shared `ROUTES` array rather than restating the pattern. ([#12988](https://github.com/diegosouzapw/OmniRoute/pull/12988))
|
||||
- fix(plugins): stop leaking an exit listener per plugin hook timeout, which triggered MaxListenersExceededWarning on plugins that ignore SIGTERM ([#13092](https://github.com/diegosouzapw/OmniRoute/pull/13092)) — thanks @anhtahaylove
|
||||
- **fix(validation):** Provider node edits no longer fail with a generic "Invalid request" when the optional daily-quota reset fields are left blank. The dashboard sends `dailyQuotaResetTimezone` and `dailyQuotaResetHour` as `null`, and only the hour accepted it. ([#13066](https://github.com/diegosouzapw/OmniRoute/issues/13066)) ([#13083](https://github.com/diegosouzapw/OmniRoute/pull/13083)) — thanks @ntdatt812
|
||||
- **fix(combo):** quota-aware expansion drops banned, inactive, missing, and wrong-provider connections before quota fetch or model dispatch; pins and allowlists stay selectors, not a bypass. Antigravity automatic exhaustion now requires a reported zero remaining, so a positive balance below 1% stays eligible. ([#13006](https://github.com/diegosouzapw/OmniRoute/pull/13006)) — thanks @HouMinXi
|
||||
- Fix an intermittent failure in the quota-weighted routing test suite: scores are a function of `Date.now()`, so peers with identical quota scored microseconds apart never tied and swapped order. ([#12946](https://github.com/diegosouzapw/OmniRoute/pull/12946))
|
||||
- fix(volcengine): resolve quota window mapping gaps, safe multi-connection console binding, and response cookie sanitization ([#12950](https://github.com/diegosouzapw/OmniRoute/pull/12950)) — thanks @HouMinXi
|
||||
- **fix(test):** resolve the WebDAV handler path with `fileURLToPath` so the suite's 37 WebDAV tests run on Windows instead of failing with a doubled `C:\C:\` drive prefix ([#13196](https://github.com/diegosouzapw/OmniRoute/pull/13196)) — thanks @anhtahaylove
|
||||
- **fix(ui):** fall back from invalid AnySearch icon alias (direct commit `9587e07b69`)
|
||||
- **fix(sse):** stop rebuilding a truncated summary from the collector's cap-dropped event array ([#12718](https://github.com/diegosouzapw/OmniRoute/pull/12718)) — thanks @hartmark
|
||||
- **fix(conversations):** show a pending spinner for unresolved tool nodes instead of "(empty)" ([#12727](https://github.com/diegosouzapw/OmniRoute/pull/12727)) — thanks @hartmark
|
||||
- **fix(streaming):** fail fast when an upstream stream produces only lifecycle/heartbeat events, never real content ([#12741](https://github.com/diegosouzapw/OmniRoute/pull/12741)) — thanks @hartmark
|
||||
- **fix(responses-continuation):** bridge the write-in-flight window with an in-memory pending store ([#12854](https://github.com/diegosouzapw/OmniRoute/pull/12854)) — thanks @hartmark
|
||||
- **fix(translator):** drop or map agent_message on Chat Completions fallback ([#12880](https://github.com/diegosouzapw/OmniRoute/pull/12880)) — thanks @RaviTharuma
|
||||
- **fix(api):** page and stream-complete GET /v1/models for large catalogs ([#12882](https://github.com/diegosouzapw/OmniRoute/pull/12882)) — thanks @RaviTharuma
|
||||
- **fix(quota):** align AUTH window labels with usage API and clarify 503 ([#12884](https://github.com/diegosouzapw/OmniRoute/pull/12884)) — thanks @RaviTharuma
|
||||
- **fix(ci):** clear two base-reds on release/v3.8.51 — mutation-coverage gate + image-only-model guard ([#12945](https://github.com/diegosouzapw/OmniRoute/pull/12945))
|
||||
- **fix(api):** scope batch bulk-delete to the calling API key ([#13211](https://github.com/diegosouzapw/OmniRoute/pull/13211))
|
||||
- **fix(tests):** align model-sync log assertion to shipped path redaction (base-red) ([#13227](https://github.com/diegosouzapw/OmniRoute/pull/13227))
|
||||
- **fix(test):** make the ToS heading guard actually require the parentheses ([#13228](https://github.com/diegosouzapw/OmniRoute/pull/13228))
|
||||
- **fix(quality):** register 4 drifted covering tests in stryker tap.testFiles ([#13229](https://github.com/diegosouzapw/OmniRoute/pull/13229))
|
||||
- **fix(logging):** mask *-api-key headers in request-log pipeline (#13273) ([#13401](https://github.com/diegosouzapw/OmniRoute/pull/13401)) — thanks @KooshaPari
|
||||
- **fix(tests):** correct stale Jina catalog prefix in custom-models test ([#13403](https://github.com/diegosouzapw/OmniRoute/pull/13403)) — thanks @KooshaPari
|
||||
- **fix(backend):** prune old backups after health-check-repair snapshots (#13308) ([#13404](https://github.com/diegosouzapw/OmniRoute/pull/13404)) — thanks @KooshaPari
|
||||
- **fix(backend):** guard memory_fts_au trigger with WHEN clause to stop FTS bloat (#13326) ([#13405](https://github.com/diegosouzapw/OmniRoute/pull/13405)) — thanks @KooshaPari
|
||||
- **fix(free-tiers):** remove obsolete Together $25 signup credit (#12526) ([#13407](https://github.com/diegosouzapw/OmniRoute/pull/13407)) — thanks @KooshaPari
|
||||
- **fix:** skip non-chat generation models in Test all models ([#13409](https://github.com/diegosouzapw/OmniRoute/pull/13409)) — thanks @KooshaPari
|
||||
- **fix(providers):** de-duplicate agy and antigravity model catalogs ([#13410](https://github.com/diegosouzapw/OmniRoute/pull/13410)) — thanks @KooshaPari
|
||||
- **fix(dashboard):** send provider in body for mini-playground web search ([#13411](https://github.com/diegosouzapw/OmniRoute/pull/13411)) — thanks @KooshaPari
|
||||
- **fix(resilience):** add CJK quota-exhaustion patterns to 429 classifier ([#13412](https://github.com/diegosouzapw/OmniRoute/pull/13412)) — thanks @KooshaPari
|
||||
- **fix(backend):** eval runner: failed calls no longer score as passed + dotAll regex ([#13413](https://github.com/diegosouzapw/OmniRoute/pull/13413)) — thanks @KooshaPari
|
||||
- **fix(backend):** eval runner opts out of output-style and memory injection ([#13414](https://github.com/diegosouzapw/OmniRoute/pull/13414)) — thanks @KooshaPari
|
||||
- **fix:** chain serial quarantine step in npm test script (fixes #13286) ([#13418](https://github.com/diegosouzapw/OmniRoute/pull/13418)) — thanks @KooshaPari
|
||||
- **fix(antigravity):** derive stable session ID from credentials for prompt-cache reuse (#13305) ([#13424](https://github.com/diegosouzapw/OmniRoute/pull/13424)) — thanks @KooshaPari
|
||||
- **fix(memory):** merge memory into top-level system field instead of unshifting at messages[0] (#13425) ([#13427](https://github.com/diegosouzapw/OmniRoute/pull/13427)) — thanks @KooshaPari
|
||||
- **fix(compression):** preserve instruction blocks from lossy engines ([#13523](https://github.com/diegosouzapw/OmniRoute/pull/13523)) — thanks @KooshaPari
|
||||
- **fix(compression):** preserve polarity words and newlines in ultra heuristic ([#13524](https://github.com/diegosouzapw/OmniRoute/pull/13524)) — thanks @KooshaPari
|
||||
- **fix(compression):** run user preservePatterns before built-in patterns (#13457) ([#13525](https://github.com/diegosouzapw/OmniRoute/pull/13525)) — thanks @KooshaPari
|
||||
- **fix(reasoning):** expand deepseek-v4 regex to match v4.1+ and add command-code provider (#13430) ([#13528](https://github.com/diegosouzapw/OmniRoute/pull/13528)) — thanks @KooshaPari
|
||||
- **fix(providers):** add alwaysReasons metadata for reka-flash-3 ([#13531](https://github.com/diegosouzapw/OmniRoute/pull/13531)) — thanks @KooshaPari
|
||||
- **fix(playground):** allow Compare columns to scroll independently ([#13532](https://github.com/diegosouzapw/OmniRoute/pull/13532)) — thanks @KooshaPari
|
||||
- **fix(security):** empty whitelist allows through to prevent admin lockout ([#13534](https://github.com/diegosouzapw/OmniRoute/pull/13534)) — thanks @KooshaPari
|
||||
- **fix(tests):** add DISABLE_IOREG_STRATEGY env var for macOS test compatibility (fixes #13467) ([#13539](https://github.com/diegosouzapw/OmniRoute/pull/13539)) — thanks @KooshaPari
|
||||
- **fix(zed-hosted):** map developer role and Gemini enums for Zed proxy ([#13543](https://github.com/diegosouzapw/OmniRoute/pull/13543)) — thanks @KooshaPari
|
||||
- **fix(analytics):** normalize provider aliases to canonical id in usage_history ([#13545](https://github.com/diegosouzapw/OmniRoute/pull/13545)) — thanks @KooshaPari
|
||||
- **fix(combo):** use traceId for call log id to prevent UNIQUE constraint failure ([#13546](https://github.com/diegosouzapw/OmniRoute/pull/13546)) — thanks @KooshaPari
|
||||
- **fix(auth):** check model lockout before returning synthetic noauth connection ([#13547](https://github.com/diegosouzapw/OmniRoute/pull/13547)) — thanks @KooshaPari
|
||||
- **fix(guardrails):** skip credential redaction for base64 image data URLs ([#13550](https://github.com/diegosouzapw/OmniRoute/pull/13550)) — thanks @KooshaPari
|
||||
- **fix(resilience):** increase requestQueue.maxWaitMs default from 15s to 30s ([#13553](https://github.com/diegosouzapw/OmniRoute/pull/13553)) — thanks @KooshaPari
|
||||
- **fix(docs):** document OMNIROUTE_READY_TIMEOUT_MS and allowlist the test-only DISABLE_IOREG_STRATEGY ([#13692](https://github.com/diegosouzapw/OmniRoute/pull/13692))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -849,6 +1050,39 @@ _By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), au
|
||||
- **docs(security):** document socket.yml scanner config + CI workflow link (#12575) ([#12764](https://github.com/diegosouzapw/OmniRoute/pull/12764)) — thanks @KooshaPari
|
||||
- **chore(ci):** guard commit identity in pre-commit to stop author misattribution ([#12772](https://github.com/diegosouzapw/OmniRoute/pull/12772))
|
||||
- **deps:** 17 Dependabot bumps — bump electron from 43.4.0 to 43.4.1 in /electron ([#11426](https://github.com/diegosouzapw/OmniRoute/pull/11426)); bump the development group across 1 directory with 8 updates ([#11428](https://github.com/diegosouzapw/OmniRoute/pull/11428)); bump the production group across 1 directory with 11 updates ([#11440](https://github.com/diegosouzapw/OmniRoute/pull/11440)); chore(deps): bump github/codeql-action/init from 4.37.7 to 4.37.8 ([#11925](https://github.com/diegosouzapw/OmniRoute/pull/11925)); chore(deps): bump github/codeql-action from 4.37.7 to 4.37.8 ([#11926](https://github.com/diegosouzapw/OmniRoute/pull/11926)); chore(deps): bump github/codeql-action/analyze from 4.37.7 to 4.37.8 ([#11927](https://github.com/diegosouzapw/OmniRoute/pull/11927)); build(deps): bump github/codeql-action/init from 4.37.8 to 4.37.9 ([#12345](https://github.com/diegosouzapw/OmniRoute/pull/12345)); build(deps): bump github/codeql-action/analyze from 4.37.8 to 4.37.9 ([#12346](https://github.com/diegosouzapw/OmniRoute/pull/12346)); bump the development group across 1 directory with 2 updates ([#12347](https://github.com/diegosouzapw/OmniRoute/pull/12347)); build(deps): bump github/codeql-action from 4.37.8 to 4.37.9 ([#12349](https://github.com/diegosouzapw/OmniRoute/pull/12349)); bump browserslist from 4.28.2 to 4.28.8 ([#12396](https://github.com/diegosouzapw/OmniRoute/pull/12396)); bump the production group across 1 directory with 4 updates ([#12399](https://github.com/diegosouzapw/OmniRoute/pull/12399)); bump @xmldom/xmldom ([#12500](https://github.com/diegosouzapw/OmniRoute/pull/12500)); bump qs from 6.15.2 to 6.16.0 ([#12512](https://github.com/diegosouzapw/OmniRoute/pull/12512)); bump @xmldom/xmldom from 0.9.10 to 0.9.12 ([#12513](https://github.com/diegosouzapw/OmniRoute/pull/12513)); bump fast-uri from 3.1.5 to 3.1.7 ([#12514](https://github.com/diegosouzapw/OmniRoute/pull/12514)); bump @humanfs/node from 0.16.7 to 0.16.8 ([#12515](https://github.com/diegosouzapw/OmniRoute/pull/12515))
|
||||
- Clear the `release/v3.8.51` `check:agent-skills-sync` base-red: regenerate `skills/cli-tunnel/SKILL.md` so the `tunnel create [type]` positional that #13009 taught the generator to read is reflected in the committed skill. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- Clear the `release/v3.8.51` typecheck base-red from #12731: the new `Mode pack ranking gates` candidates in `open-sse/services/autoCombo/__tests__/autoCombo.test.ts` omitted the required `provider`, `model` and `errorRate` fields of `ProviderCandidate`, failing `check:open-sse-typecheck` (and with it `Fast Quality Gates`) on every open PR. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- Clear the `release/v3.8.51` docs-sync base-red: EURouter (#13025) and GreenPT (#13024) took the live provider count to 358, leaving 7 STRICT drifts (`PROVIDER_REFERENCE.md`, 4 diagrams, 2 tier-flow images) plus the `AGENTS.md` / `llm.txt` / `package.json` count claims stale. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- **test(models):** Pin `CATALOG_BUILD_TIMEOUT_MS` in the #12058 canonical-catalog regression so a cold catalog build racing the 8s default bound no longer flakes the suite with a 500 ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **fix(ci):** Allow `@omniroute/opencode-plugin-v2/` in the npm pack policy — [#12870](https://github.com/diegosouzapw/OmniRoute/pull/12870) shipped the v2 plugin beside its v1 sibling without widening the allowlist, so every packed file under it read as an unexpected artifact and failed `check:pack-policy` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **fix(ci):** Point the release-green validator's pack gate at the tree it is validating — the build-provenance guard defaulted to `origin/main`, which a release branch tip can never be an ancestor of mid-cycle, so `Package artifact` reported a false red on every run and the tarball boot-smoke cascaded off it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **docs(checks):** keep doc counts honest — headings, rankings, catalog, weights, quality gate and scoring diagram now covered ([#12507](https://github.com/diegosouzapw/OmniRoute/pull/12507)) — thanks @maxmad64bis
|
||||
- **chore(lifecycle):** `check:model-lifecycle` now also diffs `DEFAULT_DEGRADATION_MAP` (the background-task redirect table) against the vendor lifecycle snapshot, refusing a retired id as source or target, with a table-driven unit test beside it. Three rows whose source the vendor had retired — `claude-sonnet-4-20250514`, `gemini-3-pro-preview` and `gpt-5.1-codex` (whose target `gpt-5.1-codex-mini` is retired too) — were dead code, since `checkLifecycle` answers 410 before the redirect runs; they are dropped (#12535 — thanks @pacocartones)
|
||||
- **test(compression):** cover idle worker eviction at the resource level — the pool must call `terminate()` and must not retain the worker's `MessagePort`, complementing the `exit`-event assertion added with the fix ([#13371](https://github.com/diegosouzapw/OmniRoute/pull/13371))
|
||||
- **chore(quality):** raise the `imageGeneration.ts` file-size ceiling for the image-only-model guard that clears the #12945 base-red ([#13392](https://github.com/diegosouzapw/OmniRoute/pull/13392))
|
||||
- **chore(quality):** type the combo-test route's JSON response bodies instead of casting them to `any`, drop the now-empty suppression entry, and remove the `hasPerModelQuota` import `chatCore.ts` stopped using when the failure-classification helper was extracted ([#13379](https://github.com/diegosouzapw/OmniRoute/pull/13379))
|
||||
- **chore(electron):** the desktop bundle no longer ships the root-level files of every translated docs mirror (`README.md`, `llm.txt`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`) — the packaged app only reads `docs/i18n/<locale>/docs/**`, which stays. Saves ~11 MB on top of the translated CHANGELOGs already pruned. (#0000) ([#13235](https://github.com/diegosouzapw/OmniRoute/pull/13235))
|
||||
- **chore(quality):** retighten the `src/sse/handlers/chat.ts` unused-vars suppression count to the 8 violations that actually remain ([#13385](https://github.com/diegosouzapw/OmniRoute/pull/13385))
|
||||
- Bring the documented counts back in line with the code: 171 migrations (was 169) and 20 routing strategies (was 19), plus the regenerated `cli-tunnel` skill reference. ([#12970](https://github.com/diegosouzapw/OmniRoute/pull/12970))
|
||||
- Update the documented migration count to 172 after the call-logs provider-stats indexes landed. ([#13209](https://github.com/diegosouzapw/OmniRoute/pull/13209))
|
||||
- **chore(ci):** two gates that close the blind spots behind the exclusions above. `check:vitest-exclusions` requires every Vitest exclusion to name a tracking issue and to appear in `config/quality/vitest-exclusions.json` — the previous list grew to 62 files behind a comment pointing at an issue that had been closed for a month. `check-new-key-coverage` requires a key newly added to `en.json` to reach every locale; the existing coverage gate is a percentage floor per locale, so eleven absent keys out of ~13,000 left it at 99.9% while a whole feature shipped untranslated in nine languages. Both are diff-aware, so pre-existing debt stays frozen and neither needed a migration to turn on. ([#13233](https://github.com/diegosouzapw/OmniRoute/pull/13233))
|
||||
- **build:** root layout no longer loads Inter from `next/font/google`, so a production image build does not need fonts.googleapis.com ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) — thanks @HouMinXi
|
||||
- **docs(i18n):** bring the eight new `llm.txt` mirrors (km, kn, ml, my, ne, or, pa, si) up to the current provider and migration counts, so `check-docs-sync` stops rejecting every commit ([#13674](https://github.com/diegosouzapw/OmniRoute/pull/13674))
|
||||
- **refactor(ui):** flow surfaces (home topology, combo live studio, compression cockpit/waterfall/nodes, token-health badge) express state through the theme-aware `--orch-status-*` tokens instead of fixed dark-mode hex, so green/red/amber/grey stay legible in the light theme; dark mode is byte-identical. Categorical palettes (routing-strategy hues, compression-layer pills, provider brand colors) deliberately stay hex ([#12378](https://github.com/diegosouzapw/OmniRoute/issues/12378)) ([#13203](https://github.com/diegosouzapw/OmniRoute/pull/13203))
|
||||
- **chore(tests):** 51 test files that had been excluded from Vitest are running again, restoring roughly 350 assertions to the blocking suite. Each was measured individually first: of the 62 files parked behind the `// #8618 — pre-existing failure` comment, 51 pass against the current tree with no source change, so the exclusions had outlived the failures they were added for. The 11 that genuinely still fail stay excluded, but now point at a live tracker (#13204) rather than at #8618, which was closed in August while the list it tracked kept growing. ([#13233](https://github.com/diegosouzapw/OmniRoute/pull/13233))
|
||||
- **chore(quality):** drop two ESLint suppression entries whose violations no longer exist, so `eslint --suppressions-location` stops rejecting every commit that touches the surrounding files ([#13384](https://github.com/diegosouzapw/OmniRoute/pull/13384))
|
||||
- **chore(release):** bring main's post-tag fixes into release/v3.8.51 — sync-back step 2/2 (direct commit `21c488f210`)
|
||||
- **chore(deps):** patch toml and esbuild in the opencode plugin lockfiles ([#12965](https://github.com/diegosouzapw/OmniRoute/pull/12965))
|
||||
- **docs(changelog):** reconcile the v3.8.51 living section — fold 366 fragments, cover every cycle commit, credit every contributor ([#12971](https://github.com/diegosouzapw/OmniRoute/pull/12971))
|
||||
- **test(plugin):** wait for the v2 lazy-refresh tier instead of sleeping past it ([#12984](https://github.com/diegosouzapw/OmniRoute/pull/12984))
|
||||
- **chore(quality):** rebaseline chatCore.ts after the non-streaming regression fixes ([#13045](https://github.com/diegosouzapw/OmniRoute/pull/13045))
|
||||
- **docs(changelog):** add the missing bullet marker to the DeepSeek PoW fragment ([#13200](https://github.com/diegosouzapw/OmniRoute/pull/13200))
|
||||
- **chore(deps):** drain the Dependabot queue — 10 of 13 alerts ([#13213](https://github.com/diegosouzapw/OmniRoute/pull/13213))
|
||||
- **chore(ci):** make worktrees carry the hooks that enforce the identity gate ([#13236](https://github.com/diegosouzapw/OmniRoute/pull/13236))
|
||||
- **chore(deps):** pin csv-parse to ^7.0.2 via overrides (#13117) ([#13300](https://github.com/diegosouzapw/OmniRoute/pull/13300)) — thanks @KooshaPari
|
||||
- **chore(deps):** bump hono from ^4.12.34 to ^4.13.7 (#13148) ([#13301](https://github.com/diegosouzapw/OmniRoute/pull/13301)) — thanks @KooshaPari
|
||||
- **chore(deps):** pin joi to ^18.2.8 via overrides (#13085) ([#13302](https://github.com/diegosouzapw/OmniRoute/pull/13302)) — thanks @KooshaPari
|
||||
- **docs(providers):** add Microsoft 365 Copilot (BizChat) provider guide (#12779) ([#13340](https://github.com/diegosouzapw/OmniRoute/pull/13340)) — thanks @KooshaPari
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
@@ -866,6 +1100,7 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@amaleta](https://github.com/amaleta) | #11906, #11934 |
|
||||
| [@amartinawi](https://github.com/amartinawi) | #11575 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #11083, #11830, #11834, #11844 |
|
||||
| [@anhtahaylove](https://github.com/anhtahaylove) | #13091, #13092, #13093, #13096, #13097, #13100, #13106, #13109, #13114, #13155, #13166, #13171, #13175, #13187, #13196 |
|
||||
| [@aniruddhaadak80](https://github.com/aniruddhaadak80) | #11760 |
|
||||
| [@app](https://github.com/app) | #12554 |
|
||||
| [@arminanton](https://github.com/arminanton) | #11461, #11513 |
|
||||
@@ -895,10 +1130,10 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@gonisulaimann](https://github.com/gonisulaimann) | #12368, #12369, #12371 |
|
||||
| [@Gorillaz322](https://github.com/Gorillaz322) | #12207 |
|
||||
| [@groovecityJO](https://github.com/groovecityJO) | #12682 |
|
||||
| [@hartmark](https://github.com/hartmark) | #11434, #11452, #11473, #11499, #11703, #11983, #11984, #11985, #11986, #11988, #11989, #11990, #11994, #12221, #12293, #12445, #12446, #12447, #12448, #12460, #12461, #12623, #12646, #12650, #12680 |
|
||||
| [@hartmark](https://github.com/hartmark) | #11434, #11452, #11473, #11499, #11703, #11983, #11984, #11985, #11986, #11988, #11989, #11990, #11994, #12221, #12293, #12445, #12446, #12447, #12448, #12460, #12461, #12623, #12646, #12650, #12680, #12717, #12718, #12727, #12741, #12854 |
|
||||
| [@hizzt](https://github.com/hizzt) | #11894 |
|
||||
| [@hongnoul](https://github.com/hongnoul) | #11484 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #11411, #11414, #11512, #11518, #11520, #11641, #11642, #11643, #11687, #11779, #11849, #11850, #11851, #11915, #11916, #11918, #11919, #11920, #12013, #12017, #12033, #12042, #12106, #12139, #12166, #12169, #12171, #12205, #12213, #12312, #12325, #12487, #12488, #12495, #12504, #12557, #12566, #12590, #12591, #12624, #12626, #12632, #12637, #12678, #12696, #12697, #12711, #12733, #12746, #12767, #12770, #12789, #12803, #12805, #12811, #12866, #12868, #12899, #12926, #12934 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #11411, #11414, #11512, #11518, #11520, #11641, #11642, #11643, #11687, #11779, #11849, #11850, #11851, #11915, #11916, #11918, #11919, #11920, #12013, #12017, #12033, #12042, #12106, #12139, #12166, #12169, #12171, #12205, #12213, #12312, #12325, #12487, #12488, #12495, #12504, #12557, #12566, #12590, #12591, #12624, #12626, #12632, #12637, #12678, #12696, #12697, #12711, #12733, #12746, #12767, #12770, #12789, #12803, #12805, #12811, #12866, #12868, #12899, #12926, #12934, #12950, #12951, #12974, #13001, #13006, #13011, #13017, #13026, #13027, #13034, #13035, #13038, #13042, #13050, #13060, #13061, #13069, #13107, #13120, #13136, #13195, #13197 |
|
||||
| [@Hsia97](https://github.com/Hsia97) | #11624 |
|
||||
| [@jacobsparts](https://github.com/jacobsparts) | #11854, #12155, #12167 |
|
||||
| [@joglomedia](https://github.com/joglomedia) | #11980 |
|
||||
@@ -908,11 +1143,11 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@KaspaPulse](https://github.com/KaspaPulse) | #11389, #11469, #11628, #11666, #11910 |
|
||||
| [@keeltrace](https://github.com/keeltrace) | #12080, #12223 |
|
||||
| [@killer30001000](https://github.com/killer30001000) | #12178 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #12470, #12592, #12667, #12699, #12703, #12706, #12764, #12769, #12771 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #12470, #12592, #12667, #12699, #12703, #12706, #12764, #12769, #12771, #13300, #13301, #13302, #13340, #13401, #13403, #13404, #13405, #13406, #13407, #13409, #13410, #13411, #13412, #13413, #13414, #13418, #13424, #13427, #13433, #13523, #13524, #13525, #13528, #13531, #13532, #13534, #13539, #13543, #13545, #13546, #13547, #13550, #13553 |
|
||||
| [@kriptoburak](https://github.com/kriptoburak) | #11370 |
|
||||
| [@KrzysiekSko](https://github.com/KrzysiekSko) | #12425, #12432, #12673 |
|
||||
| [@marcelokarval](https://github.com/marcelokarval) | direct commit / report |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #1622, #11435, #11437, #11441, #11537, #11550, #11553, #11555, #11557, #11812, #11842, #11843, #11903, #12151, #12214, #12215, #12218, #12226, #12314, #12316, #12317, #12318, #12319, #12320, #12321, #12870 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #1622, #11435, #11437, #11441, #11537, #11550, #11553, #11555, #11557, #11812, #11842, #11843, #11903, #12151, #12214, #12215, #12218, #12226, #12314, #12316, #12317, #12318, #12319, #12320, #12321, #12507, #12715, #12731, #12744, #12786, #12787, #12788, #12790, #12792, #12794, #12795, #12828, #12832, #12853, #12857, #12870, #12937, #12941, #12975, #13141, #13142, #13146, #13147 |
|
||||
| [@mdigitalbh81](https://github.com/mdigitalbh81) | #12240 |
|
||||
| [@Meet6338-X](https://github.com/Meet6338-X) | #11598, #11609, #12110 |
|
||||
| [@morpheus9393](https://github.com/morpheus9393) | #11943 |
|
||||
@@ -921,11 +1156,11 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@NightStalker-87](https://github.com/NightStalker-87) | #12183 |
|
||||
| [@NoxzRCW](https://github.com/NoxzRCW) | #11879, #11880, #11881, #11882, #11883, #11935 |
|
||||
| [@ntdat812](https://github.com/ntdat812) | #11585, #12095 |
|
||||
| [@ntdatt812](https://github.com/ntdatt812) | #11368, #11573, #11574, #11576, #11577, #11580, #11582, #11583, #11588, #11589, #11590, #11591, #11592, #11593, #11672, #12177, #12180 |
|
||||
| [@ntdatt812](https://github.com/ntdatt812) | #11368, #11573, #11574, #11576, #11577, #11580, #11582, #11583, #11588, #11589, #11590, #11591, #11592, #11593, #11672, #12177, #12180, #12873, #12918, #12920, #12921, #12925, #12930, #13007, #13009, #13024, #13025, #13083, #13087, #13101, #13104, #13110 |
|
||||
| [@official-burak](https://github.com/official-burak) | #11542 |
|
||||
| [@opensource-elearning](https://github.com/opensource-elearning) | #12179, #12189, #12278, #12286 |
|
||||
| [@oyi77](https://github.com/oyi77) | #11408, #11409, #11421, #11505, #11677, #12036, #12110 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #11521, #11522, #11527, #11528, #11529, #11530, #11531, #11532, #11533, #11534, #11595, #11599, #11603, #11604, #11605, #11607, #11610, #11676, #11714, #11716, #11718, #11767, #11838, #11860, #11862, #11869, #11871, #11872, #11873, #11903, #11906, #11921, #11934, #12359, #12360, #12361, #12362, #12364, #12365, #12373, #12374, #12375, #12376, #12377, #12379, #12380, #12381, #12386, #12387, #12389, #12390, #12394, #12395, #12397, #12401, #12403, #12404 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #11521, #11522, #11527, #11528, #11529, #11530, #11531, #11532, #11533, #11534, #11595, #11599, #11603, #11604, #11605, #11607, #11610, #11676, #11714, #11716, #11718, #11767, #11838, #11860, #11862, #11869, #11871, #11872, #11873, #11903, #11906, #11921, #11934, #12356, #12358, #12359, #12360, #12361, #12362, #12364, #12365, #12373, #12374, #12375, #12376, #12377, #12379, #12380, #12381, #12386, #12387, #12389, #12390, #12394, #12395, #12397, #12401, #12403, #12404, #12522, #12523, #12535, #12536, #12540, #12541, #12543, #12545, #12548, #12549, #12550, #12551, #12552, #12647, #12651, #12653, #13055 |
|
||||
| [@patrykkopycinski](https://github.com/patrykkopycinski) | #11936, #11937, #12224 |
|
||||
| [@PauloFH](https://github.com/PauloFH) | #11509 |
|
||||
| [@PauloHSOliveira](https://github.com/PauloHSOliveira) | #12241 |
|
||||
@@ -937,7 +1172,7 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@raheemuddin786](https://github.com/raheemuddin786) | #11491, #11839, #11840, #11841, #12003, #12230, #12231, #12232, #12233, #12234 |
|
||||
| [@rahilmavani](https://github.com/rahilmavani) | #11761 |
|
||||
| [@Rahulsharma0810](https://github.com/Rahulsharma0810) | #11771 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #11710, #11727, #11797, #11798, #11802, #11805, #11806, #11811, #12098, #12099, #12101, #12449, #12452, #12472, #12473, #12493, #12533, #12607, #12628, #12631, #12636 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #11710, #11727, #11797, #11798, #11802, #11805, #11806, #11811, #12098, #12099, #12101, #12449, #12452, #12472, #12473, #12493, #12533, #12607, #12628, #12631, #12636, #12875, #12876, #12880, #12882, #12884 |
|
||||
| [@rezjalibd](https://github.com/rezjalibd) | #12186 |
|
||||
| [@RhianB14](https://github.com/RhianB14) | #11692 |
|
||||
| [@rifqiawl](https://github.com/rifqiawl) | #11517, #11519, #11969 |
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -227,6 +227,18 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
|
||||
ENV DATA_DIR=/app/data
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# #13679: default the PUBLISHED image to requiring an API key. A bare
|
||||
# `docker run -p 20128:20128 … diegosouzapw/omniroute` (README/QUICK-START
|
||||
# one-liners) does not pass `--env-file .env`, so without this default the
|
||||
# anonymous /v1 LLM proxy would be both keyless AND world-reachable on the
|
||||
# published container. This does NOT change the npm/CLI local-dev default
|
||||
# (`REQUIRE_API_KEY` stays `"false"` in featureFlagDefinitions.ts) — only the
|
||||
# shipped deployment artifact's posture. docker-compose.yml is unaffected: it
|
||||
# loads the operator's own `.env` (env_file:) which overrides this ENV, and
|
||||
# already binds loopback-only by default (#12568). Override with
|
||||
# `-e REQUIRE_API_KEY=false` for an intentionally keyless deployment.
|
||||
ENV REQUIRE_API_KEY=true
|
||||
|
||||
# `npm run build` (build-next-isolated → assembleStandalone) bundles ALL runtime
|
||||
# files into .build/next/standalone/ — .next, node_modules, migrations, scripts,
|
||||
# docs, and the previously hand-COPY'd modules below (@swc/helpers, pino-*, split2,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
||||
FROM oven/bun:1.4.0-slim AS base
|
||||
FROM oven/bun:1.4.2-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
@@ -56,7 +56,7 @@ ENV NODE_ENV=production
|
||||
RUN bun run --quiet build
|
||||
|
||||
# ── Runner Base stage (100% Bun Native Production Runtime) ──────────────────
|
||||
FROM oven/bun:1.4.0-slim AS runner-base
|
||||
FROM oven/bun:1.4.2-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)" \
|
||||
|
||||
16
README.md
16
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 → 359 providers — 150+ 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. 359 AI providers · 150+ free tiers · ~1.62B 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 → 360 providers — 150+ 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. 360 AI providers · 150+ free tiers · ~1.62B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **482 free-tier entries across 35 recurring pool keys** and computes the token headline from the **17 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **491 free-tier entries across 35 recurring pool keys** and computes the token headline from the **17 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 35 documented recurring pool keys covering 482 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, xKiro 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 35 documented recurring pool keys covering 491 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, xKiro 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
|
||||
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
|
||||
>
|
||||
@@ -233,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 360 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 360 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 54 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers 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 and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 360 providers, 150+ free tiers 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 and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -672,7 +672,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
</div>
|
||||
|
||||
> **357 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **482 per-model rows**, **35 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
> **357 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **491 per-model rows**, **35 recurring pools** and **54 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1268,7 +1268,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<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) — 122 domain modules, 175 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 176 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>
|
||||
@@ -1331,7 +1331,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>16-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 35 documented recurring pools / 482 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 35 documented recurring pools / 491 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -79,7 +79,8 @@ export async function runChatCommand(promptArg, opts, cmd) {
|
||||
const data = await response.json();
|
||||
const text = extractText(data, opts.responsesApi);
|
||||
|
||||
if (!opts.noHistory) {
|
||||
// Commander stores `--no-history` as `history === false`, never as `noHistory`.
|
||||
if (opts.history !== false && opts.noHistory !== true) {
|
||||
appendHistory({ prompt, model: opts.model, latencyMs, usage: data.usage, response: text });
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,9 @@ export function registerContexts(program) {
|
||||
.option("--no-secrets", "Omit API keys from export")
|
||||
.action(async (opts, cmd) => {
|
||||
const cfg = loadContexts();
|
||||
const out = opts.noSecrets ? redactContextSecrets(cfg) : JSON.parse(JSON.stringify(cfg));
|
||||
// Commander stores `--no-secrets` as `secrets === false`, never as `noSecrets`.
|
||||
const redact = opts.secrets === false || opts.noSecrets === true;
|
||||
const out = redact ? redactContextSecrets(cfg) : JSON.parse(JSON.stringify(cfg));
|
||||
const json = JSON.stringify(out, null, 2);
|
||||
if (opts.out) {
|
||||
const { writeFileSync } = await import("node:fs");
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
|
||||
import { printHeading } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
|
||||
import { getCrashLogPath } from "../runtime/processSupervisor.mjs";
|
||||
|
||||
const STATIC_SALT = "omniroute-field-encryption-v1";
|
||||
const KEY_LENGTH = 32;
|
||||
@@ -380,6 +381,33 @@ function checkMemory() {
|
||||
});
|
||||
}
|
||||
|
||||
// #13538: surfaces the supervisor's give-up crash record (persisted by
|
||||
// ServerSupervisor.persistCrashLog(), bin/cli/runtime/processSupervisor.mjs)
|
||||
// so a user whose `--tray` worker died silently (detached, stdio:"ignore")
|
||||
// has something concrete `doctor` can point at without needing `--log`.
|
||||
function checkCrashLog() {
|
||||
const crashLogPath = getCrashLogPath();
|
||||
if (!fs.existsSync(crashLogPath)) {
|
||||
return ok("Crash log", "No supervisor crash record found", { crashLogPath });
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(crashLogPath);
|
||||
const contents = fs.readFileSync(crashLogPath, "utf8");
|
||||
const lastEntry = contents.split("\n").filter(Boolean).slice(-6).join("\n");
|
||||
return warn(
|
||||
"Crash log",
|
||||
`Supervisor recorded a give-up crash at ${crashLogPath} (last modified ${stat.mtime.toISOString()})`,
|
||||
{ crashLogPath, modifiedAt: stat.mtime.toISOString(), tail: lastEntry }
|
||||
);
|
||||
} catch (error) {
|
||||
return warn("Crash log", `Crash record exists at ${crashLogPath} but could not be read`, {
|
||||
crashLogPath,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
||||
@@ -579,6 +607,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
checks.push(await checkNodeRuntime(rootDir));
|
||||
checks.push(await checkNativeBinary(rootDir));
|
||||
checks.push(checkMemory());
|
||||
checks.push(checkCrashLog());
|
||||
|
||||
if (!options.skipLiveness) {
|
||||
checks.push(await checkServerLiveness(options));
|
||||
|
||||
@@ -9,6 +9,8 @@ function truncate(v, len = 60) {
|
||||
return s.length > len ? s.slice(0, len - 1) + "…" : s;
|
||||
}
|
||||
|
||||
const VALID_MCP_TRANSPORTS = ["stdio", "sse", "streamable-http"];
|
||||
|
||||
const mcpToolSchema = [
|
||||
{ key: "name", header: "Tool", width: 36 },
|
||||
{
|
||||
@@ -43,6 +45,25 @@ export function registerMcp(program) {
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("enable")
|
||||
.description(t("mcp.enable.description"))
|
||||
.option("--transport <transport>", t("mcp.enable.transport"))
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runMcpEnableCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("disable")
|
||||
.description(t("mcp.disable.description"))
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runMcpDisableCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
// 5.1 — mcp call + mcp scopes
|
||||
mcp
|
||||
.command("call <tool> [argsJson]")
|
||||
@@ -61,10 +82,15 @@ export function registerMcp(program) {
|
||||
? JSON.parse(argsPositional)
|
||||
: {};
|
||||
|
||||
const exitCode = await runMcpCallCommand(tool, args, {
|
||||
...opts,
|
||||
stream: opts.stream,
|
||||
}, globalOpts);
|
||||
const exitCode = await runMcpCallCommand(
|
||||
tool,
|
||||
args,
|
||||
{
|
||||
...opts,
|
||||
stream: opts.stream,
|
||||
},
|
||||
globalOpts
|
||||
);
|
||||
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
@@ -127,7 +153,9 @@ async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } =
|
||||
|
||||
if (!initRes.ok) {
|
||||
const text = await initRes.text().catch(() => "");
|
||||
process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n`);
|
||||
process.stderr.write(
|
||||
`MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -227,6 +255,7 @@ export async function runMcpStatusCommand(opts = {}) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.log(t("mcp.stopped"));
|
||||
console.log(t("mcp.stoppedHint"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -240,6 +269,9 @@ export async function runMcpStatusCommand(opts = {}) {
|
||||
const transport = status.transport || "stdio";
|
||||
const online = status.online ?? status.running;
|
||||
console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped"));
|
||||
if (!online && status.enabled === false) {
|
||||
console.log(t("mcp.stoppedHint"));
|
||||
}
|
||||
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
|
||||
if (status.scopes?.length) {
|
||||
console.log(" Scopes:");
|
||||
@@ -270,10 +302,76 @@ export async function runMcpRestartCommand(opts = {}) {
|
||||
console.log(t("mcp.restarted"));
|
||||
return 0;
|
||||
}
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
const body = await res.json().catch(() => null);
|
||||
const message = body?.error || `HTTP ${res.status}`;
|
||||
console.error(t("common.error", { message }));
|
||||
return 1;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMcpEnableCommand(opts = {}) {
|
||||
const serverUp = await isServerUp();
|
||||
if (!serverUp) {
|
||||
console.error(t("common.serverOffline"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.transport && !VALID_MCP_TRANSPORTS.includes(opts.transport)) {
|
||||
console.error(
|
||||
t("common.error", {
|
||||
message: `Invalid transport '${opts.transport}'. Valid: ${VALID_MCP_TRANSPORTS.join(", ")}`,
|
||||
})
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = { mcpEnabled: true };
|
||||
if (opts.transport) body.mcpTransport = opts.transport;
|
||||
|
||||
const res = await apiFetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
body,
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(t("mcp.enabled"));
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMcpDisableCommand(opts = {}) {
|
||||
const serverUp = await isServerUp();
|
||||
if (!serverUp) {
|
||||
console.error(t("common.serverOffline"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiFetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
body: { mcpEnabled: false },
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(t("mcp.disabled"));
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ export function registerRestart(program) {
|
||||
program
|
||||
.command("restart")
|
||||
.description(t("restart.description"))
|
||||
.option("--port <port>", t("serve.port"), "20128")
|
||||
// No Commander default: runServe() falls back to PORT, then 20128 (#7049).
|
||||
.option("--port <port>", t("serve.port"))
|
||||
.action(async (opts) => {
|
||||
const exitCode = await runRestartCommand(opts);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
|
||||
@@ -281,7 +281,8 @@ export async function runServe(opts = {}) {
|
||||
return runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort);
|
||||
}
|
||||
|
||||
if (opts.noRecovery) {
|
||||
// Commander stores `--no-recovery` as `recovery === false`, never as `noRecovery`.
|
||||
if (opts.recovery === false || opts.noRecovery === true) {
|
||||
return runWithoutRecovery(
|
||||
serverJs,
|
||||
env,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"opencode": "ከOpenCode ጋር የተካተተውን @omniroute/opencode-plugin ጫን እና አዋቅር"
|
||||
},
|
||||
"doctor": {
|
||||
"title": "OmniRoute Doctor",
|
||||
"title": "OmniRoute ዶክተር",
|
||||
"dbOk": "የውሂብ ጎታ፦ ደህና ({path})",
|
||||
"dbMissing": "የውሂብ ጎታ፦ አልተጀመረም — `omniroute setup` ያስኪዱ",
|
||||
"portOk": "ወደብ {port}፦ ይገኛል",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ከመተው በፊት በ30s ውስጥ የሚፈቀደው ከፍተኛ የብልሽት ዳግም መጀመር ብዛት (ነባሪ፦ 2)",
|
||||
"tray": "በስርዓት ትሪ ውስጥ አስጀምር (ለዴስክቶፕ ብቻ፣ በፈቃድ)",
|
||||
"no_tray": "የስርዓት ትሪ አዶን አሰናክል",
|
||||
"ready_timeout": "የዝግጁነት ምርመራ ጊዜ ማብቂያ በሚሊሰከንድ (እንዲሁም OMNIROUTE_READY_TIMEOUT_MS፣ ነባሪ 60000)",
|
||||
"tls_cert": "HTTPSን ለማቅረብ የTLS ሰርተፍኬት (PEM) ዱካ (OMNIROUTE_TLS_CERTም ጭምር)",
|
||||
"tls_key": "HTTPSን ለማቅረብ የTLS የግል ቁልፍ (PEM) ዱካ (OMNIROUTE_TLS_KEYም ጭምር)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP ሰርቨር እየሰራ ነው ({transport})",
|
||||
"stopped": "MCP ሰርቨር ቆሟል።",
|
||||
"restarted": "MCP ሰርቨር እንደገና ተጀምሯል።",
|
||||
"stoppedHint": "ለማብራት `omniroute mcp enable`ን ያሂዱ።",
|
||||
"enabled": "MCP አገልጋይ ነቅቷል።",
|
||||
"disabled": "MCP አገልጋይ ተሰናክሏል።",
|
||||
"enable": {
|
||||
"description": "MCP አገልጋዩን አንቃ",
|
||||
"transport": "ጥቅም ላይ የሚውል ማጓጓዣ፦ stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP አገልጋዩን አሰናክል"
|
||||
},
|
||||
"call": {
|
||||
"description": "የMCP መሣሪያን በቀጥታ ይጥሩ",
|
||||
"args": "የJSON ነጋሪ እሴቶች ኦብጀክት (በቦታው)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Μέγιστος αριθμός επανεκκινήσεων λόγω σφάλματος εντός 30 δευτερολέπτων πριν την οριστική διακοπή (προεπιλογή: 2)",
|
||||
"tray": "Εκκίνηση στην περιοχή ειδοποιήσεων (μόνο για επιτραπέζιους υπολογιστές, προαιρετικό)",
|
||||
"no_tray": "Απενεργοποίηση εικονιδίου περιοχής ειδοποιήσεων",
|
||||
"ready_timeout": "Χρονικό όριο λήξης του ελέγχου ετοιμότητας σε ms (επίσης OMNIROUTE_READY_TIMEOUT_MS, προεπιλογή 60000)",
|
||||
"tls_cert": "Διαδρομή προς πιστοποιητικό TLS (PEM) για εξυπηρέτηση HTTPS (επίσης OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Διαδρομή προς το ιδιωτικό κλειδί TLS (PEM) για εξυπηρέτηση HTTPS (επίσης OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Ο διακομιστής MCP εκτελείται ({transport})",
|
||||
"stopped": "Ο διακομιστής MCP διακόπηκε.",
|
||||
"restarted": "Ο διακομιστής MCP επανεκκινήθηκε.",
|
||||
"stoppedHint": "Εκτελέστε `omniroute mcp enable` για να το ενεργοποιήσετε.",
|
||||
"enabled": "Ο διακομιστής MCP ενεργοποιήθηκε.",
|
||||
"disabled": "Ο διακομιστής MCP απενεργοποιήθηκε.",
|
||||
"enable": {
|
||||
"description": "Ενεργοποίηση του διακομιστή MCP",
|
||||
"transport": "Μέθοδος μεταφοράς προς χρήση: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Απενεργοποίηση του διακομιστή MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Άμεση κλήση εργαλείου MCP",
|
||||
"args": "Αντικείμενο ορισμάτων JSON (ενσωματωμένο)",
|
||||
@@ -1093,7 +1104,7 @@
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"title": "Συνδυασμοί",
|
||||
"switched": "Ενεργό combo: {name}",
|
||||
"created": "Δημιουργήθηκε combo: {name}",
|
||||
"deleted": "Διαγράφηκε combo: {name}",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Max crash restarts within 30s before giving up (default: 2)",
|
||||
"tray": "Start in the system tray (desktop only, opt-in)",
|
||||
"no_tray": "Disable system tray icon",
|
||||
"ready_timeout": "Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)",
|
||||
"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)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP server running ({transport})",
|
||||
"stopped": "MCP server stopped.",
|
||||
"restarted": "MCP server restarted.",
|
||||
"stoppedHint": "Run `omniroute mcp enable` to turn it on.",
|
||||
"enabled": "MCP server enabled.",
|
||||
"disabled": "MCP server disabled.",
|
||||
"enable": {
|
||||
"description": "Enable the MCP server",
|
||||
"transport": "Transport to use: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Disable the MCP server"
|
||||
},
|
||||
"call": {
|
||||
"description": "Invoke an MCP tool directly",
|
||||
"args": "JSON arguments object (inline)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
"opencode": "Installi ja seadista OpenCode'i jaoks kaasas olev @omniroute/opencode-plugin"
|
||||
},
|
||||
"doctor": {
|
||||
"title": "OmniRoute Doctor",
|
||||
"title": "OmniRoute'i diagnostika",
|
||||
"dbOk": "Andmebaas: korras ({path})",
|
||||
"dbMissing": "Andmebaas: pole lähtestatud — käivita `omniroute setup`",
|
||||
"portOk": "Port {port}: saadaval",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Maksimaalne krahhijärgsete taaskäivituste arv 30 sekundi jooksul enne alla andmist (vaikimisi: 2)",
|
||||
"tray": "Käivita süsteemisalves (ainult töölaual, valikuline)",
|
||||
"no_tray": "Keela süsteemisalve ikoon",
|
||||
"ready_timeout": "Valmisolekukontrolli ajalõpp millisekundites (ka OMNIROUTE_READY_TIMEOUT_MS, vaikimisi 60000)",
|
||||
"tls_cert": "TLS-sertifikaadi (PEM) tee HTTPS-i teenindamiseks (ka OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "TLS privaatvõtme (PEM) tee HTTPS-i teenindamiseks (ka OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP server töötab ({transport})",
|
||||
"stopped": "MCP server peatatud.",
|
||||
"restarted": "MCP server taaskäivitatud.",
|
||||
"stoppedHint": "Selle sisselülitamiseks käivitage `omniroute mcp enable`.",
|
||||
"enabled": "MCP-server on lubatud.",
|
||||
"disabled": "MCP-server on keelatud.",
|
||||
"enable": {
|
||||
"description": "Luba MCP-server",
|
||||
"transport": "Kasutatav transport: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Keela MCP-server"
|
||||
},
|
||||
"call": {
|
||||
"description": "Käivita MCP tööriist otse",
|
||||
"args": "JSON argumentide objekt (otseselt sisestatud)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Uaslíon atosuithe titim laistigh de 30 sula bhfágann tú suas (réamhshocrú: 2)",
|
||||
"tray": "Tosaigh i mbosca córas (leicsitheoir amháin, roghnach)",
|
||||
"no_tray": "Díchumasaigh deilbhín bosca córas",
|
||||
"ready_timeout": "Teorainn ama an tseiceála ullmhachta i ms (OMNIROUTE_READY_TIMEOUT_MS freisin, réamhshocrú 60000)",
|
||||
"tls_cert": "Cosán go dtí deimhniú TLS (PEM) chun HTTPS a sheirbhísiú (freisin OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Cosán go dtí eochair phríobháideach TLS (PEM) chun HTTPS a sheirbhísiú (freisin OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Tá freastalaí MCP ag rith ({transport})",
|
||||
"stopped": "Tá freastalaí MCP stoptha.",
|
||||
"restarted": "Tá freastalaí MCP atosaíte.",
|
||||
"stoppedHint": "Rith `omniroute mcp enable` chun é a chur ar siúl.",
|
||||
"enabled": "Freastalaí MCP cumasaithe.",
|
||||
"disabled": "Freastalaí MCP díchumasaithe.",
|
||||
"enable": {
|
||||
"description": "Cumasaigh an freastalaí MCP",
|
||||
"transport": "Iompar le húsáid: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Díchumasaigh an freastalaí MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Glaoigh ar uirlis MCP go díreach",
|
||||
"args": "Réimse argóintí JSON (inlíne)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Matsakaicin sake farawa bayan rushewa cikin daƙiƙa 30 kafin a haƙura (tsoho: 2)",
|
||||
"tray": "Fara a tiren tsarin (na kwamfutar tebur kawai, sai an zaɓa)",
|
||||
"no_tray": "Kashe gunkin tiren tsarin",
|
||||
"ready_timeout": "Lokacin ƙarewar gwajin shiri a ms (har ila yau OMNIROUTE_READY_TIMEOUT_MS, tsoho 60000)",
|
||||
"tls_cert": "Hanyar zuwa takardar shaidar TLS (PEM) don samar da HTTPS (haka kuma OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Hanyar zuwa maɓallin sirri na TLS (PEM) don samar da HTTPS (haka kuma OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Sabar MCP tana aiki ({transport})",
|
||||
"stopped": "An dakatar da sabar MCP.",
|
||||
"restarted": "An sake kunna sabar MCP.",
|
||||
"stoppedHint": "Gudanar da `omniroute mcp enable` don kunna shi.",
|
||||
"enabled": "An kunna sabar MCP.",
|
||||
"disabled": "An kashe sabar MCP.",
|
||||
"enable": {
|
||||
"description": "Kunna sabar MCP",
|
||||
"transport": "Hanyar jigilar da za a yi amfani da ita: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Kashe sabar MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Kira kayan aikin MCP kai tsaye",
|
||||
"args": "Abun hujjojin JSON (a cikin layi)",
|
||||
@@ -475,7 +486,7 @@
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
"title": "Tunnels",
|
||||
"title": "Ramuka",
|
||||
"listDescription": "Jera tunnels masu aiki",
|
||||
"createDescription": "Ƙirƙiri tunnel",
|
||||
"created": "An ƙirƙiri tunnel: {url}",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Maksimalni broj ponovnih pokretanja nakon pada unutar 30 s prije odustajanja (zadano: 2)",
|
||||
"tray": "Pokretanje u programskoj traci (samo za stolna računala, po izboru)",
|
||||
"no_tray": "Onemogući ikonu programske trake",
|
||||
"ready_timeout": "Vremensko ograničenje provjere spremnosti u ms (također OMNIROUTE_READY_TIMEOUT_MS, zadano 60000)",
|
||||
"tls_cert": "Putanja do TLS certifikata (PEM) za posluživanje HTTPS-a (također OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Putanja do TLS privatnog ključa (PEM) za posluživanje HTTPS-a (također OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP poslužitelj je pokrenut ({transport})",
|
||||
"stopped": "MCP poslužitelj je zaustavljen.",
|
||||
"restarted": "MCP poslužitelj je ponovo pokrenut.",
|
||||
"stoppedHint": "Pokrenite `omniroute mcp enable` da biste ga uključili.",
|
||||
"enabled": "MCP poslužitelj omogućen.",
|
||||
"disabled": "MCP poslužitelj onemogućen.",
|
||||
"enable": {
|
||||
"description": "Omogući MCP poslužitelj",
|
||||
"transport": "Prijenos koji će se koristiti: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Onemogući MCP poslužitelj"
|
||||
},
|
||||
"call": {
|
||||
"description": "Izravno pozovi MCP alat",
|
||||
"args": "JSON objekt argumenata (unutarnji)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Վթարից հետո վերագործարկումների առավելագույն քանակը 30 վրկ-ի ընթացքում՝ մինչև փորձերը դադարեցնելը (լռելյայն՝ 2)",
|
||||
"tray": "Գործարկել համակարգային սկուտեղում (միայն աշխատասեղանի տարբերակում, ըստ ցանկության)",
|
||||
"no_tray": "Անջատել համակարգային սկուտեղի պատկերակը",
|
||||
"ready_timeout": "Պատրաստության ստուգման սպասաժամը՝ մվ-ով (նաև OMNIROUTE_READY_TIMEOUT_MS, լռելյայն՝ 60000)",
|
||||
"tls_cert": "HTTPS սպասարկելու համար TLS վկայագրի (PEM) ուղին (նաև՝ OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "HTTPS սպասարկելու համար TLS գաղտնի բանալու (PEM) ուղին (նաև՝ OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP սերվերը գործարկված է ({transport})",
|
||||
"stopped": "MCP սերվերը կանգնեցվել է։",
|
||||
"restarted": "MCP սերվերը վերագործարկվել է։",
|
||||
"stoppedHint": "Այն միացնելու համար գործարկեք `omniroute mcp enable` հրամանը։",
|
||||
"enabled": "MCP սերվերը միացված է։",
|
||||
"disabled": "MCP սերվերն անջատված է։",
|
||||
"enable": {
|
||||
"description": "Միացնել MCP սերվերը",
|
||||
"transport": "Օգտագործվող փոխադրման եղանակը՝ stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Անջատել MCP սերվերը"
|
||||
},
|
||||
"call": {
|
||||
"description": "Անմիջապես կանչել MCP գործիք",
|
||||
"args": "JSON արգումենտների օբյեկտ (ներտողային)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Ọnụọgụ mmalitegharị kachasị elu mgbe ọ dara n'ime 30s tupu ịkwụsị mbọ (ndabara: 2)",
|
||||
"tray": "Malite na tray sistemụ (naanị na desktọpụ, ma ọ bụrụ na ahọrọ ya)",
|
||||
"no_tray": "Gbanyụọ akara ngosi tray sistemụ",
|
||||
"ready_timeout": "Oge njedebe nyocha ịdị njikere na ms (yana OMNIROUTE_READY_TIMEOUT_MS, ndabara bụ 60000)",
|
||||
"tls_cert": "Ụzọ gaa na asambodo TLS (PEM) iji nye HTTPS (nakwa OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Ụzọ gaa na igodo nzuzo TLS (PEM) iji nye HTTPS (nakwa OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Sava MCP na-arụ ọrụ ({transport})",
|
||||
"stopped": "Sava MCP akwụsịla.",
|
||||
"restarted": "Amalitegharịrị sava MCP.",
|
||||
"stoppedHint": "Gbaa `omniroute mcp enable` iji gbanye ya.",
|
||||
"enabled": "Agbanyela sava MCP.",
|
||||
"disabled": "Agbanyụọla sava MCP.",
|
||||
"enable": {
|
||||
"description": "Gbanye sava MCP",
|
||||
"transport": "Usoro mbufe a ga-eji: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Gbanyụọ sava MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Kpọọ ngwa MCP ozugbo",
|
||||
"args": "Ihe arụmụka JSON (n'ime ahịrị)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "30 წამში ავარიული გათიშვის შემდეგ ხელახალი გაშვებების მაქსიმალური რაოდენობა დანებებამდე (ნაგულისხმევი: 2)",
|
||||
"tray": "სისტემურ უჯრაში გაშვება (მხოლოდ დესკტოპისთვის, სურვილისამებრ)",
|
||||
"no_tray": "სისტემური უჯრის ხატულის გამორთვა",
|
||||
"ready_timeout": "მზადყოფნის შემოწმების მოლოდინის დრო მილიწამებში (ასევე OMNIROUTE_READY_TIMEOUT_MS, ნაგულისხმევად 60000)",
|
||||
"tls_cert": "HTTPS-ისთვის TLS სერტიფიკატის (PEM) გზა (ასევე OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "HTTPS-ისთვის TLS პირადი გასაღების (PEM) გზა (ასევე OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP სერვერი მუშაობს ({transport})",
|
||||
"stopped": "MCP სერვერი გაჩერდა.",
|
||||
"restarted": "MCP სერვერი გადაიტვირთა.",
|
||||
"stoppedHint": "მის ჩასართავად გაუშვით `omniroute mcp enable`.",
|
||||
"enabled": "MCP სერვერი ჩართულია.",
|
||||
"disabled": "MCP სერვერი გამორთულია.",
|
||||
"enable": {
|
||||
"description": "MCP სერვერის ჩართვა",
|
||||
"transport": "გამოსაყენებელი ტრანსპორტი: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP სერვერის გამორთვა"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP ხელსაწყოს პირდაპირ გამოძახება",
|
||||
"args": "JSON არგუმენტების ობიექტი (ჩაშენებული)",
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
}
|
||||
},
|
||||
"keys": {
|
||||
"title": "API Keys",
|
||||
"title": "សោ API",
|
||||
"addDescription": "បន្ថែម ឬធ្វើបច្ចុប្បន្នភាព API key សម្រាប់អ្នកផ្តល់សេវា",
|
||||
"listDescription": "រាយ API keys ដែលបានកំណត់រចនាសម្ព័ន្ធទាំងអស់",
|
||||
"removeDescription": "លុប API key សម្រាប់អ្នកផ្តល់សេវា",
|
||||
@@ -226,7 +226,7 @@
|
||||
"description": "ផ្ញើ chat prompt មួយលើកទៅ OmniRoute",
|
||||
"file": "អាន prompt ពីឯកសារ",
|
||||
"stdin": "អាន prompt ពី stdin",
|
||||
"system": "System prompt",
|
||||
"system": "ប្រូមប្រព័ន្ធ",
|
||||
"model": "Model ID (លំនាំដើម៖ auto)",
|
||||
"max_tokens": "ចំនួន token អតិបរមាក្នុងការឆ្លើយតប",
|
||||
"temperature": "សីតុណ្ហភាព sampling (0–2)",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ចំនួនអតិបរមានៃការចាប់ផ្តើមឡើងវិញបន្ទាប់ពីគាំងក្នុងរយៈពេល 30s មុនពេលបោះបង់ (លំនាំដើម៖ 2)",
|
||||
"tray": "ចាប់ផ្ដើមក្នុងថាសប្រព័ន្ធ (សម្រាប់កុំព្យូទ័រលើតុប៉ុណ្ណោះ និងត្រូវជ្រើសរើសបើក)",
|
||||
"no_tray": "បិទរូបតំណាងថាសប្រព័ន្ធ",
|
||||
"ready_timeout": "រយៈពេលអស់ពេលនៃការត្រួតពិនិត្យភាពរួចរាល់ គិតជាមិល្លីវិនាទី (ក៏អាចប្រើ OMNIROUTE_READY_TIMEOUT_MS ផងដែរ, លំនាំដើម 60000)",
|
||||
"tls_cert": "ផ្លូវទៅកាន់វិញ្ញាបនបត្រ TLS (PEM) សម្រាប់បម្រើ HTTPS (ក៏ជា OMNIROUTE_TLS_CERT ផងដែរ)",
|
||||
"tls_key": "ផ្លូវទៅកាន់សោឯកជន TLS (PEM) សម្រាប់បម្រើ HTTPS (ក៏ជា OMNIROUTE_TLS_KEY ផងដែរ)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "ម៉ាស៊ីនមេ MCP កំពុងដំណើរការ ({transport})",
|
||||
"stopped": "ម៉ាស៊ីនមេ MCP បានឈប់ដំណើរការ។",
|
||||
"restarted": "ម៉ាស៊ីនមេ MCP បានចាប់ផ្តើមឡើងវិញ។",
|
||||
"stoppedHint": "ដំណើរការ `omniroute mcp enable` ដើម្បីបើកវា។",
|
||||
"enabled": "ម៉ាស៊ីនមេ MCP ត្រូវបានបើក។",
|
||||
"disabled": "ម៉ាស៊ីនមេ MCP ត្រូវបានបិទ។",
|
||||
"enable": {
|
||||
"description": "បើកម៉ាស៊ីនមេ MCP",
|
||||
"transport": "មធ្យោបាយបញ្ជូនដែលត្រូវប្រើ៖ stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "បិទម៉ាស៊ីនមេ MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "ហៅប្រើឧបករណ៍ MCP ដោយផ្ទាល់",
|
||||
"args": "អាប់ជិចអាគុយម៉ង់ JSON (ក្នុងបន្ទាត់)",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ಬಿಟ್ಟುಕೊಡುವ ಮೊದಲು 30s ಒಳಗೆ ಕ್ರ್ಯಾಶ್ನಿಂದ ಗರಿಷ್ಠ ಮರುಪ್ರಾರಂಭಗಳ ಸಂಖ್ಯೆ (ಡೀಫಾಲ್ಟ್: 2)",
|
||||
"tray": "ಸಿಸ್ಟಮ್ ಟ್ರೇಯಲ್ಲಿ ಪ್ರಾರಂಭಿಸಿ (ಡೆಸ್ಕ್ಟಾಪ್ ಮಾತ್ರ, ಆಯ್ಕೆ ಮಾಡಿದಲ್ಲಿ)",
|
||||
"no_tray": "ಸಿಸ್ಟಮ್ ಟ್ರೇ ಐಕಾನ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ",
|
||||
"ready_timeout": "ಸಿದ್ಧತಾ ಪ್ರೋಬ್ ಕಾಲಮಿತಿ ಮಿಲಿಸೆಕೆಂಡುಗಳಲ್ಲಿ (OMNIROUTE_READY_TIMEOUT_MS ಕೂಡ, ಡೀಫಾಲ್ಟ್ 60000)",
|
||||
"tls_cert": "HTTPS ಒದಗಿಸಲು TLS ಪ್ರಮಾಣಪತ್ರದ (PEM) ಪಥ (OMNIROUTE_TLS_CERT ಸಹ)",
|
||||
"tls_key": "HTTPS ಒದಗಿಸಲು TLS ಖಾಸಗಿ ಕೀಲಿಯ (PEM) ಪಥ (OMNIROUTE_TLS_KEY ಸಹ)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP ಸರ್ವರ್ ಚಾಲನೆಯಲ್ಲಿದೆ ({transport})",
|
||||
"stopped": "MCP ಸರ್ವರ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ.",
|
||||
"restarted": "MCP ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭಿಸಲಾಗಿದೆ.",
|
||||
"stoppedHint": "ಇದನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು `omniroute mcp enable` ಅನ್ನು ಚಲಾಯಿಸಿ.",
|
||||
"enabled": "MCP ಸರ್ವರ್ ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ.",
|
||||
"disabled": "MCP ಸರ್ವರ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ.",
|
||||
"enable": {
|
||||
"description": "MCP ಸರ್ವರ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ",
|
||||
"transport": "ಬಳಸಬೇಕಾದ ಟ್ರಾನ್ಸ್ಪೋರ್ಟ್: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP ಸರ್ವರ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP ಟೂಲ್ ಅನ್ನು ನೇರವಾಗಿ ಆಹ್ವಾನಿಸಿ",
|
||||
"args": "JSON ಆರ್ಗ್ಯುಮೆಂಟ್ಗಳ ಆಬ್ಜೆಕ್ಟ್ (ಇನ್ಲೈನ್)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Didžiausias paleidimų iš naujo po strigties skaičius per 30 s prieš nutraukiant bandymus (numatyta: 2)",
|
||||
"tray": "Paleisti sistemos dėkle (tik darbalaukio programoje, pasirenkama)",
|
||||
"no_tray": "Išjungti sistemos dėklo piktogramą",
|
||||
"ready_timeout": "Parengties patikros skirtasis laikas ms (taip pat OMNIROUTE_READY_TIMEOUT_MS, numatytoji reikšmė 60000)",
|
||||
"tls_cert": "Kelias į TLS sertifikatą (PEM), skirtą HTTPS teikti (taip pat OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Kelias į privatųjį TLS raktą (PEM), skirtą HTTPS teikti (taip pat OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP serveris veikia ({transport})",
|
||||
"stopped": "MCP serveris sustabdytas.",
|
||||
"restarted": "MCP serveris paleistas iš naujo.",
|
||||
"stoppedHint": "Norėdami jį įjungti, paleiskite `omniroute mcp enable`.",
|
||||
"enabled": "MCP serveris įjungtas.",
|
||||
"disabled": "MCP serveris išjungtas.",
|
||||
"enable": {
|
||||
"description": "Įjungti MCP serverį",
|
||||
"transport": "Naudotinas perdavimo būdas: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Išjungti MCP serverį"
|
||||
},
|
||||
"call": {
|
||||
"description": "Tiesiogiai iškviesti MCP įrankį",
|
||||
"args": "JSON argumentų objektas (įterptasis)",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Maksimālās avāriju restartēšanas 30 sekunžu laikā pirms padoties (noklusējums: 2)",
|
||||
"tray": "Sākt sistēmas tray (tikai darbvirsmas, izvēlēties)",
|
||||
"no_tray": "Atspējot sistēmas tray ikonu",
|
||||
"ready_timeout": "Gatavības pārbaudes taimauts milisekundēs (arī OMNIROUTE_READY_TIMEOUT_MS, noklusējuma vērtība 60000)",
|
||||
"tls_cert": "Ceļš uz TLS sertifikātu (PEM) HTTPS apkalpošanai (arī OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Ceļš uz TLS privāto atslēgu (PEM) HTTPS apkalpošanai (arī OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP serveris darbojas ({transport})",
|
||||
"stopped": "MCP serveris apturēts.",
|
||||
"restarted": "MCP serveris restartēts.",
|
||||
"stoppedHint": "Lai to ieslēgtu, palaidiet `omniroute mcp enable`.",
|
||||
"enabled": "MCP serveris ir iespējots.",
|
||||
"disabled": "MCP serveris ir atspējots.",
|
||||
"enable": {
|
||||
"description": "Iespējot MCP serveri",
|
||||
"transport": "Izmantojamais transports: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Atspējot MCP serveri"
|
||||
},
|
||||
"call": {
|
||||
"description": "Izsaukt MCP rīku tieši",
|
||||
"args": "JSON argumentu objekts (iekšējais)",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ഉപേക്ഷിക്കുന്നതിന് മുമ്പ് 30s-നുള്ളിൽ അനുവദിക്കുന്ന പരമാവധി ക്രാഷ് പുനരാരംഭങ്ങൾ (ഡിഫോൾട്ട്: 2)",
|
||||
"tray": "സിസ്റ്റം ട്രേയിൽ ആരംഭിക്കുക (ഡെസ്ക്ടോപ്പിൽ മാത്രം, സ്വമേധയാ തിരഞ്ഞെടുക്കാവുന്നത്)",
|
||||
"no_tray": "സിസ്റ്റം ട്രേ ഐക്കൺ പ്രവർത്തനരഹിതമാക്കുക",
|
||||
"ready_timeout": "റെഡിനസ് പ്രോബ് സമയപരിധി ms-ൽ (OMNIROUTE_READY_TIMEOUT_MS എന്നതും, സ്ഥിരസ്ഥിതി 60000)",
|
||||
"tls_cert": "HTTPS നൽകുന്നതിനുള്ള TLS സർട്ടിഫിക്കറ്റിന്റെ (PEM) പാത (OMNIROUTE_TLS_CERT-ലും)",
|
||||
"tls_key": "HTTPS നൽകുന്നതിനുള്ള TLS സ്വകാര്യ കീയുടെ (PEM) പാത (OMNIROUTE_TLS_KEY-ലും)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP സെർവർ പ്രവർത്തിക്കുന്നു ({transport})",
|
||||
"stopped": "MCP സെർവർ നിർത്തി.",
|
||||
"restarted": "MCP സെർവർ പുനരാരംഭിച്ചു.",
|
||||
"stoppedHint": "ഇത് പ്രവർത്തനക്ഷമമാക്കാൻ `omniroute mcp enable` പ്രവർത്തിപ്പിക്കുക.",
|
||||
"enabled": "MCP സെർവർ പ്രവർത്തനക്ഷമമാക്കി.",
|
||||
"disabled": "MCP സെർവർ പ്രവർത്തനരഹിതമാക്കി.",
|
||||
"enable": {
|
||||
"description": "MCP സെർവർ പ്രവർത്തനക്ഷമമാക്കുക",
|
||||
"transport": "ഉപയോഗിക്കേണ്ട ട്രാൻസ്പോർട്ട്: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP സെർവർ പ്രവർത്തനരഹിതമാക്കുക"
|
||||
},
|
||||
"call": {
|
||||
"description": "ഒരു MCP ടൂൾ നേരിട്ട് പ്രവർത്തിപ്പിക്കുക",
|
||||
"args": "JSON ആർഗ്യുമെന്റുകളുടെ ഒബ്ജക്റ്റ് (ഇൻലൈൻ)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "L-ogħla numru ta' restarts wara crash fi żmien 30s qabel ma jċedi (default: 2)",
|
||||
"tray": "Bda' fil-caffettar tal-pajjiż (biss desktop, għażla)",
|
||||
"no_tray": "Iddiżattiva l-ikona tal-caffettar tal-pajjiż",
|
||||
"ready_timeout": "Limitu taż-żmien tal-verifika tat-tħejjija f'ms (ukoll OMNIROUTE_READY_TIMEOUT_MS, valur predefinit 60000)",
|
||||
"tls_cert": "Triq għal ċertifikat TLS (PEM) biex isservi HTTPS (ukoll OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Triq għas-sieqa privata tal-TLS (PEM) biex isservi HTTPS (ukoll OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Servier MCP qed jaħdem ({transport})",
|
||||
"stopped": "Servier MCP waqaf.",
|
||||
"restarted": "Servier MCP restartat.",
|
||||
"stoppedHint": "Ħaddem `omniroute mcp enable` biex tattivah.",
|
||||
"enabled": "Is-server MCP huwa attivat.",
|
||||
"disabled": "Is-server MCP huwa diżattivat.",
|
||||
"enable": {
|
||||
"description": "Attiva s-server MCP",
|
||||
"transport": "Trasport li għandu jintuża: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Iddiżattiva s-server MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Sejjaħ għodda MCP direttament",
|
||||
"args": "Oġġett argomenti JSON (internament)",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"testFailed": "Provider စမ်းသပ်မှု မအောင်မြင်ပါ: {error}",
|
||||
"loginEnabled": "လော့ဂ်အင်: ဖွင့်ထားသည် (စကားဝှက်ကို အပ်ဒိတ်လုပ်ပြီး)",
|
||||
"loginDisabled": "လော့ဂ်အင်: ပိတ်ထားသည်",
|
||||
"providerInfo": "Provider: {info}",
|
||||
"providerInfo": "ပံ့ပိုးသူ: {info}",
|
||||
"opencode": "OpenCode အတွက် တွဲဖက်ပါရှိသော @omniroute/opencode-plugin ကို ထည့်သွင်းပြီး စီစဉ်သတ်မှတ်ရန်"
|
||||
},
|
||||
"doctor": {
|
||||
@@ -41,7 +41,7 @@
|
||||
"warnings": "သတိပေးချက် {count} ခု — အပေါ်တွင် ကြည့်ပါ။"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
"title": "ပံ့ပိုးသူများ",
|
||||
"noProviders": "မည်သည့် provider ကိုမျှ စီစဉ်သတ်မှတ်မထားပါ။ လုပ်ဆောင်ရန်: omniroute setup",
|
||||
"testing": "{name} ကို စမ်းသပ်နေသည်...",
|
||||
"available": "အသုံးပြုနိုင်သော provider {count} ခု",
|
||||
@@ -86,7 +86,7 @@
|
||||
}
|
||||
},
|
||||
"keys": {
|
||||
"title": "API Keys",
|
||||
"title": "API သော့များ",
|
||||
"addDescription": "ဝန်ဆောင်မှုပေးသူတစ်ခုအတွက် API key ကို ထည့်ရန် သို့မဟုတ် အပ်ဒိတ်လုပ်ရန်",
|
||||
"listDescription": "ပြင်ဆင်သတ်မှတ်ထားသော API key အားလုံးကို စာရင်းပြုစုရန်",
|
||||
"removeDescription": "ဝန်ဆောင်မှုပေးသူတစ်ခုအတွက် API key ကို ဖယ်ရှားရန်",
|
||||
@@ -152,7 +152,7 @@
|
||||
"file": "ဖိုင်မှ prompt ကို ဖတ်ပါ",
|
||||
"stdin": "stdin မှ prompt ကို ဖတ်ပါ",
|
||||
"model": "Model ID (မူလသတ်မှတ်ချက်: auto)",
|
||||
"system": "System prompt",
|
||||
"system": "စနစ်ညွှန်ကြားချက်",
|
||||
"combo": "သတ်မှတ်ထားသော combo တစ်ခုကို အမည်ဖြင့် မဖြစ်မနေ အသုံးပြုပါ",
|
||||
"max_tokens": "Response အတွင်း အများဆုံး token အရေအတွက်",
|
||||
"responses_api": "/v1/chat/completions အစား /v1/responses ကို အသုံးပြုပါ",
|
||||
@@ -226,7 +226,7 @@
|
||||
"description": "OmniRoute သို့ တစ်ကြိမ်တည်းသုံး chat prompt ပို့ရန်",
|
||||
"file": "ဖိုင်မှ prompt ကို ဖတ်ရန်",
|
||||
"stdin": "stdin မှ prompt ကို ဖတ်ရန်",
|
||||
"system": "System prompt",
|
||||
"system": "စနစ်ညွှန်ကြားချက်",
|
||||
"model": "Model ID (မူလသတ်မှတ်ချက်: auto)",
|
||||
"max_tokens": "တုံ့ပြန်ချက်တွင် ပါဝင်မည့် token အရေအတွက် အများဆုံး",
|
||||
"temperature": "နမူနာရွေးချယ်မှု temperature (0–2)",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "လက်လျှော့မတိုင်မီ 30s အတွင်း ပျက်သွား၍ ပြန်လည်စတင်နိုင်သည့် အများဆုံးအကြိမ်ရေ (မူလသတ်မှတ်ချက်: 2)",
|
||||
"tray": "စနစ် tray တွင် စတင်ပါ (desktop အတွက်သာ၊ ရွေးချယ်ဖွင့်သုံးရန်)",
|
||||
"no_tray": "စနစ် tray အိုင်ကွန်ကို ပိတ်ပါ",
|
||||
"ready_timeout": "အဆင်သင့်ဖြစ်မှု စစ်ဆေးချက်၏ အချိန်ကုန်ဆုံးကာလကို ms ဖြင့် သတ်မှတ်ပါ (OMNIROUTE_READY_TIMEOUT_MS လည်းဖြစ်ပြီး မူလတန်ဖိုးမှာ 60000 ဖြစ်သည်)",
|
||||
"tls_cert": "HTTPS ဖြင့် ဝန်ဆောင်မှုပေးရန် TLS certificate (PEM) သို့ လမ်းကြောင်း (OMNIROUTE_TLS_CERT လည်းဖြစ်သည်)",
|
||||
"tls_key": "HTTPS ဖြင့် ဝန်ဆောင်မှုပေးရန် TLS private key (PEM) သို့ လမ်းကြောင်း (OMNIROUTE_TLS_KEY လည်းဖြစ်သည်)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP ဆာဗာ လည်ပတ်နေသည် ({transport})",
|
||||
"stopped": "MCP ဆာဗာ ရပ်တန့်သွားပါပြီ။",
|
||||
"restarted": "MCP ဆာဗာကို ပြန်လည်စတင်ပြီးပါပြီ။",
|
||||
"stoppedHint": "၎င်းကို ဖွင့်ရန် `omniroute mcp enable` ကို လုပ်ဆောင်ပါ။",
|
||||
"enabled": "MCP ဆာဗာကို ဖွင့်ထားသည်။",
|
||||
"disabled": "MCP ဆာဗာကို ပိတ်ထားသည်။",
|
||||
"enable": {
|
||||
"description": "MCP ဆာဗာကို ဖွင့်ရန်",
|
||||
"transport": "အသုံးပြုမည့် ပို့ဆောင်မှုနည်းလမ်း: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP ဆာဗာကို ပိတ်ရန်"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP ကိရိယာတစ်ခုကို တိုက်ရိုက် ခေါ်ယူအသုံးပြုရန်",
|
||||
"args": "JSON အငြင်းပွားဖွယ်ရာ အရာဝတ္ထု (စာကြောင်းအတွင်း)",
|
||||
@@ -633,7 +644,7 @@
|
||||
"content": "Memory စာသားအကြောင်းအရာ",
|
||||
"file": "ဖိုင်မှ အကြောင်းအရာကို ဖတ်ရန်",
|
||||
"type": "Memory အမျိုးအစား (မူလသတ်မှတ်ချက်: user)",
|
||||
"metadata": "JSON metadata object",
|
||||
"metadata": "JSON မက်တာဒေတာ အရာဝတ္ထု",
|
||||
"api_key": "API key နှင့် ချိတ်ဆက်ရန်"
|
||||
},
|
||||
"clear": {
|
||||
@@ -667,7 +678,7 @@
|
||||
},
|
||||
"start": {
|
||||
"description": "provider တစ်ခုအတွက် OAuth ခွင့်ပြုချက် flow ကို စတင်ရန်",
|
||||
"provider": "Provider ID (gemini, copilot, cursor, …)",
|
||||
"provider": "ပံ့ပိုးသူ ID (gemini, copilot, cursor, …)",
|
||||
"no_browser": "URL ကိုသာ ဖော်ပြရန် — browser ကို မဖွင့်ပါနှင့်",
|
||||
"import_system": "စက်တွင်း system config မှ အထောက်အထားများကို အလိုအလျောက် ထည့်သွင်းရန်",
|
||||
"social": "Social login provider (google|github) — kiro အတွက် လိုအပ်သည်",
|
||||
@@ -704,7 +715,7 @@
|
||||
"prompt_file": "ဖိုင်မှ prompt ကို ဖတ်ရန်",
|
||||
"repo": "လုပ်ဆောင်စရာအတွက် clone ပြုလုပ်မည့် repository URL",
|
||||
"branch": "အသုံးပြုမည့် branch အမည်",
|
||||
"metadata": "JSON metadata object"
|
||||
"metadata": "JSON မက်တာဒေတာ အရာဝတ္ထု"
|
||||
},
|
||||
"list": {
|
||||
"description": "agent လုပ်ဆောင်စရာများကို စာရင်းပြုရန်",
|
||||
@@ -1268,7 +1279,7 @@
|
||||
"description": "LLM ဖြင့် အပြန်အလှန် ဆွေးနွေးမှုအကြိမ်များစွာ ပြုလုပ်နိုင်သော REPL",
|
||||
"model": "အသုံးပြုမည့် မော်ဒယ် (ပုံသေ- auto)",
|
||||
"combo": "အသုံးပြုမည့် Combo အမည်",
|
||||
"system": "System prompt",
|
||||
"system": "စနစ်ညွှန်ကြားချက်",
|
||||
"resume": "သိမ်းဆည်းထားသော session ကို အမည်ဖြင့် ပြန်လည်စတင်ရန်"
|
||||
},
|
||||
"plugin": {
|
||||
|
||||
@@ -226,11 +226,11 @@
|
||||
"description": "OmniRoute मा एकपटकको chat prompt पठाउनुहोस्",
|
||||
"file": "file बाट prompt पढ्नुहोस्",
|
||||
"stdin": "stdin बाट prompt पढ्नुहोस्",
|
||||
"system": "System prompt",
|
||||
"system": "प्रणाली प्रम्प्ट",
|
||||
"model": "Model ID (पूर्वनिर्धारित: auto)",
|
||||
"max_tokens": "response मा अधिकतम tokens",
|
||||
"temperature": "sampling temperature (0–2)",
|
||||
"top_p": "Top-p nucleus sampling",
|
||||
"top_p": "Top-p न्युक्लियस नमुना चयन",
|
||||
"reasoning_effort": "तर्क प्रयासको स्तर (low|medium|high)",
|
||||
"thinking_budget": "विस्तारित सोचाइको token बजेट",
|
||||
"combo": "नामद्वारा कुनै विशिष्ट combo प्रयोग गर्न बाध्य पार्नुहोस्",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "प्रयास छोड्नुअघि 30s भित्र क्र्यासपछि पुनः सुरु गर्ने अधिकतम सङ्ख्या (पूर्वनिर्धारित: 2)",
|
||||
"tray": "सिस्टम ट्रेमा सुरु गर्नुहोस् (डेस्कटपमा मात्र, स्वैच्छिक)",
|
||||
"no_tray": "सिस्टम ट्रे आइकन असक्षम गर्नुहोस्",
|
||||
"ready_timeout": "मिलिसेकेन्डमा तत्परता प्रोबको समयसीमा (OMNIROUTE_READY_TIMEOUT_MS पनि, पूर्वनिर्धारित 60000)",
|
||||
"tls_cert": "HTTPS सेवा दिन TLS प्रमाणपत्र (PEM) को पथ (OMNIROUTE_TLS_CERT पनि)",
|
||||
"tls_key": "HTTPS सेवा दिन TLS निजी कुञ्जी (PEM) को पथ (OMNIROUTE_TLS_KEY पनि)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP सर्भर चलिरहेको छ ({transport})",
|
||||
"stopped": "MCP सर्भर रोकियो।",
|
||||
"restarted": "MCP सर्भर पुनः सुरु गरियो।",
|
||||
"stoppedHint": "यसलाई सक्रिय गर्न `omniroute mcp enable` चलाउनुहोस्।",
|
||||
"enabled": "MCP सर्भर सक्रिय गरियो।",
|
||||
"disabled": "MCP सर्भर निष्क्रिय गरियो।",
|
||||
"enable": {
|
||||
"description": "MCP सर्भर सक्रिय गर्नुहोस्",
|
||||
"transport": "प्रयोग गर्ने ट्रान्सपोर्ट: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP सर्भर निष्क्रिय गर्नुहोस्"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP उपकरणलाई सिधै आह्वान गर्नुहोस्",
|
||||
"args": "JSON आर्गुमेन्ट अब्जेक्ट (इनलाइन)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ହାର ମାନିବା ପୂର୍ବରୁ 30s ମଧ୍ୟରେ ସର୍ବାଧିକ କ୍ରାଶ୍ ପୁନଃଆରମ୍ଭ ସଂଖ୍ୟା (ଡିଫଲ୍ଟ: 2)",
|
||||
"tray": "ସିଷ୍ଟମ୍ ଟ୍ରେରେ ଆରମ୍ଭ କରନ୍ତୁ (କେବଳ ଡେସ୍କଟପ୍, ଇଚ୍ଛାଧୀନ)",
|
||||
"no_tray": "ସିଷ୍ଟମ୍ ଟ୍ରେ ଆଇକନ୍ ଅକ୍ଷମ କରନ୍ତୁ",
|
||||
"ready_timeout": "ମିଲିସେକେଣ୍ଡରେ ପ୍ରସ୍ତୁତି ପ୍ରୋବ୍ର ସମୟସୀମା (OMNIROUTE_READY_TIMEOUT_MS ମଧ୍ୟ, ଡିଫଲ୍ଟ 60000)",
|
||||
"tls_cert": "HTTPS ପରିବେଷଣ ପାଇଁ TLS ସର୍ଟିଫିକେଟ୍ (PEM)ର ପଥ (OMNIROUTE_TLS_CERT ମଧ୍ୟ)",
|
||||
"tls_key": "HTTPS ପରିବେଷଣ ପାଇଁ TLS ବ୍ୟକ୍ତିଗତ କୀ (PEM)ର ପଥ (OMNIROUTE_TLS_KEY ମଧ୍ୟ)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP ସର୍ଭର ଚାଲୁଛି ({transport})",
|
||||
"stopped": "MCP ସର୍ଭର ବନ୍ଦ ହୋଇଛି।",
|
||||
"restarted": "MCP ସର୍ଭର ପୁନଃଚାଲୁ ହୋଇଛି।",
|
||||
"stoppedHint": "ଏହାକୁ ସକ୍ରିୟ କରିବା ପାଇଁ `omniroute mcp enable` ଚଲାନ୍ତୁ।",
|
||||
"enabled": "MCP ସର୍ଭର୍ ସକ୍ରିୟ ହୋଇଛି।",
|
||||
"disabled": "MCP ସର୍ଭର୍ ନିଷ୍କ୍ରିୟ ହୋଇଛି।",
|
||||
"enable": {
|
||||
"description": "MCP ସର୍ଭର୍କୁ ସକ୍ରିୟ କରନ୍ତୁ",
|
||||
"transport": "ବ୍ୟବହାର କରିବାକୁ ଥିବା ପରିବହନ: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP ସର୍ଭର୍କୁ ନିଷ୍କ୍ରିୟ କରନ୍ତୁ"
|
||||
},
|
||||
"call": {
|
||||
"description": "ସିଧାସଳଖ ଏକ MCP ଟୁଲ୍ ଆହ୍ୱାନ କରନ୍ତୁ",
|
||||
"args": "JSON ଆର୍ଗୁମେଣ୍ଟ ଅବଜେକ୍ଟ (ଇନ୍ଲାଇନ୍)",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "ਹਾਰ ਮੰਨਣ ਤੋਂ ਪਹਿਲਾਂ 30s ਦੇ ਅੰਦਰ ਕ੍ਰੈਸ਼ ਤੋਂ ਬਾਅਦ ਮੁੜ-ਚਾਲੂ ਕਰਨ ਦੀ ਵੱਧ ਤੋਂ ਵੱਧ ਗਿਣਤੀ (ਡਿਫਾਲਟ: 2)",
|
||||
"tray": "ਸਿਸਟਮ ਟਰੇ ਵਿੱਚ ਸ਼ੁਰੂ ਕਰੋ (ਸਿਰਫ਼ ਡੈਸਕਟਾਪ, ਚੋਣਵਾਂ)",
|
||||
"no_tray": "ਸਿਸਟਮ ਟਰੇ ਆਈਕਨ ਅਸਮਰੱਥ ਕਰੋ",
|
||||
"ready_timeout": "ਰੈਡੀਨੈੱਸ ਪ੍ਰੋਬ ਦਾ ਟਾਈਮਆਉਟ ਮਿਲੀਸਕਿੰਟਾਂ ਵਿੱਚ (OMNIROUTE_READY_TIMEOUT_MS ਵੀ, ਡਿਫਾਲਟ 60000)",
|
||||
"tls_cert": "HTTPS ਸਰਵ ਕਰਨ ਲਈ TLS ਸਰਟੀਫਿਕੇਟ (PEM) ਦਾ ਪਾਥ (OMNIROUTE_TLS_CERT ਵੀ)",
|
||||
"tls_key": "HTTPS ਸਰਵ ਕਰਨ ਲਈ TLS ਨਿੱਜੀ ਕੁੰਜੀ (PEM) ਦਾ ਪਾਥ (OMNIROUTE_TLS_KEY ਵੀ)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP ਸਰਵਰ ਚੱਲ ਰਿਹਾ ਹੈ ({transport})",
|
||||
"stopped": "MCP ਸਰਵਰ ਰੋਕਿਆ ਗਿਆ।",
|
||||
"restarted": "MCP ਸਰਵਰ ਮੁੜ ਚਾਲੂ ਕੀਤਾ ਗਿਆ।",
|
||||
"stoppedHint": "ਇਸਨੂੰ ਚਾਲੂ ਕਰਨ ਲਈ `omniroute mcp enable` ਚਲਾਓ।",
|
||||
"enabled": "MCP ਸਰਵਰ ਚਾਲੂ ਹੈ।",
|
||||
"disabled": "MCP ਸਰਵਰ ਬੰਦ ਹੈ।",
|
||||
"enable": {
|
||||
"description": "MCP ਸਰਵਰ ਚਾਲੂ ਕਰੋ",
|
||||
"transport": "ਵਰਤਣ ਲਈ ਟ੍ਰਾਂਸਪੋਰਟ: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP ਸਰਵਰ ਬੰਦ ਕਰੋ"
|
||||
},
|
||||
"call": {
|
||||
"description": "ਕਿਸੇ MCP ਟੂਲ ਨੂੰ ਸਿੱਧੇ ਚਲਾਓ",
|
||||
"args": "JSON ਆਰਗੂਮੈਂਟ ਆਬਜੈਕਟ (ਇਨਲਾਈਨ)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
"opencode": "Instala e configura o plugin @omniroute/opencode-plugin incluído para o OpenCode"
|
||||
},
|
||||
"doctor": {
|
||||
"title": "OmniRoute Doctor",
|
||||
"title": "Diagnóstico do OmniRoute",
|
||||
"dbOk": "Banco de dados: OK ({path})",
|
||||
"dbMissing": "Banco de dados: não inicializado — execute `omniroute setup`",
|
||||
"portOk": "Porta {port}: disponível",
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)",
|
||||
"tray": "Mostrar ícone na bandeja do sistema (apenas desktop, opt-in)",
|
||||
"no_tray": "Desabilitar ícone na bandeja do sistema",
|
||||
"ready_timeout": "Tempo limite da verificação de prontidão em ms (também OMNIROUTE_READY_TIMEOUT_MS, padrão 60000)",
|
||||
"tls_cert": "Caminho para um certificado TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Caminho para a chave privada TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -301,7 +302,7 @@
|
||||
"noServer": "Servidor não está em execução. Inicie com: omniroute serve",
|
||||
"title": "Saúde",
|
||||
"status": "Status: {status}",
|
||||
"uptime": "Uptime: {uptime}",
|
||||
"uptime": "Tempo de atividade: {uptime}",
|
||||
"requests": "Requisições (24h): {count}",
|
||||
"cost": "Custo (24h): ${cost}"
|
||||
},
|
||||
@@ -344,6 +345,19 @@
|
||||
},
|
||||
"mcp": {
|
||||
"title": "Servidor MCP",
|
||||
"running": "Servidor MCP em execução ({transport})",
|
||||
"stopped": "Servidor MCP parado.",
|
||||
"restarted": "Servidor MCP reiniciado.",
|
||||
"stoppedHint": "Execute `omniroute mcp enable` para ativá-lo.",
|
||||
"enabled": "Servidor MCP ativado.",
|
||||
"disabled": "Servidor MCP desativado.",
|
||||
"enable": {
|
||||
"description": "Ativar o servidor MCP",
|
||||
"transport": "Transporte a ser usado: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Desativar o servidor MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Invocar uma ferramenta MCP diretamente",
|
||||
"args": "Objeto JSON de argumentos (inline)",
|
||||
@@ -371,10 +385,7 @@
|
||||
},
|
||||
"audit": {
|
||||
"description": "Log de auditoria MCP (alias para audit --source mcp)"
|
||||
},
|
||||
"running": "Servidor MCP em execução ({transport})",
|
||||
"stopped": "Servidor MCP parado.",
|
||||
"restarted": "Servidor MCP reiniciado."
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"skills": {
|
||||
@@ -1093,7 +1104,7 @@
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"title": "Combinações",
|
||||
"switched": "Combo ativo: {name}",
|
||||
"created": "Combo criado: {name}",
|
||||
"deleted": "Combo removido: {name}",
|
||||
@@ -1268,7 +1279,7 @@
|
||||
"description": "REPL interativo multi-turn com LLM",
|
||||
"model": "Modelo a usar (padrão: auto)",
|
||||
"combo": "Nome do combo a usar",
|
||||
"system": "System prompt",
|
||||
"system": "Prompt do sistema",
|
||||
"resume": "Retomar sessão salva pelo nome"
|
||||
},
|
||||
"plugin": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "අත්හැරීමට පෙර තත්පර 30ක් තුළ සිදු කළ හැකි උපරිම බිඳවැටීම්-පසු නැවත ආරම්භ කිරීම් ගණන (පෙරනිමිය: 2)",
|
||||
"tray": "පද්ධති ට්රේ තුළ ආරම්භ කරන්න (ඩෙස්ක්ටොප් සඳහා පමණි, කැමැත්තෙන් සක්රිය කළ යුතුය)",
|
||||
"no_tray": "පද්ධති ට්රේ නිරූපකය අක්රිය කරන්න",
|
||||
"ready_timeout": "සූදානම්දැයි පරීක්ෂා කිරීමේ කල් ඉකුත්වීම ms වලින් (OMNIROUTE_READY_TIMEOUT_MS ද අදාළ වේ, පෙරනිමිය 60000)",
|
||||
"tls_cert": "HTTPS සේවය සැපයීම සඳහා TLS සහතිකයකට (PEM) ඇති මාර්ගය (OMNIROUTE_TLS_CERT ද අදාළ වේ)",
|
||||
"tls_key": "HTTPS සේවය සැපයීම සඳහා TLS පුද්ගලික යතුරට (PEM) ඇති මාර්ගය (OMNIROUTE_TLS_KEY ද අදාළ වේ)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP සේවාදායකය ක්රියාත්මකයි ({transport})",
|
||||
"stopped": "MCP සේවාදායකය නවතා ඇත.",
|
||||
"restarted": "MCP සේවාදායකය නැවත ආරම්භ කරන ලදී.",
|
||||
"stoppedHint": "එය සක්රීය කිරීමට `omniroute mcp enable` ධාවනය කරන්න.",
|
||||
"enabled": "MCP සේවාදායකය සක්රීය කර ඇත.",
|
||||
"disabled": "MCP සේවාදායකය අක්රීය කර ඇත.",
|
||||
"enable": {
|
||||
"description": "MCP සේවාදායකය සක්රීය කරන්න",
|
||||
"transport": "භාවිත කළ යුතු ප්රවාහන ක්රමය: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP සේවාදායකය අක්රීය කරන්න"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP මෙවලමක් සෘජුවම කැඳවන්න",
|
||||
"args": "JSON තර්ක වස්තුව (පේළිය තුළ)",
|
||||
@@ -1093,7 +1104,7 @@
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"title": "Combos",
|
||||
"title": "සංයෝජන",
|
||||
"switched": "සක්රිය combo එක: {name}",
|
||||
"created": "Combo එක සාදන ලදී: {name}",
|
||||
"deleted": "Combo එක මකා දමන ලදී: {name}",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Največje število ponovnih zagonov po sesutju v 30 s, preden se poskušanje opusti (privzeto: 2)",
|
||||
"tray": "Zaženi v sistemski vrstici (samo za namizne sisteme, po izbiri)",
|
||||
"no_tray": "Onemogoči ikono sistemske vrstice",
|
||||
"ready_timeout": "Časovna omejitev preverjanja pripravljenosti v ms (tudi OMNIROUTE_READY_TIMEOUT_MS, privzeto 60000)",
|
||||
"tls_cert": "Pot do potrdila TLS (PEM) za streženje prek HTTPS (tudi OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Pot do zasebnega ključa TLS (PEM) za streženje prek HTTPS (tudi OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Strežnik MCP deluje ({transport})",
|
||||
"stopped": "Strežnik MCP je ustavljen.",
|
||||
"restarted": "Strežnik MCP je znova zagnan.",
|
||||
"stoppedHint": "Za vklop zaženite `omniroute mcp enable`.",
|
||||
"enabled": "Strežnik MCP je omogočen.",
|
||||
"disabled": "Strežnik MCP je onemogočen.",
|
||||
"enable": {
|
||||
"description": "Omogoči strežnik MCP",
|
||||
"transport": "Prenos, ki naj se uporabi: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Onemogoči strežnik MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Neposredno prikliči orodje MCP",
|
||||
"args": "Objekt argumentov JSON (v vrstici)",
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Maksimalan broj ponovnih pokretanja pri padu u toku 30s pre odustajanja (podrazumevano: 2)",
|
||||
"tray": "Покретање у системској траци (само десктоп, опционо)",
|
||||
"no_tray": "Онемогући икону у системској траци",
|
||||
"ready_timeout": "Временско ограничење провере спремности у ms (такође OMNIROUTE_READY_TIMEOUT_MS, подразумевано 60000)",
|
||||
"tls_cert": "Путања до TLS сертификата (PEM) за HTTPS (такође OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Путања до TLS приватног кључа (PEM) за HTTPS (такође OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -300,7 +301,7 @@
|
||||
"description": "Провери здравље сервера и статус компоненти",
|
||||
"noServer": "Сервер није покренут. Покрените са: omniroute serve",
|
||||
"title": "Здравље",
|
||||
"status": "Status: {status}",
|
||||
"status": "Статус: {status}",
|
||||
"uptime": "Vreme rada: {uptime}",
|
||||
"requests": "Zahtevi (24h): {count}",
|
||||
"cost": "Trošak (24h): ${cost}"
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP server je pokrenut ({transport})",
|
||||
"stopped": "MCP server je zaustavljen.",
|
||||
"restarted": "MCP server je ponovo pokrenut.",
|
||||
"stoppedHint": "Покрените `omniroute mcp enable` да бисте га укључили.",
|
||||
"enabled": "MCP сервер је омогућен.",
|
||||
"disabled": "MCP сервер је онемогућен.",
|
||||
"enable": {
|
||||
"description": "Омогући MCP сервер",
|
||||
"transport": "Протокол за пренос: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Онемогући MCP сервер"
|
||||
},
|
||||
"call": {
|
||||
"description": "Direktno pozovi MCP alat",
|
||||
"args": "JSON objekat argumenata (inline)",
|
||||
@@ -810,7 +821,7 @@
|
||||
}
|
||||
},
|
||||
"program": {
|
||||
"description": "OmniRoute — Smart AI Router with Auto Fallback",
|
||||
"description": "OmniRoute — паметни AI рутер са аутоматским пребацивањем на резервну опцију",
|
||||
"version": "Прикажи верзију и изађи",
|
||||
"output": "Формат излаза (table, json, jsonl, csv)",
|
||||
"quiet": "Сакрий небитан излаз",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Taslim bo‘lishdan oldin 30 soniya ichidagi nosozlikdan keyingi qayta ishga tushirishlarning maksimal soni (standart: 2)",
|
||||
"tray": "Tizim treyida ishga tushirish (faqat ish stoli versiyasida, ixtiyoriy)",
|
||||
"no_tray": "Tizim treyi belgisini o‘chirish",
|
||||
"ready_timeout": "Tayyorlik tekshiruvi uchun kutish vaqti, ms (shuningdek OMNIROUTE_READY_TIMEOUT_MS, standart qiymat 60000)",
|
||||
"tls_cert": "HTTPS orqali xizmat ko‘rsatish uchun TLS sertifikati (PEM) yo‘li (shuningdek, OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "HTTPS orqali xizmat ko‘rsatish uchun TLS maxfiy kaliti (PEM) yo‘li (shuningdek, OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP serveri ishlamoqda ({transport})",
|
||||
"stopped": "MCP serveri toʻxtatildi.",
|
||||
"restarted": "MCP serveri qayta ishga tushirildi.",
|
||||
"stoppedHint": "Uni yoqish uchun `omniroute mcp enable` buyrugʻini ishga tushiring.",
|
||||
"enabled": "MCP serveri yoqildi.",
|
||||
"disabled": "MCP serveri oʻchirildi.",
|
||||
"enable": {
|
||||
"description": "MCP serverini yoqish",
|
||||
"transport": "Foydalaniladigan transport: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "MCP serverini oʻchirish"
|
||||
},
|
||||
"call": {
|
||||
"description": "MCP vositasini bevosita chaqirish",
|
||||
"args": "JSON argumentlar obyekti (ichki)",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Iye ìtunbẹ̀rẹ̀ lẹ́yìn ìkọ̀lù tó pọ̀ jù láàárín 30s kí ó tó jáwọ́ (àìyípadà: 2)",
|
||||
"tray": "Bẹ̀rẹ̀ nínú atẹ eto (dẹ́sítọ́ọ̀pù nìkan, àṣàyàn ni)",
|
||||
"no_tray": "Pa àmì atẹ eto rẹ́",
|
||||
"ready_timeout": "Àkókò ìdádúró tó pọ̀ jù fún àyẹ̀wò ìmúrasílẹ̀ ní ms (bákan náà OMNIROUTE_READY_TIMEOUT_MS, àìyípadà 60000)",
|
||||
"tls_cert": "Ọ̀nà sí ìwé-ẹ̀rí TLS kan (PEM) láti pèsè HTTPS (bákan náà OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Ọ̀nà sí kọ́kọ́rọ́ àdáni TLS (PEM) láti pèsè HTTPS (bákan náà OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "Olùpín MCP ń ṣiṣẹ́ ({transport})",
|
||||
"stopped": "Olùpín MCP ti dá iṣẹ́ dúró.",
|
||||
"restarted": "A ti tún olùpín MCP bẹ̀rẹ̀.",
|
||||
"stoppedHint": "Ṣe `omniroute mcp enable` láti mú un ṣiṣẹ́.",
|
||||
"enabled": "A ti mú olupin MCP ṣiṣẹ́.",
|
||||
"disabled": "A ti pa olupin MCP.",
|
||||
"enable": {
|
||||
"description": "Mú olupin MCP ṣiṣẹ́",
|
||||
"transport": "Ọ̀nà ìgbéṣùmọ̀ tí a ó lò: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Pa olupin MCP"
|
||||
},
|
||||
"call": {
|
||||
"description": "Pe ohun èlò MCP kan ní tààrà",
|
||||
"args": "Ohun àríyànjiyàn JSON (níbẹ̀-níbẹ̀)",
|
||||
|
||||
@@ -14,18 +14,6 @@
|
||||
"jsonOpt": "以 JSON 格式输出",
|
||||
"yesOpt": "跳过确认"
|
||||
},
|
||||
"program": {
|
||||
"description": "OmniRoute — 具有自动故障转移的智能 AI 路由器",
|
||||
"version": "打印版本并退出",
|
||||
"output": "输出格式(table, json, jsonl, csv)",
|
||||
"quiet": "禁止非必要输出",
|
||||
"no_color": "禁用彩色输出",
|
||||
"timeout": "HTTP 请求超时(毫秒)",
|
||||
"api_key": "OmniRoute 服务器的 API 密钥",
|
||||
"base_url": "OmniRoute 服务器的基础 URL",
|
||||
"context": "此命令使用的服务器上下文/配置文件",
|
||||
"lang": "设置 CLI 显示语言(覆盖 OMNIROUTE_LANG)"
|
||||
},
|
||||
"setup": {
|
||||
"title": "OmniRoute 设置",
|
||||
"passwordPrompt": "管理员密码",
|
||||
@@ -145,6 +133,20 @@
|
||||
"listTitle": "{days} 天内过期的密钥:"
|
||||
}
|
||||
},
|
||||
"authExport": {
|
||||
"description": "导出已解密的提供者凭据(仅限本地,明文输出)",
|
||||
"idOpt": "仅导出与此 id/名称/提供者匹配的连接",
|
||||
"formatOpt": "输出格式:json 或 env",
|
||||
"outOpt": "将输出写入文件而非标准输出(以 0600 权限写入)",
|
||||
"forceOpt": "确认你了解此操作会打印/写入明文密钥",
|
||||
"warning": "⚠ 此操作会打印/写入已解密的明文 API 密钥和 OAuth 令牌。请确保你的屏幕、shell 历史记录以及任何输出文件保持私密。",
|
||||
"confirmHeading": "⚠ 警告:此操作会以明文导出已解密的提供者凭据",
|
||||
"confirmBody": "此命令会为所选连接解密并打印/写入 apiKey、accessToken、refreshToken 和\nidToken。请将输出视为机密。",
|
||||
"confirmFooter": "如需确认,请运行:\n omniroute auth export --force",
|
||||
"missingKey": "导出凭据需要 STORAGE_ENCRYPTION_KEY。",
|
||||
"notFound": "未找到连接:{id}",
|
||||
"invalidFormat": "无效格式:{format}。请使用 json 或 env。"
|
||||
},
|
||||
"stream": {
|
||||
"description": "使用 SSE 检查模式流式传输聊天响应",
|
||||
"file": "从文件读取提示",
|
||||
@@ -254,6 +256,7 @@
|
||||
"max_restarts": "30 秒内的最大崩溃重启次数(默认:2)",
|
||||
"tray": "显示系统托盘图标(仅桌面,选择加入)",
|
||||
"no_tray": "禁用系统托盘图标",
|
||||
"ready_timeout": "就绪探测超时(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS,默认 60000)",
|
||||
"tls_cert": "用于提供 HTTPS 服务的 TLS 证书(PEM)路径(也可用 OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "用于提供 HTTPS 服务的 TLS 私钥(PEM)路径(也可用 OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -301,7 +304,7 @@
|
||||
"status": "状态:{status}",
|
||||
"uptime": "运行时间:{uptime}",
|
||||
"requests": "请求数(24h):{count}",
|
||||
"cost": "成本(24h):"
|
||||
"cost": "成本(24h):${cost}"
|
||||
},
|
||||
"quota": {
|
||||
"description": "显示提供者配额使用情况",
|
||||
@@ -345,6 +348,16 @@
|
||||
"running": "MCP 服务器正在运行({transport})",
|
||||
"stopped": "MCP 服务器已停止。",
|
||||
"restarted": "MCP 服务器已重启。",
|
||||
"stoppedHint": "运行 `omniroute mcp enable` 以启用它。",
|
||||
"enabled": "MCP 服务器已启用。",
|
||||
"disabled": "MCP 服务器已禁用。",
|
||||
"enable": {
|
||||
"description": "启用 MCP 服务器",
|
||||
"transport": "要使用的传输方式:stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "禁用 MCP 服务器"
|
||||
},
|
||||
"call": {
|
||||
"description": "直接调用 MCP 工具",
|
||||
"args": "JSON 参数对象(内联)",
|
||||
@@ -807,6 +820,18 @@
|
||||
"event": "要模拟的事件类型(默认:request.completed)"
|
||||
}
|
||||
},
|
||||
"program": {
|
||||
"description": "OmniRoute — 具有自动故障转移的智能 AI 路由器",
|
||||
"version": "打印版本并退出",
|
||||
"output": "输出格式(table, json, jsonl, csv)",
|
||||
"quiet": "禁止非必要输出",
|
||||
"no_color": "禁用彩色输出",
|
||||
"timeout": "HTTP 请求超时(毫秒)",
|
||||
"api_key": "OmniRoute 服务器的 API 密钥",
|
||||
"base_url": "OmniRoute 服务器的基础 URL",
|
||||
"context": "此命令使用的服务器上下文/配置文件",
|
||||
"lang": "设置 CLI 显示语言(覆盖 OMNIROUTE_LANG)"
|
||||
},
|
||||
"files": {
|
||||
"description": "管理文件(上传、列出、获取、下载、删除)",
|
||||
"list": {
|
||||
@@ -907,6 +932,11 @@
|
||||
"model": "按模型筛选"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "检查并同步本地 Radar 目录订阅源",
|
||||
"status": "显示本地 Radar 设置和订阅源缓存状态",
|
||||
"sync": "通过本地服务器同步目录、推荐、优惠和 Intel"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "检查和管理弹性机制",
|
||||
"status": {
|
||||
@@ -1234,8 +1264,8 @@
|
||||
"description": "管理 OmniRoute 开机自启(Linux:systemd 用户服务)",
|
||||
"enable": "启用开机自启",
|
||||
"disable": "禁用开机自启",
|
||||
"status": "显示自启状态",
|
||||
"toggle": "切换开机自启"
|
||||
"toggle": "切换开机自启",
|
||||
"status": "显示自启状态"
|
||||
},
|
||||
"runtime": {
|
||||
"description": "管理本地运行时依赖",
|
||||
@@ -1262,25 +1292,6 @@
|
||||
"update": "更新已安装的插件",
|
||||
"scaffold": "搭建新的插件模板"
|
||||
},
|
||||
"authExport": {
|
||||
"description": "导出已解密的提供者凭据(仅限本地,明文输出)",
|
||||
"idOpt": "仅导出与此 id/名称/提供者匹配的连接",
|
||||
"formatOpt": "输出格式:json 或 env",
|
||||
"outOpt": "将输出写入文件而非标准输出(以 0600 权限写入)",
|
||||
"forceOpt": "确认你了解此操作会打印/写入明文密钥",
|
||||
"warning": "⚠ 此操作会打印/写入已解密的明文 API 密钥和 OAuth 令牌。请确保你的屏幕、shell 历史记录以及任何输出文件保持私密。",
|
||||
"confirmHeading": "⚠ 警告:此操作会以明文导出已解密的提供者凭据",
|
||||
"confirmBody": "此命令会为所选连接解密并打印/写入 apiKey、accessToken、refreshToken 和\nidToken。请将输出视为机密。",
|
||||
"confirmFooter": "如需确认,请运行:\n omniroute auth export --force",
|
||||
"missingKey": "导出凭据需要 STORAGE_ENCRYPTION_KEY。",
|
||||
"notFound": "未找到连接:{id}",
|
||||
"invalidFormat": "无效格式:{format}。请使用 json 或 env。"
|
||||
},
|
||||
"radar": {
|
||||
"description": "检查并同步本地 Radar 目录订阅源",
|
||||
"status": "显示本地 Radar 设置和订阅源缓存状态",
|
||||
"sync": "通过本地服务器同步目录、推荐、优惠和 Intel"
|
||||
},
|
||||
"launch": {
|
||||
"description": "启动指向 OmniRoute 的 Claude Code(本地或远程,使用 --profile)",
|
||||
"token": "Claude 客户端应发送的令牌(ANTHROPIC_AUTH_TOKEN)",
|
||||
|
||||
@@ -14,18 +14,6 @@
|
||||
"jsonOpt": "以 JSON 格式輸出",
|
||||
"yesOpt": "跳過確認"
|
||||
},
|
||||
"program": {
|
||||
"description": "OmniRoute — 具有自動故障轉移的智慧 AI 路由器",
|
||||
"version": "列印版本並退出",
|
||||
"output": "輸出格式(table, json, jsonl, csv)",
|
||||
"quiet": "禁止非必要輸出",
|
||||
"no_color": "停用彩色輸出",
|
||||
"timeout": "HTTP 請求超時(毫秒)",
|
||||
"api_key": "OmniRoute 伺服器的 API 金鑰",
|
||||
"base_url": "OmniRoute 伺服器的基礎 URL",
|
||||
"context": "此命令使用的伺服器上下文/配置檔案",
|
||||
"lang": "設定 CLI 顯示語言(覆蓋 OMNIROUTE_LANG)"
|
||||
},
|
||||
"setup": {
|
||||
"title": "OmniRoute 設定",
|
||||
"passwordPrompt": "管理員密碼",
|
||||
@@ -145,6 +133,20 @@
|
||||
"listTitle": "{days} 天內過期的金鑰:"
|
||||
}
|
||||
},
|
||||
"authExport": {
|
||||
"description": "匯出已解密的提供者憑據(僅限本機,明文輸出)",
|
||||
"idOpt": "僅匯出與此 id/名稱/提供者相符的連線",
|
||||
"formatOpt": "輸出格式:json 或 env",
|
||||
"outOpt": "將輸出寫入檔案而非標準輸出(以 0600 權限寫入)",
|
||||
"forceOpt": "確認你了解此操作會列印/寫入明文密鑰",
|
||||
"warning": "⚠ 此操作會列印/寫入已解密的明文 API 金鑰和 OAuth 令牌。請確保你的螢幕、shell 歷史記錄以及任何輸出檔案保持私密。",
|
||||
"confirmHeading": "⚠ 警告:此操作會以明文匯出已解密的提供者憑據",
|
||||
"confirmBody": "此命令會為所選連線解密並列印/寫入 apiKey、accessToken、refreshToken 和\nidToken。請將輸出視為機密。",
|
||||
"confirmFooter": "如需確認,請執行:\n omniroute auth export --force",
|
||||
"missingKey": "匯出憑據需要 STORAGE_ENCRYPTION_KEY。",
|
||||
"notFound": "找不到連線:{id}",
|
||||
"invalidFormat": "無效格式:{format}。請使用 json 或 env。"
|
||||
},
|
||||
"stream": {
|
||||
"description": "使用 SSE 檢查模式流式傳輸聊天響應",
|
||||
"file": "從檔案讀取提示",
|
||||
@@ -254,6 +256,7 @@
|
||||
"max_restarts": "30 秒內的最大崩潰重啟次數(預設:2)",
|
||||
"tray": "顯示系統托盤圖示(僅桌面,選擇加入)",
|
||||
"no_tray": "停用系統托盤圖示",
|
||||
"ready_timeout": "就緒探測逾時(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS,預設 60000)",
|
||||
"tls_cert": "用於提供 HTTPS 服務的 TLS 憑證(PEM)路徑(也可用 OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "用於提供 HTTPS 服務的 TLS 私鑰(PEM)路徑(也可用 OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -301,7 +304,7 @@
|
||||
"status": "狀態:{status}",
|
||||
"uptime": "執行時間:{uptime}",
|
||||
"requests": "請求數(24h):{count}",
|
||||
"cost": "成本(24h):"
|
||||
"cost": "成本(24h):${cost}"
|
||||
},
|
||||
"quota": {
|
||||
"description": "顯示提供者配額使用情況",
|
||||
@@ -345,6 +348,16 @@
|
||||
"running": "MCP 伺服器正在執行({transport})",
|
||||
"stopped": "MCP 伺服器已停止。",
|
||||
"restarted": "MCP 伺服器已重啟。",
|
||||
"stoppedHint": "執行 `omniroute mcp enable` 以啟用它。",
|
||||
"enabled": "MCP 伺服器已啟用。",
|
||||
"disabled": "MCP 伺服器已停用。",
|
||||
"enable": {
|
||||
"description": "啟用 MCP 伺服器",
|
||||
"transport": "要使用的傳輸方式:stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "停用 MCP 伺服器"
|
||||
},
|
||||
"call": {
|
||||
"description": "直接呼叫 MCP 工具",
|
||||
"args": "JSON 引數物件(內聯)",
|
||||
@@ -807,6 +820,18 @@
|
||||
"event": "要模擬的事件型別(預設:request.completed)"
|
||||
}
|
||||
},
|
||||
"program": {
|
||||
"description": "OmniRoute — 具有自動故障轉移的智慧 AI 路由器",
|
||||
"version": "列印版本並退出",
|
||||
"output": "輸出格式(table, json, jsonl, csv)",
|
||||
"quiet": "禁止非必要輸出",
|
||||
"no_color": "停用彩色輸出",
|
||||
"timeout": "HTTP 請求超時(毫秒)",
|
||||
"api_key": "OmniRoute 伺服器的 API 金鑰",
|
||||
"base_url": "OmniRoute 伺服器的基礎 URL",
|
||||
"context": "此命令使用的伺服器上下文/配置檔案",
|
||||
"lang": "設定 CLI 顯示語言(覆蓋 OMNIROUTE_LANG)"
|
||||
},
|
||||
"files": {
|
||||
"description": "管理檔案(上傳、列出、獲取、下載、刪除)",
|
||||
"list": {
|
||||
@@ -907,6 +932,11 @@
|
||||
"model": "按模型篩選"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "檢查並同步本機 Radar 目錄訂閱來源",
|
||||
"status": "顯示本機 Radar 設定和訂閱來源快取狀態",
|
||||
"sync": "透過本機伺服器同步目錄、推薦、優惠和 Intel"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "檢查和管理彈性機制",
|
||||
"status": {
|
||||
@@ -1234,8 +1264,8 @@
|
||||
"description": "管理 OmniRoute 開機自啟(Linux:systemd 使用者服務)",
|
||||
"enable": "啟用開機自啟",
|
||||
"disable": "停用開機自啟",
|
||||
"status": "顯示自啟狀態",
|
||||
"toggle": "切換開機自啟"
|
||||
"toggle": "切換開機自啟",
|
||||
"status": "顯示自啟狀態"
|
||||
},
|
||||
"runtime": {
|
||||
"description": "管理本地執行時依賴",
|
||||
@@ -1262,25 +1292,6 @@
|
||||
"update": "更新已安裝的外掛",
|
||||
"scaffold": "搭建新的外掛模板"
|
||||
},
|
||||
"authExport": {
|
||||
"description": "匯出已解密的提供者憑據(僅限本機,明文輸出)",
|
||||
"idOpt": "僅匯出與此 id/名稱/提供者相符的連線",
|
||||
"formatOpt": "輸出格式:json 或 env",
|
||||
"outOpt": "將輸出寫入檔案而非標準輸出(以 0600 權限寫入)",
|
||||
"forceOpt": "確認你了解此操作會列印/寫入明文密鑰",
|
||||
"warning": "⚠ 此操作會列印/寫入已解密的明文 API 金鑰和 OAuth 令牌。請確保你的螢幕、shell 歷史記錄以及任何輸出檔案保持私密。",
|
||||
"confirmHeading": "⚠ 警告:此操作會以明文匯出已解密的提供者憑據",
|
||||
"confirmBody": "此命令會為所選連線解密並列印/寫入 apiKey、accessToken、refreshToken 和\nidToken。請將輸出視為機密。",
|
||||
"confirmFooter": "如需確認,請執行:\n omniroute auth export --force",
|
||||
"missingKey": "匯出憑據需要 STORAGE_ENCRYPTION_KEY。",
|
||||
"notFound": "找不到連線:{id}",
|
||||
"invalidFormat": "無效格式:{format}。請使用 json 或 env。"
|
||||
},
|
||||
"radar": {
|
||||
"description": "檢查並同步本機 Radar 目錄訂閱來源",
|
||||
"status": "顯示本機 Radar 設定和訂閱來源快取狀態",
|
||||
"sync": "透過本機伺服器同步目錄、推薦、優惠和 Intel"
|
||||
},
|
||||
"launch": {
|
||||
"description": "啟動指向 OmniRoute 的 Claude Code(本機或遠端,使用 --profile)",
|
||||
"token": "Claude 用戶端應傳送的令牌(ANTHROPIC_AUTH_TOKEN)",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, appendFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writePidFile, cleanupPidFile, killAllSubprocesses, isPidRunning } from "../utils/pid.mjs";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
import {
|
||||
RESTART_RESET_MS,
|
||||
DEFAULT_MAX_RESTARTS,
|
||||
@@ -14,10 +16,18 @@ import { stopProcessGracefully } from "../../../src/shared/platform/windowsProce
|
||||
import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
isFatalStartupDiagnostic,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
|
||||
// #13538: shared path resolver so `omniroute doctor` (bin/cli/commands/doctor.mjs)
|
||||
// can surface the same file persistCrashLog() writes, without duplicating the
|
||||
// `<DATA_DIR>/server/...` convention from bin/cli/utils/pid.mjs.
|
||||
export function getCrashLogPath() {
|
||||
return join(resolveDataDir(), "server", "crash.log");
|
||||
}
|
||||
|
||||
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
// Bun needs the Node-compat polyfill preloaded (#9761). The file ships at the
|
||||
// package root via package.json "files" (see scripts/build/pack-artifact-policy.ts)
|
||||
@@ -55,12 +65,14 @@ export class ServerSupervisor {
|
||||
this.child = null;
|
||||
this.isShuttingDown = false;
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
this.fatalStartupDiagnosticPrinted = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.crashLog = [];
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
this.fatalStartupDiagnosticPrinted = false;
|
||||
|
||||
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
|
||||
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
|
||||
@@ -99,6 +111,15 @@ export class ServerSupervisor {
|
||||
)
|
||||
);
|
||||
}
|
||||
// #13314: surface any `[STARTUP] Fatal:`-guarded boot diagnostic
|
||||
// immediately, even without --log — otherwise it is only buffered and
|
||||
// reaches the operator on exit/crash, which never happens when the
|
||||
// HTTP listener still comes up after the fatal failure (every route
|
||||
// then 500s with zero visible diagnostic anywhere).
|
||||
if (!this.fatalStartupDiagnosticPrinted && isFatalStartupDiagnostic(text)) {
|
||||
this.fatalStartupDiagnosticPrinted = true;
|
||||
process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.child.stdout) {
|
||||
@@ -152,7 +173,8 @@ export class ServerSupervisor {
|
||||
if (aliveMs >= RESTART_RESET_MS) this.restartCount = 0;
|
||||
|
||||
if (this.restartCount >= this.maxRestarts) {
|
||||
console.error(`\n⚠ Server crashed ${this.maxRestarts} times in <30s.`);
|
||||
const summary = `Server crashed ${this.maxRestarts} times in <30s.`;
|
||||
console.error(`\n⚠ ${summary}`);
|
||||
if (this.onCrashCallback) {
|
||||
const action = this.onCrashCallback(this.crashLog);
|
||||
if (action === "disable-mitm-and-retry") {
|
||||
@@ -163,6 +185,14 @@ export class ServerSupervisor {
|
||||
}
|
||||
}
|
||||
this.dumpCrashLog();
|
||||
// #13538: the give-up path used to only console.error() this diagnostic.
|
||||
// In `--tray`/`--tray-worker` mode this process is launched detached with
|
||||
// stdio:"ignore" (bin/cli/tray/detachedTray.mjs buildTrayLaunch()), so
|
||||
// that console output is discarded by the OS and nothing ever explains
|
||||
// why the tray + gateway disappeared together. Best-effort persist a
|
||||
// durable record next to the existing per-service PID file convention
|
||||
// (bin/cli/utils/pid.mjs) so it survives the process exit below.
|
||||
this.persistCrashLog(summary);
|
||||
process.exit(exitCode ?? 1);
|
||||
return;
|
||||
}
|
||||
@@ -194,6 +224,23 @@ export class ServerSupervisor {
|
||||
console.error("--- End crash log ---\n");
|
||||
}
|
||||
|
||||
// #13538: best-effort append a durable crash record to
|
||||
// `<DATA_DIR>/server/crash.log`, mirroring the `<DATA_DIR>/<service>/.pid`
|
||||
// layout from bin/cli/utils/pid.mjs. Wrapped in try/catch — this diagnostic
|
||||
// write must NEVER block or fail shutdown (the give-up branch always calls
|
||||
// process.exit() right after this).
|
||||
persistCrashLog(summary) {
|
||||
try {
|
||||
const crashLogPath = getCrashLogPath();
|
||||
mkdirSync(dirname(crashLogPath), { recursive: true });
|
||||
const timestamp = new Date().toISOString();
|
||||
const body = [`[${timestamp}] ${summary}`, ...this.crashLog, ""].join("\n");
|
||||
appendFileSync(crashLogPath, body, "utf8");
|
||||
} catch {
|
||||
// Best-effort only — a diagnostic write failure must not prevent shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isShuttingDown = true;
|
||||
if (this.child?.pid) {
|
||||
|
||||
BIN
bin/cli/tray/icon.ico
Normal file
BIN
bin/cli/tray/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 286 B |
@@ -1,15 +1,86 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
|
||||
// #13679 PR B: checked-in literal, used ONLY as a last-resort fallback (see
|
||||
// getActiveSalt() below) — /etc/machine-id is commonly world-readable, so relying on
|
||||
// this literal as the real default let any local user derive the same bearer token.
|
||||
const BUILTIN_DEFAULT_SALT = "omniroute-cli-auth-v1";
|
||||
const SALT_FILE_NAME = "cli-token-salt.json";
|
||||
const PERSISTED_SALT_RE = /^[0-9a-f]{64}$/;
|
||||
export const CLI_TOKEN_HEADER = "x-omniroute-cli-token";
|
||||
|
||||
let _cached = null;
|
||||
let _cachedSalt = null;
|
||||
let _cachedActiveSalt = null;
|
||||
|
||||
/** A `node --test` (or vitest) process that never opted into an explicit DATA_DIR must
|
||||
* not write a salt file into the operator's real home directory. Mirrors
|
||||
* dataPaths.ts::isTestContext() on the TS side. */
|
||||
function isTestContext() {
|
||||
return (
|
||||
process.env.NODE_ENV === "test" ||
|
||||
!!process.env.VITEST ||
|
||||
!!process.env.NODE_TEST_CONTEXT ||
|
||||
process.execArgv.includes("--test") ||
|
||||
process.argv.includes("--test")
|
||||
);
|
||||
}
|
||||
|
||||
function saltFilePath(dataDir) {
|
||||
return path.join(dataDir, SALT_FILE_NAME);
|
||||
}
|
||||
|
||||
function readPersistedSalt(filePath) {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
const salt = parsed && typeof parsed === "object" ? parsed.salt : undefined;
|
||||
if (typeof salt === "string" && PERSISTED_SALT_RE.test(salt)) return salt;
|
||||
} catch {
|
||||
// Missing, unreadable, or corrupt — fall through to (re)generation.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Mirrors establishPersistedSalt() in src/lib/machineToken.ts — same resolution
|
||||
* order, same salt file, same `wx`-flag create-race handling — so the CLI and the
|
||||
* server converge on the same bearer token (docs/security/CLI_TOKEN.md). */
|
||||
function establishPersistedSalt(dataDir) {
|
||||
const filePath = saltFilePath(dataDir);
|
||||
const existing = readPersistedSalt(filePath);
|
||||
if (existing) return existing;
|
||||
|
||||
const generated = crypto.randomBytes(32).toString("hex");
|
||||
try {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify({ salt: generated }), { flag: "wx", mode: 0o600 });
|
||||
return generated;
|
||||
} catch (err) {
|
||||
if (err && err.code === "EEXIST") return readPersistedSalt(filePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Mirrors getActiveSalt() in src/lib/machineToken.ts so a rotated
|
||||
* OMNIROUTE_CLI_SALT reaches the CLI too (docs/security/CLI_TOKEN.md). */
|
||||
function getActiveSalt() {
|
||||
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
|
||||
const envSalt = process.env.OMNIROUTE_CLI_SALT;
|
||||
if (envSalt) return envSalt;
|
||||
|
||||
if (_cachedActiveSalt) return _cachedActiveSalt;
|
||||
|
||||
const hasExplicitDataDir = !!(process.env.DATA_DIR && process.env.DATA_DIR.trim());
|
||||
if (!hasExplicitDataDir && isTestContext()) {
|
||||
_cachedActiveSalt = BUILTIN_DEFAULT_SALT;
|
||||
return _cachedActiveSalt;
|
||||
}
|
||||
|
||||
const dataDir = resolveDataDir();
|
||||
const persisted = establishPersistedSalt(dataDir);
|
||||
_cachedActiveSalt = persisted || BUILTIN_DEFAULT_SALT;
|
||||
return _cachedActiveSalt;
|
||||
}
|
||||
|
||||
export function deriveCliToken(machineIdModule, salt) {
|
||||
@@ -19,8 +90,7 @@ export function deriveCliToken(machineIdModule, salt) {
|
||||
// undefined and calling it throws — which the catch below turned into an
|
||||
// empty token, silently disabling CLI auth for every management request.
|
||||
// Same resolution order as src/lib/machineToken.ts.
|
||||
const machineIdSync =
|
||||
machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
const machineIdSync = machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") return "";
|
||||
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
|
||||
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
|
||||
|
||||
@@ -105,6 +105,27 @@ export function isFatalInstrumentationHookFailure(text) {
|
||||
return /Unsupported platform:\s*android/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect any fatal boot-time diagnostic guarded by the `[STARTUP] Fatal:`
|
||||
* prefix (`src/instrumentation-node.ts::ensureDbReadyForBoot()`,
|
||||
* `src/instrumentation.ts::register()`, and any future guard using the same
|
||||
* marker). #13314: in the default `omniroute serve` mode (no `--log`),
|
||||
* `ServerSupervisor` only buffers stdout/stderr and flushes it to the real
|
||||
* console on exit/crash/readiness-timeout — so if the HTTP listener still
|
||||
* comes up after a fatal boot diagnostic was already printed (e.g. the
|
||||
* better-sqlite3 / node:sqlite driver cascade failing hard), the operator
|
||||
* sees "OmniRoute is running!" with zero visible diagnostic anywhere, and
|
||||
* every route 500s. This generalizes the #10028 Android/Termux carve-out to
|
||||
* every `[STARTUP] Fatal:` guard, not just that one platform-specific string.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFatalStartupDiagnostic(text) {
|
||||
if (!text) return false;
|
||||
return /^\[STARTUP\] Fatal:/m.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator-facing hint when that instrumentation failure shows up in child
|
||||
* output — defense in depth if prep was skipped or a future Next.js probe
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(gamification): pay the documented `streak_bonus` and `badge_unlock` XP rewards.** `XP_REWARDS` listed both rewards but the award pipeline never paid them: the private reward table in `events.ts` omitted them, `updateStreak()` did not report when a streak extended, and badge unlocks carried no XP. Every request that extends a daily streak now pays `streak_bonus × streak length` once per UTC day (guarded by a same-day `xp_audit_log` check), and every badge unlocked through the pipeline pays `badge_unlock` once per badge (guarded by the `user_badges` primary key; `unlockBadge()` now reports whether it inserted). Bonus XP flows through the same `addXp` + level sync + global/weekly/monthly leaderboard path as action XP, so level-ups and rankings include it. The Radar supporter recognition unlock stays XP-free. (#12522 — thanks @pacocartones)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** advertise a `free-tier` capability in the provider plugin manifest for every provider with documented free models, so sidecars and dashboards can filter free-capable providers without reading the quota catalog ([#12786](https://github.com/diegosouzapw/OmniRoute/pull/12786)) — thanks @maxmad64bis
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(sse):** parse/scrub DSML tool-call markers embedded in reasoning and recognize adaptive thinking on the response side — `dsmlToolCalls.ts` module + translator/stream/handler wiring ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user