Compare commits

..

20 Commits

Author SHA1 Message Date
diegosouzapw
8d569e8fea fix(types): restore clean typecheck:core for v3.8.18 release gate
- getPendingRequests() typed to real shape (was widened to object) → fixes
  unknown 'count' in the unified-requests view (#3401)
- streamChunks log payload cast to its declared type (callLogs.ts)
- preScreenTargets aligned to canonical IsModelAvailable signature (#3169),
  Promise.resolve-normalized so .catch never hits a bare boolean

All 5 gates green: lint(0 err) + typecheck:core + cycles + docs-all + unit + vitest(146).
2026-06-09 15:01:36 -03:00
diegosouzapw
f9a93f4ea4 fix(opencode-plugin): bound regex quantifiers in normaliseFreeLabel (polynomial-ReDoS)
CodeQL js/polynomial-redos: unbounded \s* before an anchored \s*$ allowed
O(n²) backtracking on attacker-influenced display names. Bounded to {0,8}/{1,8}
(ample for any real label spacing). Plugin builds + 254 tests green.
2026-06-09 14:42:10 -03:00
diegosouzapw
2eca33547b docs(changelog): reconcile v3.8.18 — add #3519, #3513, #3435-repair, gitignore chore (full commit↔changelog coverage) 2026-06-09 14:38:17 -03:00
Markus Hartung
d83f215a50 fix: llama base url normalization (#3519) 2026-06-09 14:29:44 -03:00
diegosouzapw
7edec34bd8 docs(changelog): record #3510, #3504, #3503 under v3.8.18 2026-06-09 13:26:49 -03:00
Felipe Almeman
20d8240252 fix(playground): authenticate via session, test key policy by id (#3503)
Integrated into release/v3.8.18
2026-06-09 13:26:09 -03:00
Felipe Almeman
557bfddfed fix(authz): fall back to URL token when Authorization isn't a usable Bearer (#3504)
Integrated into release/v3.8.18
2026-06-09 13:22:17 -03:00
Diego Rodrigues de Sa e Souza
c3c21fd56a fix(sse): drop empty-choices chunks without usage instead of injecting retry text (#3513)
PR #3422 ('allow OpenAI usage-only empty choices chunks') reintroduced the
assistant-content injection '[OmniRoute] Upstream returned an empty response.
Please retry.' for empty `choices: []` chunks that carry no valid usage. Clients
(Goose/opencode) feed that text back as a turn and spin in a retry loop -- the
exact regression #3400 had fixed by dropping the chunk.

Restore the drop behavior for the no-usage case while preserving #3422's
standards-compliant forwarding of usage-only `include_usage` final chunks.
Realign the mislabeled stream-utils test (it asserted the injection) and add a
dedicated regression guard.

Reported-by: @mochizzan
Refs: #3502, #3388, #3400, #3422
2026-06-09 13:18:56 -03:00
Paijo
4c8ad86cd9 feat(docs): add doc accuracy gate + refresh AGENTS.md counts (#3510)
Integrated into release/v3.8.18
2026-06-09 13:17:52 -03:00
diegosouzapw
e6752623d7 docs(changelog): record #3401, #3473, #3492, #3490, #3451, #3491, #3169 under v3.8.18 2026-06-09 12:33:29 -03:00
Markus Hartung
2664ddb815 feat(ui): unifi active and finished requests into single view #1422 (#3401)
Integrated into release/v3.8.18
2026-06-09 12:31:32 -03:00
PizzaV
c77215f204 fix(combo): parallel pre-screen + circuit-breaker fast-exit for priority combos (#3169)
Integrated into release/v3.8.18
2026-06-09 11:14:27 -03:00
Paijo
0d9f1fc395 feat(plugins): add lifecycle hooks and theme-manager plugin (#3473)
Integrated into release/v3.8.18
2026-06-09 11:12:53 -03:00
Andrey Borodulin
35ede42b60 fix(theoldllm): generate X-Request-Token server-side, drop Playwright (#3491)
Integrated into release/v3.8.18
2026-06-09 10:43:52 -03:00
Andrey Borodulin
a8e04f513b feat(browserPool): resolve Playwright proxy from proxy_registry DB (#3492)
Integrated into release/v3.8.18
2026-06-09 10:41:09 -03:00
Dmitrii Safronov
c5b9b2b0db fix(sse): normalize numeric provider ids to strings (#3451)
Integrated into release/v3.8.18
2026-06-09 10:40:25 -03:00
Andrey Borodulin
212fe8ed04 fix(provider): detect Responses API format when body has input but … (#3490)
Integrated into release/v3.8.18
2026-06-09 10:39:38 -03:00
diegosouzapw
d03fd61876 chore: ignore quality reports and local prompt artifacts
Add generated quality gate reports, metrics files, and local setup prompt
artifacts to .gitignore to prevent committing environment-specific or
temporary files.
2026-06-09 10:27:55 -03:00
Diego Rodrigues de Sa e Souza
dd43b01025 fix(catalog): stop Codex CLI model-catalog refresh from erroring (#3481)
Codex's model-catalog refresh (codex_models_manager) does
GET /v1/models?client_version=<v> and decodes a JSON object with a
TOP-LEVEL `models` array. OmniRoute answers in the OpenAI-standard
`{object,data}` shape, so codex fails with "missing field `models`"
and logs "failed to refresh available models" on every startup.

Detect codex clients via the `originator` / `user-agent` = `codex_*`
headers they send and add an EMPTY top-level `models: []` so the decode
succeeds. Non-codex OpenAI clients keep the byte-identical `{object,data}`
response.

The array is intentionally empty: codex replaces its built-in per-model
agent prompt (`base_instructions`, ~21k chars) with whatever a populated
entry carries for the selected model, so emitting our catalog would drop
the agent prompt to nothing and break codex's agent behaviour (verified
empirically against codex 0.137). An empty list keeps codex on its
built-in model info — same inference as before, minus the error.

Validated end-to-end with the real handler against codex 0.137:
"failed to refresh available models" → 0 occurrences, instructions
preserved (built-in Codex agent prompt, not empty).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:48:11 -03:00
diegosouzapw
b27bf84715 chore(release): open v3.8.18 development cycle 2026-06-09 08:31:35 -03:00
4649 changed files with 116573 additions and 550351 deletions

View File

@@ -77,8 +77,6 @@ bun.lock
# Agent config
.agents
.gemini
.claude
.source
# Misc
llm.txt
@@ -125,4 +123,3 @@ app.__qa_backup/
.worktrees
.next-playwright/
cloud/
electron/dist-electron

View File

@@ -1,12 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.sh]
indent_size = 4

View File

@@ -56,12 +56,10 @@ STORAGE_ENCRYPTION_KEY_VERSION=v1
DISABLE_SQLITE_AUTO_BACKUP=false
# ── Redis (Rate Limiting) ──
# Redis connection URL for the rate limiter backend. OPT-IN: leave this
# commented out to use the built-in in-memory rate limiter. Setting it to a
# non-running localhost (#4878) makes ioredis flood "[REDIS] Error:" logs.
# Redis connection URL for the rate limiter backend.
# Used by: src/shared/utils/rateLimiter.ts
# Example: redis://localhost:6379 (or redis://redis:6379 in Docker)
# REDIS_URL=redis://localhost:6379
# Default: redis://localhost:6379 (or redis://redis:6379 in Docker)
REDIS_URL=redis://localhost:6379
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
@@ -73,11 +71,6 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Default: 20128
PORT=20128
# Base path (URL subpath) when serving OmniRoute behind a reverse proxy under a subpath.
# Used by: next.config.mjs — sets Next.js `basePath`; auth redirects are basePath-aware.
# Default: "" (served at the domain root). Example: /omniroute to serve under https://host/omniroute
# OMNIROUTE_BASE_PATH=
# Split-port mode: serve Dashboard and API on separate ports for network isolation.
# Used by: src/lib/runtime/ports.ts — overrides PORT for each service.
# API_PORT=20129
@@ -97,28 +90,8 @@ PORT=20128
# Comma-separated extra origins allowed to open a live WebSocket. The
# loopback dashboard origins are already permitted by default; use this
# var when fronting the server with a domain (e.g. https://omni.local).
# ⚠️ When using NEXT_PUBLIC_LIVE_WS_PUBLIC_URL or exposing the WS server
# beyond loopback, this MUST include the public origin(s) — otherwise
# the Origin allow-list check will reject all browser connections.
# Example: LIVE_WS_ALLOWED_ORIGINS=https://omni.local,https://dashboard.example.com,https://ws.my-ai.com
# LIVE_WS_ALLOWED_ORIGINS=https://omni.local,https://dashboard.example.com
# Comma-separated extra hostnames allowed to open a live WebSocket (LAN/Tailscale).
# Unlike LIVE_WS_ALLOWED_ORIGINS (which matches full origin URLs), this matches
# only the host portion — useful for wildcard-ish LAN/Tailscale setups.
# Used by: src/server/ws/liveServerAllowList.ts
# Example: LIVE_WS_ALLOWED_HOSTS=omni.local,tailscale-host,192.168.1.50
# LIVE_WS_ALLOWED_HOSTS=omni.local,tailscale-host,192.168.1.50
# Public URL for the live dashboard WebSocket (client-side, browser only).
# Set this when fronting the WS server with a reverse proxy or Cloudflare Tunnel.
# The browser will connect to this URL instead of ws://hostname:20129.
# The /live-ws path is already proxied from the main app (port 20128) to the
# live WS server (port 20129) by scripts/dev/standalone-server-ws.mjs.
# Used by: src/hooks/useLiveDashboard.ts
# Example: NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=wss://ws.my-ai.com/live-ws
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=
# Disable the standalone live WebSocket helper used by scripts/start-ws-server.mjs.
# Used by: scripts/start-ws-server.mjs (CI/embedded harness toggle).
# OMNIROUTE_DISABLE_LIVE_WS=0
@@ -136,18 +109,8 @@ PORT=20128
# Used by: src/app/api/v1/relay/chat/completions/route.ts
# RELAY_IP_PER_MINUTE=30
# Bundler selection for `npm run dev`. Set to 0 to fall back to webpack.
# Default is 1 (Turbopack). PR #4092 had forced webpack because earlier
# Turbopack 16.2.x panicked on the OmniRoute module graph with "internal error:
# entered unreachable code: there must be a path to a root"
# (turbopack-core/module_graph/mod.rs:662). That panic no longer reproduces on
# the pinned Next 16.2.9 — verified across a broad cold-compile sweep (36
# dashboard routes + open-sse-heavy API routes incl. /api/v1/chat/completions,
# /api/v1/models, /api/mcp) and repeated HMR rebuilds: zero panics. Turbopack
# also keeps dev memory far lower on the edit→rebuild loop (HMR rebuild RSS stays
# ~flat vs webpack's monotonic growth), which mitigates the dev-server OOM on
# this 60+ route app. The production build still uses webpack (build pipeline is
# unaffected by this dev-only flag).
# Use Turbopack in local dev. Next 16.2.4 can fail to compile next/font/google
# through the custom dev runner without this on Windows.
OMNIROUTE_USE_TURBOPACK=1
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
@@ -187,13 +150,8 @@ OMNIROUTE_USE_TURBOPACK=1
# Hostname/bind address for the Next.js server.
# Used by: scripts/dev/run-next.mjs (HOST), Playwright runner (HOSTNAME).
# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests).
# NOTE: Do NOT use `HOSTNAME` — it is a POSIX shell variable automatically set to
# the machine name by bash/zsh. The .env loader cannot override it (first-wins
# semantics). Use OMNIROUTE_SERVER_HOST instead for `omniroute serve`.
# See: https://github.com/diegosouzapw/OmniRoute/issues/6194
#HOST=0.0.0.0
#HOSTNAME=127.0.0.1
#OMNIROUTE_SERVER_HOST=0.0.0.0
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
# Values: production | development | Default: production
@@ -231,8 +189,7 @@ AUTH_COOKIE_SECURE=false
REQUIRE_API_KEY=false
# Allow revealing full API key values in the Dashboard UI.
# Used by: src/shared/constants/featureFlagDefinitions.ts — controls show/hide of key values.
# Also configurable from Dashboard > Settings > Feature Flags.
# Used by: Dashboard providers page — controls show/hide of key values.
# Default: false | Security risk if enabled on shared instances.
ALLOW_API_KEY_REVEAL=false
@@ -269,33 +226,10 @@ ALLOW_API_KEY_REVEAL=false
# Default: 10485760 (10 MB)
# MAX_BODY_SIZE_BYTES=10485760
# Heap-pressure-aware admission for POST /v1/chat/completions (#5152). A large
# coding-agent "compact" body amplifies into hundreds of MB of transient JS objects
# on the combo path; concurrent compacts can stack past the V8 heap ceiling and OOM
# the process. These shed a LARGE body with 503 (Retry-After) only while the heap is
# already under pressure — healthy heap admits every body untouched.
# Used by: src/shared/middleware/chatBodyAdmission.ts
# Bodies below this size skip the guard entirely (heap not even sampled). Default 262144 (256 KB).
# OMNIROUTE_CHAT_LARGE_BODY_BYTES=262144
# Hard cap — bodies above this are rejected with 413 before any clone/parse. Default 52428800 (50 MB).
# OMNIROUTE_CHAT_HARD_MAX_BODY_BYTES=52428800
# Shed large bodies once heapUsed/heap_size_limit reaches this ratio (0<r<1). Default 0.75.
# OMNIROUTE_CHAT_HEAP_SHED_RATIO=0.75
# Hard cap (bytes) for a non-streaming upstream response buffered fully into memory
# (#5152). Past this the upstream reader is cancelled and the request fails fast
# instead of growing an unbounded string until the V8 heap is exhausted.
# Used by: open-sse/handlers/chatCore/nonStreamingResponseBody.ts
# Default: 67108864 (64 MB)
# OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES=67108864
# CORS configuration — controls which cross-origin browser clients can call the API.
# Used by: src/server/cors/origins.ts — sets Access-Control-Allow-Origin.
# Same-origin dashboard requests behind a reverse proxy do not need CORS; they
# use session-bound CSRF protection. No wildcard is sent unless CORS_ALLOW_ALL=true.
# CORS_ALLOWED_ORIGINS=https://your-frontend.example.com
# CORS_ORIGIN=https://your-frontend.example.com # legacy single-origin alias
# CORS_ALLOW_ALL=false
# CORS configuration — controls which origins can call the API.
# Used by: Next.js middleware — sets Access-Control-Allow-Origin header.
# Default: * (all origins) | Restrict for production security.
# CORS_ORIGIN=https://your-domain.com
# Allow provider URLs pointing to private/local networks (localhost, 192.168.x.x, etc.).
# REQUIRED for self-hosted providers: LM Studio, Ollama, vLLM, Llamafile, Triton, etc.
@@ -303,12 +237,6 @@ ALLOW_API_KEY_REVEAL=false
# Default: false (blocked) | Set true to enable local providers.
# OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true
# Allow adding/validating providers on local/private addresses (127.0.0.1, localhost, LAN).
# Used by: src/shared/network/outboundUrlGuard.ts — scopes to the provider validation path and
# still blocks cloud-metadata (169.254.169.254, metadata.google.internal). Default: true
# (OmniRoute is local-first). Set false to enforce strict public-only blocking.
# OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS=false
# Legacy alias toggling the SSRF guard. Used by: src/shared/network/outboundUrlGuard.ts
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
# OUTBOUND_SSRF_GUARD_ENABLED=true
@@ -345,15 +273,6 @@ ALLOW_API_KEY_REVEAL=false
# PII_RESPONSE_SANITIZATION=false
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
# ── VS Code Tokenized-Route Context Sanitizer ──
# Strips implicit active-editor context (editorContext/activeEditor/currentFile/
# selection/openTabs…) from requests on the /v1/vscode/[token]/* routes before
# forwarding upstream, and redacts the content of explicitly-attached sensitive
# files (.env, private keys, kubeconfig, credentials/secrets). Explicit
# attachments otherwise pass through. Secure-by-default: ON unless set to 0.
# Used by: src/app/api/v1/vscode/contextSanitizer.ts
# OMNIROUTE_VSCODE_SANITIZE_CONTEXT=1 # set to 0 to disable
# ═══════════════════════════════════════════════════════════════════════════════
# 6. TOOL & ROUTING POLICIES
# ═══════════════════════════════════════════════════════════════════════════════
@@ -373,19 +292,12 @@ ALLOW_API_KEY_REVEAL=false
# Default: 5000 | Minimum: 1000
# OMNIROUTE_PAYLOAD_RULES_RELOAD_MS=5000
# Prefer Claude Code OAuth for unprefixed Claude-family model IDs such as
# claude-sonnet-4-6 or newly released IDs like claude-fable-5.
# Used by: open-sse/services/model.ts. Explicit provider prefixes still win.
# Default: false
# OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS=false
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
# ═══════════════════════════════════════════════════════════════════════════════
# URLs used for internal sync jobs, OAuth callbacks, and cloud relay.
# Internal base URL — used by server-side sync jobs to call /api/sync/cloud.
# Keep this as a loopback/container URL even when the app is publicly proxied.
# Used by: src/lib/cloudSync.ts, src/lib/initCloudSync.ts
# Default: http://localhost:20128
BASE_URL=http://localhost:20128
@@ -399,39 +311,27 @@ CLOUD_URL=
# Default: 12000 (12 seconds)
# CLOUD_SYNC_TIMEOUT_MS=12000
# Public-facing base URL — required for stable reverse proxy / OAuth callback setups.
# Used by: OAuth redirect_uri computation, Dashboard UI links, and generated public URLs.
# Set to your stable public URL when OAuth callbacks or generated browser links need a
# canonical host behind nginx/Caddy (e.g., https://omniroute.example.com).
# Public-facing base URL — CRITICAL for reverse proxy / OAuth callback setups.
# Used by: OAuth redirect_uri computation, Dashboard UI links, cloud/model sync.
# Set to your public URL when behind nginx/Caddy (e.g., https://omniroute.example.com).
#
# Dashboard display behavior: when this variable is unset, the dashboard
# auto-detects the base URL shown in curl examples and CLI tool snippets
# from window.location.origin (the host the user is browsing). Setting it
# explicitly is only required when running behind a reverse proxy with a
# different public hostname, or when OAuth callbacks / generated browser links must point
# to a canonical URL. Authenticated dashboard writes use same-origin requests plus
# session-bound CSRF protection and do not require a static public base URL.
# different public hostname, or when OAuth callbacks must point to a
# canonical URL.
#
# Default: http://localhost:20128
NEXT_PUBLIC_BASE_URL=http://localhost:20128
# Browser-facing OmniRoute origin for generated assets in API responses.
# Highest-priority public origin override; also used by non-dashboard public-origin validation.
# Used by: chatgpt-web image generation cache URLs (/v1/chatgpt-web/image/<id>).
# Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL
# but the user's browser must fetch images from a LAN, tunnel, or public origin.
# Do not include /v1; if included accidentally it will be normalized away.
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128
# Absolute provider plugin manifest URL advertised to sidecar clients.
# Used by: open-sse/config/providerPluginManifestUrl.ts. When unset, OmniRoute
# derives the URL from request origin or HOST/PORT using OMNIROUTE_PUBLIC_PROTOCOL.
# OMNIROUTE_PROVIDER_MANIFEST_URL=https://omniroute.example.com/api/v1/provider-plugin-manifest
# Protocol used when deriving provider plugin manifest URLs without a request origin.
# Used by: open-sse/config/providerPluginManifestUrl.ts. Defaults to http.
# OMNIROUTE_PUBLIC_PROTOCOL=http
# Max wait time for an async chatgpt-web image to land via the celsius
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
# upstream queue-deep windows ("Lots of people are creating images right now").
@@ -443,30 +343,12 @@ NEXT_PUBLIC_BASE_URL=http://localhost:20128
# is heavy and clients are racing the 30-minute TTL.
# OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB=256
# Overall wait budget for a chatgpt-web GPT-5.5 Pro background-poll handoff,
# in milliseconds. Default 1200000 (20 minutes). Pro reasoning runs are slow
# and complete out-of-band, so OmniRoute polls until the answer lands or this
# budget elapses. Raise it if Pro requests time out before finishing.
# OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS=1200000
# Interval between chatgpt-web GPT-5.5 Pro background-poll attempts, in
# milliseconds. Default 4000 (4 seconds). Lower for snappier completion at the
# cost of more upstream polling; raise to reduce request volume.
# OMNIROUTE_CGPT_WEB_PRO_POLL_INTERVAL_MS=4000
# Public cloud URL — client-side mirror of CLOUD_URL.
NEXT_PUBLIC_CLOUD_URL=
# Legacy alias — fallback for NEXT_PUBLIC_BASE_URL in sync schedulers.
# NEXT_PUBLIC_APP_URL=http://localhost:20128
# Advanced reverse-proxy trust mode for deriving public origin from Forwarded /
# X-Forwarded-* headers when no explicit public base URL is set. Prefer setting
# NEXT_PUBLIC_BASE_URL. Only enable if direct client access to OmniRoute is blocked
# and your proxy strips/rebuilds incoming forwarded headers.
# Values: true/loopback (trust loopback proxy peers), private/lan (also trust LAN peers).
# OMNIROUTE_TRUST_PROXY=
# Public callback URL for asynchronous image/audio jobs (kie.ai, etc.).
# Used by: open-sse/utils/kieTask.ts — overrides callbackUrlFromBaseUrl().
# Honor order: KIE_CALLBACK_URL → OMNIROUTE_KIE_CALLBACK_URL → OMNIROUTE_PUBLIC_URL.
@@ -474,35 +356,14 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_KIE_CALLBACK_URL=
#OMNIROUTE_PUBLIC_URL=
# Headroom token-saver proxy URL. The dashboard lifecycle (api/headroom/*) spawns
# a local headroom-ai CLI on loopback by default; override only to point at an
# external Docker sidecar proxy. Defaults to http://localhost:8787 when unset.
# Used by: src/lib/headroom/detect.ts.
#HEADROOM_URL=http://localhost:8787
# Upstream quota endpoints used by the Usage page. Override only for
# debugging or when routing through a corporate mirror. Used by:
# open-sse/services/usage.ts.
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
#OMNIROUTE_GEMINI_CLI_USAGE_URL=https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
# OpenCode Go dashboard quota scraping. Prefer configuring these per connection
# in Dashboard → Providers → OpenCode Go. Env vars are useful for headless
# deployments or shared server defaults. The cookie is sensitive.
#OPENCODE_GO_WORKSPACE_ID=wrk_...
#OMNIROUTE_OPENCODE_GO_WORKSPACE_ID=wrk_...
#OPENCODE_GO_AUTH_COOKIE=auth=...
#OMNIROUTE_OPENCODE_GO_AUTH_COOKIE=auth=...
# Ollama Cloud quota scraping. Prefer configuring this per connection in
# Dashboard → Providers → Ollama Cloud. The cookie is sensitive.
#OLLAMA_USAGE_COOKIE=__Secure-session=...
#OLLAMA_CLOUD_USAGE_COOKIE=__Secure-session=...
#OMNIROUTE_OLLAMA_USAGE_COOKIE=__Secure-session=...
# ═══════════════════════════════════════════════════════════════════════════════
# 8. OUTBOUND PROXY (Upstream Provider Calls)
@@ -521,26 +382,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# ALL_PROXY=socks5://127.0.0.1:7890
# NO_PROXY=localhost,127.0.0.1
# Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher.
# Long-lived SSE streams such as Codex /v1/responses need more than one
# connection when multiple requests share the same account-level proxy.
# Set to 1 only for legacy diagnostics. Values above 256 are capped.
# OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS=32
# SOCKS5 handshake (connect) timeout in ms (default 10000, capped at 120000).
# Raise it when a single residential gateway host is hit by high concurrency
# (e.g. 100 simultaneous requests): the real SOCKS5 handshake can exceed 10s
# under a saturated pool even though the proxy is reachable, which otherwise
# surfaces as a false "[Proxy Fast-Fail] Proxy unreachable".
# SOCKS_HANDSHAKE_TIMEOUT_MS=10000
# Proxy fail-open mode (default: false = fail-closed).
# When false, a request whose assigned proxy fails to resolve is REFUSED rather than
# falling back to a direct connection — prevents real-IP leaks in egress-controlled
# deployments. Set true to restore the legacy DIRECT fallback (legacy behaviour).
# Used by: src/sse/handlers/chatHelpers.ts
# PROXY_FAIL_OPEN=false
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js.
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google).
# Used by: open-sse/executors — replaces Node.js default TLS fingerprint.
@@ -569,14 +410,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Allow OmniRoute to write CLI config files (token refresh, etc.).
# CLI_ALLOW_CONFIG_WRITES=true
# Auto-sync CLI profile files after provider model discovery changes. OPT-IN, default OFF for
# both. When enabled, writes only the tool's profile files (~/.codex/*.config.toml or
# ~/.claude/profiles/<name>/settings.json); never changes the active/default config. Both also
# require CLI_ALLOW_CONFIG_WRITES (default on). Toggle from the CLI Code dashboard, or set here.
# Leave unset to disable. (Feature flags — a DB/dashboard override takes precedence over env.)
# OMNIROUTE_AUTO_SYNC_CODEX_PROFILES=true
# OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES=true
# Override binary paths for individual CLI tools.
# CLI_CLAUDE_BIN=claude
# CLI_CODEX_BIN=codex
@@ -587,14 +420,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# CLI_CONTINUE_BIN=cn
# CLI_QODER_BIN=qoder
# CLI_QWEN_BIN=qwen
# CLI_AUGGIE_BIN=auggie
# AUGGIE_BIN=auggie
# Override the Hermes Agent home directory (where OmniRoute reads/writes the
# Hermes CLI config). Matches the env var the Hermes PowerShell installer sets
# on Windows (%LOCALAPPDATA%\hermes); defaults to ~/.hermes when unset.
# Used by: src/lib/cli-helper/config-generator/hermesHome.ts
# HERMES_HOME=~/.hermes
# ═══════════════════════════════════════════════════════════════════════════════
# 10. INTERNAL AGENT & MCP INTEGRATIONS
@@ -616,11 +441,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Legacy alias for OMNIROUTE_API_KEY.
# ROUTER_API_KEY=
# CLI remote-mode context/profile for `omniroute` commands (overrides the active
# context in the local contexts store). Equivalent to the `--context <name>` flag.
# Used by: bin/cli/program.mjs, bin/cli/api.mjs (remote mode).
# OMNIROUTE_CONTEXT=
# Enforce scope-based access control on MCP tool calls.
# Used by: open-sse/mcp-server/server.ts — rejects calls outside allowed scopes.
# OMNIROUTE_MCP_ENFORCE_SCOPES=false
@@ -656,19 +476,6 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
# to opt out (restores fully concurrent fetches). Default: 1500
PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Min interval (ms) between consecutive UPSTREAM quota fetches on the per-request
# preflight/monitor path (e.g. Codex /wham/usage), complementing the bulk-sync
# spacing above. Many accounts on one IP fetching quota in the same second can look
# like automation to the upstream and get an OAuth token revoked (#6009). This gate
# serializes genuine network calls (cache hits are unaffected). Set to 0 to disable.
# Default: 250 (clamped 0..5000).
# OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS=250
# Delay (ms) before refreshing provider limits after a real usage event (e.g. a
# completed request). Gives the upstream quota API time to register the consumption
# before the dashboard polls. Default: 5000
#PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS=5000
# Disable all background services (sync, pricing, model refresh).
# Used by: src/instrumentation-node.ts, src/lib/initCloudSync.ts
# Useful for: CI builds, test environments, or resource-constrained containers.
@@ -679,25 +486,10 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# heuristic in instrumentation-node.ts. Default: unset (tests skip background).
#OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS=1
# Proactive connection-cooldown recovery (#8): re-validates connections whose
# transient `rate_limited_until` window has elapsed OUTSIDE the request hot path,
# so the first request after a cooldown does not pay the probe latency. Lazy
# recovery in getProviderCredentials still applies regardless. Used by:
# src/lib/quota/connectionRecovery.ts.
# Tick cadence (ms). Default 60000, floor 5000.
# OMNIROUTE_CONNECTION_RECOVERY_INTERVAL_MS=60000
# Disable the proactive recovery scheduler entirely (default: false).
# OMNIROUTE_DISABLE_CONNECTION_RECOVERY=false
# Background job interval for budget reset checks (ms). Default: 600000 (10m).
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
# Emergency budget-exhaustion fallback (set false or 0 to disable the reroute to
# nvidia/openai/gpt-oss-120b when a request fails with a 402 budget error).
# Used by: open-sse/services/emergencyFallback.ts. Default: enabled.
#OMNIROUTE_EMERGENCY_FALLBACK=true
# Reasoning cache cleanup cadence (ms). Default: 1800000 (30m). Floor: 60000.
# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts.
#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000
@@ -722,44 +514,14 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Default: <repo>/src/lib/db/migrations.
#OMNIROUTE_MIGRATIONS_DIR=
# Mass-pending-migrations safety threshold (#3416). If more than this many
# migrations are pending on an existing DB, startup aborts (a wiped tracking
# table could cause data loss). Raise it to restore an older backup; set to 0
# to disable the check. Used by: src/lib/db/migrationRunner.ts. Default: 50.
#OMNIROUTE_MAX_PENDING_MIGRATIONS=50
# Trust user-managed RTK project filter rules without strict signature checks.
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
# T02 stacked-pipeline engine circuit-breaker (OPT-IN, default off). When enabled, a compression
# engine that throws repeatedly across requests is skipped (fail-open) for a cooldown.
# Used by: open-sse/services/compression/pipelineEngineBreaker.ts.
#COMPRESSION_PIPELINE_BREAKER_ENABLED=false # master switch (default false)
#COMPRESSION_PIPELINE_BREAKER_THRESHOLD=3 # consecutive failures before the engine opens
#COMPRESSION_PIPELINE_BREAKER_COOLDOWN_MS=30000 # ms the engine stays skipped before a probe
# T08/H8 — CCR retrieval-feedback ramp factor. Each prior retrieval of a stored block raises its
# effective minChars linearly, so frequently-retrieved content is compressed progressively less
# (>= 3 retrievals = never compressed). 1 disables the ramp (binary skip at the threshold only).
# Used by: open-sse/services/compression/engines/ccr/index.ts. Default: 2.
#COMPRESSION_CCR_RETRIEVAL_RAMP_FACTOR=2
# T08/H5 — usage-observed prefix freeze (OPT-IN, default off). When enabled, a system prompt seen
# >= THRESHOLD times is treated as a stable cacheable prefix and preserved from compression even
# for providers the static cache-aware heuristic does not recognize (freeze = preserve, never
# mutates). Used by: open-sse/services/compression/prefixFreeze.ts.
#COMPRESSION_PREFIX_FREEZE_ENABLED=false # master switch (default false)
#COMPRESSION_PREFIX_FREEZE_THRESHOLD=3 # observations before a prefix is frozen
# Skip the postinstall native-runtime warm-up (useful in CI / headless installs). Default: 0.
# Used by: scripts/postinstall.mjs.
#OMNIROUTE_SKIP_POSTINSTALL=0
# Operator-supplied JSON credentials for the offline compression-eval CLI
# (parsed with JSON.parse; leave unset for a dry run). Developer tooling only.
# Used by: scripts/compression-eval/index.ts. Default: {} (empty).
#OMNIROUTE_EVAL_CREDENTIALS={}
# Skip the DB healthcheck entirely on startup (useful for short-lived tasks / tests).
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to disable. Default: 0.
#OMNIROUTE_SKIP_DB_HEALTHCHECK=0
@@ -828,7 +590,7 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
# THEOLDLLM_NAV_TIMEOUT_MS=30000
# ── Gemini / Antigravity / Windsurf (all Google-based) ──
# ── Gemini / Gemini CLI / Antigravity / Windsurf (all Google-based) ──
# These providers ship public OAuth client_id/secret values (or Firebase Web
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
# code via open-sse/utils/publicCreds.ts — leave the env vars unset to use
@@ -837,6 +599,8 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
#
# GEMINI_OAUTH_CLIENT_ID=
# GEMINI_OAUTH_CLIENT_SECRET=
# GEMINI_CLI_OAUTH_CLIENT_ID=
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
# ANTIGRAVITY_OAUTH_CLIENT_ID=
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
# WINDSURF_FIREBASE_API_KEY=
@@ -853,7 +617,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# ── GitLab Duo ──
# Register an OAuth app at: https://gitlab.com/-/profile/applications
# Set redirect URI to: http://localhost:20128/callback (or your NEXT_PUBLIC_BASE_URL + /callback)
# Required scopes: ai_features, read_user (matches GITLAB_DUO_CONFIG.scope in src/lib/oauth/constants/oauth.ts)
# Required scopes: api, read_user, openid, profile, email
# GITLAB_DUO_OAUTH_CLIENT_ID=***
# GITLAB_DUO_OAUTH_CLIENT_SECRET=*** # optional — PKCE flow does not require a secret
#
@@ -898,8 +662,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# QODER_PERSONAL_ACCESS_TOKEN=
# QODER_CLI_WORKSPACE=
# OMNIROUTE_QODER_WORKSPACE=
# Override the Qoder CLI config dir (isolated PAT session, avoids clobbering a browser login).
# QODER_CLI_CONFIG_DIR=
# ── Blackbox Web validated-token override (issue #2252) ──
# Used by: open-sse/executors/blackbox-web.ts. Blackbox `/api/chat` rejects
@@ -926,7 +688,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# VISION_BRIDGE_API_KEY=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity) & OTHER PROVIDERS — REMOTE SERVERS
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) & OTHER PROVIDERS — REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────
# The default Client IDs above ONLY work when OmniRoute runs on localhost.
# For remote/VPS hosting (including Docker containers on remote servers):
@@ -954,7 +716,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
CLAUDE_USER_AGENT="claude-cli/2.1.158 (external, cli)"
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
@@ -963,34 +725,23 @@ CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
# forward the original names verbatim (debugging only).
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
# KIRO_VERIFY_FULL_CRC=false # opt-in: full per-frame message CRC validation on the Kiro event stream (debug corrupted streams; prelude CRC + TLS already protect framing)
# Optional override for the Kiro social device-code OAuth clientId. Kiro's
# device endpoint accepts any non-empty string and behaves like a User-Agent
# rather than a secret. Only override if AWS ever starts enforcing this field.
# Used by: src/lib/oauth/constants/oauth.ts (KIRO_CONFIG.socialClientId).
# KIRO_OAUTH_CLIENT_ID=kiro-cli
# Enable full per-frame message CRC validation for Kiro streams. Off by default
# because it is O(frame bytes) on the main thread; use only for debugging
# suspected corrupted-stream issues.
# Used by: open-sse/executors/kiro.ts
# KIRO_VERIFY_FULL_CRC=false
QODER_USER_AGENT="Qoder-Cli"
QWEN_USER_AGENT="QwenCode/0.19.3 (linux; x64)"
QWEN_USER_AGENT="QwenCode/0.15.11 (linux; x64)"
CURSOR_USER_AGENT="Cursor/3.4"
GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
# Override Codex client version sent in headers independently of the
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.142.0
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
# from the Codex Responses stream. These frames break the OpenAI SDK's
# responses.stream() with a 502 "Controller is already closed". Off by default;
# set to true/1/yes to enable. Used by: open-sse/executors/codex.ts.
# OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS=true
# CODEX_CLIENT_VERSION=0.132.0
# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
@@ -1049,8 +800,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Used by: src/shared/utils/runtimeTimeouts.ts — centralized timeout resolution.
#
# 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.
# If set, it becomes the default for FETCH_TIMEOUT_MS and STREAM_IDLE_TIMEOUT_MS.
# The fine-grained variables below override their respective defaults only when set.
# ── Global shortcut ──
@@ -1068,22 +818,12 @@ CURSOR_USER_AGENT="Cursor/3.4"
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000
# ── Firecrawl web-fetch executor ──
# Point at a self-hosted Firecrawl instance (defaults to the public cloud API).
# When set to a non-cloud base URL, the API key becomes optional.
# FIRECRAWL_BASE_URL=https://api.firecrawl.dev
# FIRECRAWL_TIMEOUT_MS=30000 # Per-request timeout (default: 30000 = 30s)
# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ──
# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
# layered on top of it when the native library is wedged.
# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000
# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000
# Max wait for the FIRST streamed byte from the ChatGPT TLS sidecar before the
# request is aborted as a dead stream, in milliseconds. Default 30000 (30s).
# Raise it if upstream cold-starts routinely exceed the window.
# OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS=30000
# ── Claude TLS sidecar (Chromium-fingerprinted client) ──
# Used by: open-sse/services/claudeTlsClient.ts — wire-level timeout for
@@ -1127,30 +867,9 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD=2
# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS=15000
# ── Context-cache pin health gate ──
# Used by: open-sse/services/combo.ts. When a context-cache pin points at a
# provider that is durably unhealthy, the pin is dropped to allow failover.
# PIN_DROP_BACKOFF_LEVEL gates how deep a connection's backoff must be before the
# pin is considered durably unhealthy; PIN_DROP_GRACE_MS is the anti-flap window
# that tolerates brief transient cooldowns before dropping the pin.
# PIN_DROP_BACKOFF_LEVEL=2
# PIN_DROP_GRACE_MS=20000
# ── Stream idle detection ──
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
# # Extended-thinking models rarely pause >90s.
# 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).
# OMNIROUTE_AGENT_GOAL_POLICY_ENABLED=true # Kill-switch for the /goal heuristic below.
# # Set to false to fully disable detection —
# # readiness timeouts and stream recovery are
# # never elevated by request body/headers when off.
# OMNIROUTE_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS=600000 # Auto cap for detected /goal agent runs
# OMNIROUTE_AGENT_GOAL_STREAM_RECOVERY=true # Auto early stream recovery for /goal runs.
# # NOTE: this can only ADD recovery on top of the
# # operator default — it never overrides an explicit
# # STREAM_RECOVERY_ENABLED / DB settings opt-out.
# ── TLS client (wreq-js fingerprint proxy) ──
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
@@ -1219,11 +938,6 @@ APP_LOG_TO_FILE=true
# Default: 100000
# CALL_LOGS_TABLE_MAX_ROWS=100000
# Maximum age for orphaned active request log entries before the in-memory
# pending-request reaper removes them. Accepts milliseconds.
# Default: 3600000 (1 hour)
# MAX_PENDING_REQUEST_AGE_MS=3600000
# Whether call log pipeline capture stores stream chunks when enabled in settings.
# Only applies when call_log_pipeline_enabled=true.
# Default: true
@@ -1340,19 +1054,6 @@ APP_LOG_TO_FILE=true
# Comma-separated data sources. Default: litellm
# PRICING_SYNC_SOURCES=litellm
# ═══════════════════════════════════════════════════════════════════════════════
# 18b. ARENA ELO SYNC
# ═══════════════════════════════════════════════════════════════════════════════
# Auto-update model intelligence from Arena AI leaderboard ELO scores (powers the
# Free Provider Rankings page). ON by default — fetches from api.wulong.dev on startup
# (non-blocking, never fatal). Set to false to opt out of the outbound sync.
# Also configurable from Dashboard > Settings > Feature Flags.
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/lib/arenaEloSync.ts
# ARENA_ELO_SYNC_ENABLED=true
# Sync interval in seconds. Default: 86400 (24 hours).
# ARENA_ELO_SYNC_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 19. MODEL SYNC (Dev)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1361,13 +1062,6 @@ APP_LOG_TO_FILE=true
# Default: 86400 (24 hours)
# MODELS_DEV_SYNC_INTERVAL=86400
# Self-correcting context-window reconciler interval in seconds (feature 5004).
# Pins provider-declared windows from /models discovery as auto:discovery overrides
# when they diverge from the catalog. Set to 0 to disable. Never overwrites manual overrides.
# Used by: src/lib/contextWindowResolver.ts
# Default: 86400 (24 hours)
# CONTEXT_WINDOW_RECONCILE_INTERVAL=86400
# ═══════════════════════════════════════════════════════════════════════════════
# 20. PROVIDER-SPECIFIC SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1404,34 +1098,6 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/executors/cloudflare-ai.ts
# CLOUDFLARE_ACCOUNT_ID=
# ── Deno Deploy proxy relay (#4643 / 9router#1437) ──
# Override the Deno Deploy REST API base used by the proxy-pool relay deployer.
# Default: https://api.deno.com/v2 (omit unless mocking).
# Used by: src/app/api/settings/proxy/deno-deploy/route.ts
# DENO_DEPLOY_API_BASE=https://api.deno.com/v2
# Default Deno Deploy app name suggested in the "Deploy Relay" modal.
# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/DenoRelayModal.tsx
# NEXT_PUBLIC_DENO_RELAY_DEFAULT_PROJECT=omniroute-deno-relay
# Set to "false" to hide the Deno Deploy relay option from the Proxy Pool tab.
# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/ProxyPoolTab.tsx
# NEXT_PUBLIC_DENO_RELAY_ENABLED=true
# ── Cloudflare Workers proxy relay (#4640 / 9router#1360) ──
# Override the Cloudflare REST API base used by the proxy-pool relay deployer.
# Default: https://api.cloudflare.com/client/v4 (omit unless mocking).
# Used by: src/app/api/settings/proxy/cloudflare-deploy/route.ts
# CLOUDFLARE_API_BASE=https://api.cloudflare.com/client/v4
# Default worker project name suggested in the "Deploy Relay" modal.
# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/CloudflareRelayModal.tsx
# NEXT_PUBLIC_CLOUDFLARE_RELAY_DEFAULT_PROJECT=omniroute-relay
# Set to "false" to hide the Cloudflare Workers relay option from the Proxy Pool tab.
# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/ProxyPoolTab.tsx
# NEXT_PUBLIC_CLOUDFLARE_RELAY_ENABLED=true
# ── Cloudflare Tunnel (cloudflared) ──
# Custom path to cloudflared binary for tunnel management.
# Used by: src/lib/cloudflaredTunnel.ts
@@ -1479,13 +1145,6 @@ APP_LOG_TO_FILE=true
# CLIPROXYAPI_PORT=5544
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
# ── Mux embedded service ──
# Override the port where the embedded Mux (coder/mux) agent-orchestration
# daemon listens. Always bound to 127.0.0.1 — never configurable to 0.0.0.0.
# Rarely needed — defaults to 8322.
# Used by: src/lib/services/bootstrap.ts, src/app/api/services/mux/_lib.ts
# MUX_SERVICE_PORT=8322
# ── Local hostnames (Docker networking) ──
# Comma-separated additional hostnames treated as "local" for provider routing.
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
@@ -1503,30 +1162,6 @@ APP_LOG_TO_FILE=true
# Health check result cache TTL (ms). Default: 30000 (30s)
# PROXY_HEALTH_CACHE_TTL_MS=30000
# Unhealthy health check result cache TTL (ms). Default: 2000 (2s)
# Keeps transient fast-fail timeouts from poisoning a proxy for the full
# healthy-result cache window under high concurrency.
# PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS=2000
# Background proxy health scheduler (src/lib/proxyHealth/scheduler.ts).
# Periodically probes every registered proxy and (optionally) removes dead ones.
# Set "false" to disable the scheduler entirely. Default: enabled.
# PROXY_HEALTH_ENABLED=true
# Sweep interval in ms (minimum 60000). Default: 600000 (10min).
# PROXY_HEALTH_INTERVAL_MS=600000
# Reachability probe target for the scheduler and the auto-test endpoint.
# Point it at an internal/self-hosted URL to avoid the public default.
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
# PROXY_AUTO_REMOVE_AFTER=3
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags.
# OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK=false
# Rate limit maximum wait time before failing a request (ms). Default: 120000 (2 min)
# Used by: open-sse/services/rateLimitManager.ts
# RATE_LIMIT_MAX_WAIT_MS=120000
@@ -1536,44 +1171,6 @@ APP_LOG_TO_FILE=true
# Accepted values: true|1|on (force on), false|0|off (force off), unset (use Dashboard).
# RATE_LIMIT_AUTO_ENABLE=
# Provider cooldown tracking: minimum time (ms) before a failed provider/connection
# can be retried. Prevents subsequent requests from re-walking failing providers.
# Scaled exponentially: minCooldown * 2^(failures-1), capped at maxRetryCooldownMs.
# Used by: open-sse/services/providerCooldownTracker.ts
# PROVIDER_COOLDOWN_MIN_MS=5000
# Provider cooldown tracking: maximum time (ms) before a failed provider/connection
# is retried regardless. Hard cap to prevent providers from being skipped indefinitely.
# Used by: open-sse/services/providerCooldownTracker.ts
# PROVIDER_COOLDOWN_MAX_MS=300000
# Enable/disable global provider cooldown tracking. Opt-in: this global
# cross-request cooldown overlaps the existing Connection Cooldown / Provider
# Circuit Breaker layers, so it is OFF by default. When disabled, only the
# existing per-request/per-connection cooldown state is used (previous behavior).
# Used by: open-sse/services/providerCooldownTracker.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# PROVIDER_COOLDOWN_ENABLED=true
# Transparent stream recovery (free-claude-code port). When enabled, the opening SSE
# window is briefly held (up to STREAM_RECOVERY.HOLDBACK_MS) so an upstream truncation
# before any byte reaches the client can be retried invisibly. Opt-in: holding the
# window adds up to that much time-to-first-token latency on every stream, so it is
# OFF by default. Seeds ResilienceSettings.streamRecovery.enabled.
# Used by: open-sse/services/streamRecovery.ts, open-sse/handlers/chatCore.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# STREAM_RECOVERY_ENABLED=true
# Mid-stream continuation (Fase 4.4): when an upstream stream truncates AFTER bytes
# already reached the client, re-request with the partial text as an assistant prefill
# and stitch the missing suffix (plain-text OpenAI-compatible streams only; never with a
# tool call in flight). OFF by default — the recovered tail arrives as one burst, not
# token-by-token. Independent of STREAM_RECOVERY_ENABLED (different risk profile).
# Seeds ResilienceSettings.streamRecovery.continueMidStream.
# Used by: open-sse/services/streamRecovery.ts, open-sse/handlers/chatCore.ts
# Accepted values: true|1|on (enable). Unset or anything else = disabled (default).
# STREAM_RECOVERY_MIDSTREAM_ENABLED=true
# Stagger interval (ms) between provider token healthchecks at startup.
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
# HEALTHCHECK_STAGGER_MS=3000
@@ -1697,12 +1294,6 @@ APP_LOG_TO_FILE=true
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
# MITM_LOCAL_PORT=443
# MITM_DISABLE_TLS_VERIFY=0
# Idle socket timeout (ms) for proxied connections; sockets idle past this are torn
# down to avoid leaking half-open tunnels (src/mitm/socketTimeouts.ts, server.cjs).
# MITM_IDLE_TIMEOUT_MS=60000
# Routing-decision log verbosity: 0 silences, higher values log more bypass/route
# decisions (src/mitm/server.cjs, _internal/bypass.cjs).
# MITM_VERBOSE=1
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
@@ -1733,15 +1324,6 @@ APP_LOG_TO_FILE=true
# FREE_PROXY_IPLOCATE_ENABLED=false
# FREE_PROXY_IPLOCATE_BASE_URL=https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols
# ── Free Proxy Pool (Webshare source) ──
# Used by: src/lib/freeProxyProviders/webshare.ts
# Paid, per-account proxy list — requires FREE_PROXY_WEBSHARE_API_KEY to activate,
# regardless of FREE_PROXY_WEBSHARE_ENABLED.
# FREE_PROXY_WEBSHARE_ENABLED=true
# FREE_PROXY_WEBSHARE_API_KEY=
# FREE_PROXY_WEBSHARE_API_URL=https://proxy.webshare.io/api/v2/proxy/list/
# FREE_PROXY_WEBSHARE_MAX=500
# ── Vercel Relay ──
# Used by: src/app/api/settings/proxy/vercel-deploy/route.ts
# Hides the "Deploy Relay" button when set to false.
@@ -1755,10 +1337,6 @@ APP_LOG_TO_FILE=true
# dashboard's tunnel manager. Used by: src/lib/tailscaleTunnel.ts.
# TAILSCALE_BIN=/usr/local/bin/tailscale
# TAILSCALED_BIN=/usr/local/bin/tailscaled
# Pre-shared Tailscale auth key for non-interactive / headless `tailscale up`
# (passed via --auth-key=). When unset, login falls back to the interactive
# browser auth URL. Used by: src/lib/tailscaleTunnel.ts.
# TAILSCALE_AUTHKEY=
# ── Ngrok tunnel ──
# Used by: src/lib/ngrokTunnel.ts — authenticates outbound tunnels.
@@ -1884,11 +1462,6 @@ PLAYGROUND_COMPARE_MAX_COLUMNS=4
# MEMORY_VEC_TOP_K=20 # default top-K for vector search
# MEMORY_RRF_K=60 # RRF k constant (sqlite-vec hybrid recipe)
# HF_HUB_ENDPOINT=https://huggingface.co # override Hugging Face Hub base URL for static potion downloads
# TV6 typed memory decay (OPT-IN, default off — the sweep DELETES decayed memories)
# MEMORY_TYPED_DECAY_ENABLED=false # master switch for the destructive sweep (default off)
# MEMORY_TYPED_DECAY_EPISODIC_DAYS=30 # episodic TTL in days; 0 = episodic immune too
# MEMORY_TYPED_DECAY_ACCESS_IMMUNITY=3 # access_count >= N → immune; 0 disables access immunity
# MEMORY_TYPED_DECAY_SWEEP_INTERVAL=0 # periodic sweep interval (seconds); 0 = no periodic sweep
# AgentBridge + Traffic Inspector (Group A)
# AgentBridge
@@ -1909,17 +1482,7 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
# STATUS_SOFT_DEPRIORITIZE_FACTOR=0.5 # 0..1; multiplicador do score p/ provider esgotado (credits_exhausted/rate_limited) quando preflight cutoff OFF (#4540)
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
# QUOTA_PREFLIGHT_CUTOFF_ENABLED=false # opt-in (default OFF): hard quota cutoff drops low-quota candidates before auto-routing scoring
# ─── Auto-Combo tier filter (#4517) ───────────────────────────────────────
# When an `auto/<category>:free` (or any `:<tier>`) request matches NO connected
# candidates, OmniRoute returns an EMPTY pool by default — so `:free` really means
# "free tier only" and a paid model is never picked just because no free provider is
# connected. Set this to `true`/`1` to restore the legacy behavior of falling back to
# the full (unfiltered) pool with a warning. Source: open-sse/services/autoCombo/virtualFactory.ts
# OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=false
# ─── OpenCode config regeneration (scripts/ad-hoc/regen-opencode-config.ts) ───
# Base URL of the OmniRoute instance to query for /v1/models when regenerating
@@ -1933,107 +1496,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# scripts/ad-hoc/regen-opencode-config.ts. Falls back to OMNIROUTE_KEY.
# OPENCODE_API_KEY=
# ─── Bifrost Go sidecar (PR-4 in #3932) ──────────────────────────────────────
# Master kill switch for the bifrost sidecar proxy. When set to 0, the
# /api/v1/relay/chat/completions/bifrost route returns 503 with the
# X-Bifrost-Killswitch header and the operator is bounced to the TS path.
# Use this to disable the sidecar without redeploying (e.g. during a
# tier-1 router incident or a key rotation). Default: 1 (sidecar active).
# BIFROST_ENABLED=1
# When BIFROST_BASE_URL is set, /api/v1/relay/chat/completions/bifrost routes
# traffic to the Go gateway instead of the TS relay handler, removing TS from
# the hot path. Auth/rate-limit/injection-guard stay in the route (security not
# duplicated). Falls back to TS path via X-Bifrost-Fallback header on
# timeout/failure. See bin/omniroute for the local-redis companion.
# BIFROST_BASE_URL=
# Port the supervised Bifrost embedded service binds to (127.0.0.1:<port>), read by
# src/lib/services/bootstrap.ts when OmniRoute manages the Bifrost sidecar lifecycle.
# Default: 8080.
# BIFROST_PORT=8080
# API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
# unset, the route expects the request to carry a valid OmniRoute API key;
# this key is for gateway-side auth only.
# BIFROST_API_KEY=
# When true, the Bifrost sidecar route streams responses back via SSE through
# the gateway rather than the TS streaming executor. Default: true (when
# BIFROST_BASE_URL is set).
# BIFROST_STREAMING_ENABLED=
# Per-request timeout when proxying to the Bifrost gateway. Default: 30000 (30s).
# BIFROST_TIMEOUT_MS=
# Alias for BIFROST_API_KEY (used by scripts that read the env via
# OMNIROUTE_*). BIFROST_API_KEY takes precedence when both are set.
# OMNIROUTE_BIFROST_KEY=
# Relay backend selection for the OpenAI-compatible relay endpoint:
# ts | bifrost | auto. "ts" (default when Bifrost is not configured) uses the
# TypeScript relay; "auto" selects Bifrost when BIFROST_BASE_URL is set (and
# BIFROST_ENABLED != 0) and falls back to TS if the sidecar is unreachable;
# "bifrost" forces Bifrost (strict — no TS fallback). Auth, rate limits,
# injection guard and model allowlists always run in the Next route first.
# RELAY_ROUTING_BACKEND is an accepted alias. Responses carry X-Routing-Backend
# and X-Routing-Fallback.
# OMNIROUTE_RELAY_BACKEND=
# RELAY_ROUTING_BACKEND=
# Cooldown (ms) after a Bifrost sidecar hop fails in "auto" mode before the relay
# re-attempts the sidecar; it goes straight to the TS path while the cooldown lasts.
# 0 disables. Default 5000. Only applies when OMNIROUTE_RELAY_BACKEND=auto.
# OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS=
# Opt-in native HTTPS/TLS for `omniroute serve` (equivalent to --tls-cert /
# --tls-key). Provide BOTH a PEM certificate and its private key and the
# standalone server terminates TLS on the same listener (wss:// works
# unchanged). With neither set the server stays plain HTTP; providing only one
# (or an unreadable path) logs a warning and stays HTTP (never half-enables).
# OMNIROUTE_TLS_CERT=
# OMNIROUTE_TLS_KEY=
# ─── 1-click local service launchers (PR-3 in #3932) ────────────────────────
# Master switch for /api/local/* routes. When unset or "0", all /api/local/*
# routes return 503 in production. Default: 0. Must be "1" in non-loopback
# deploys to enable the Redis launcher and similar 1-click local service
# starters. Belt-and-suspenders with the isLocalOnlyPath() route-guard
# classification (LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts).
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=
# Bearer token for /api/local/* callers that aren't on loopback (e.g. the
# desktop app). When set, requests from non-loopback IPs must carry
# Authorization: Bearer <token>. Required when
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1 in non-loopback deployments. Default:
# unset (loopback-only).
# OMNIROUTE_LOCAL_ENDPOINTS_TOKEN=
# Container name for the 1-click Redis launcher (`omniroute redis up`).
# Default: omniroute-redis. Used by bin/cli/commands/redis.mjs and the
# RedisLauncherPanel.
# OMNIROUTE_REDIS_CONTAINER_NAME=
# Host port for the 1-click Redis launcher. Default: 6379. Bump if the host
# already binds 6379. The container's internal port stays 6379.
# OMNIROUTE_REDIS_HOST_PORT=
# Redis image used by the 1-click Redis launcher. Default: redis:7-alpine.
# Override to redis:8-alpine or a private registry mirror as needed.
# OMNIROUTE_REDIS_IMAGE=
# ── Cluster Profile: Qdrant Vector Memory (opt-in via `docker compose --profile memory up`) ──
# Qdrant is an OPTIONAL sidecar for deployments that need cosine-distance vector
# search at >1M embeddings. The default vector store is sqlite-vec
# (src/lib/memory/vectorStore.ts:108); flip this profile on only if you hit the
# sqlite-vec ceiling or want persistent cross-replica vector state. See
# docs/architecture/cluster-decisions.md § "Qdrant (memory profile)".
# QDRANT_HOST=qdrant
# QDRANT_PORT=6333
# QDRANT_GRPC_PORT=6334
# QDRANT_API_KEY=
# QDRANT_COLLECTION=omniroute-memory
# QDRANT_EMBEDDING_MODEL=text-embedding-3-small
# QDRANT_VECTOR_SIZE=1536
# QDRANT_HNSW_EF_CONSTRUCT=128
# ── Cluster Profile: Bifrost Tier-1 Router (opt-in via `docker compose --profile bifrost up`) ──
# Bifrost is an OPTIONAL Go-based Tier-1 router that handles the upstream-provider
# multiplexing layer. Default: OmniRoute's open-sse/executors/bifrost.ts in-process
# executor handles routing directly. Flip this profile on only if you want the
# gateway as a separate sidecar (helps in 3+ replica deployments where you want
# provider rotation centralised). See docs/architecture/cluster-decisions.md §
# "Bifrost (bifrost profile)".
# Set OMNIROUTE_RELAY_BACKEND=auto to use this sidecar when healthy, or
# OMNIROUTE_RELAY_BACKEND=bifrost to require it without TS fallback.
# BIFROST_BASE_URL=http://bifrost:8080
# BIFROST_API_KEY=
# BIFROST_STREAMING_ENABLED=true
# BIFROST_TIMEOUT_MS=30000

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +0,0 @@
# Funding links for OmniRoute — rendered as the "Sponsor" button on GitHub.
# Docs: https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
github: diegosouzapw
# Additional platforms (uncomment and fill in before enabling):
# custom: ["https://omniroute.online/donate"]

View File

@@ -1,29 +0,0 @@
name: npm ci with retry
description: Run npm ci with retries for transient registry/network failures.
runs:
using: composite
steps:
- shell: bash
run: |
set -euo pipefail
max_attempts=3
delay_seconds=20
for attempt in $(seq 1 "$max_attempts"); do
if [ "$attempt" -gt 1 ]; then
echo "npm ci attempt $attempt/$max_attempts after transient failure"
fi
if npm ci; then
exit 0
fi
exit_code=$?
if [ "$attempt" -eq "$max_attempts" ]; then
exit "$exit_code"
fi
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
done

View File

@@ -24,20 +24,6 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
# jscpd v5 is a Rust rewrite (native binary, no Node.js programmatic API).
# scripts/check/check-duplication.mjs is deliberately pinned to jscpd@4 (it
# parses jscpd-report.json against a frozen baseline). A v5 major would break
# the duplication gate — migrate the gate intentionally, not via dependabot.
- dependency-name: "jscpd"
update-types: ["version-update:semver-major"]
# @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
# It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
# compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)
# and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts),
# and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must
# be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore
# every version). Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -1,121 +0,0 @@
name: VPS Full Release Benchmark
# Mede TODAS as fases pesadas do release na VPS self-hosted, para dimensionar o ganho
# real end-to-end (não só unit+build). Cobre: unit shards, vitest, build web, build:cli,
# e2e (Playwright) e integration — as mesmas fases do pipeline de release.
#
# push na branch chore/vps-runner-benchmark -> roda na VPS (self-hosted)
# workflow_dispatch (target=github) -> ubuntu-latest (baseline)
#
# Segurança: label omni-release só existe aqui; fork nunca usa a VPS.
on:
workflow_dispatch:
inputs:
target:
description: "Onde rodar (vps = self-hosted, github = ubuntu-latest)"
type: choice
options: [vps, github]
default: vps
permissions:
contents: read
concurrency:
group: vps-benchmark-full-${{ github.event.inputs.target || 'vps' }}
cancel-in-progress: true
env:
RUNS_ON_JSON: >-
${{ (github.event.inputs.target == 'github') && '"ubuntu-latest"'
|| '["self-hosted","omni-release"]' }}
jobs:
unit:
name: full-unit ${{ matrix.shard }}
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
strategy: { fail-fast: false, matrix: { shard: [1, 2, 3, 4, 5, 6, 7, 8] } }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- name: unit shard (timed)
id: t
env: { TEST_SHARD: "${{ matrix.shard }}/8" }
run: |
s=$(date +%s); npm run test:unit:ci:shard || true; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| unit ${{ matrix.shard }}/8 | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"
vitest:
name: full-vitest
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- name: vitest (timed)
id: t
run: |
s=$(date +%s); npm run test:vitest || true; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| vitest | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"
build-web:
name: full-build-web
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- name: build web (timed)
id: t
run: |
s=$(date +%s); npm run build; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| build-web | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"
build-cli:
name: full-build-cli
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- name: build cli (timed)
id: t
run: |
s=$(date +%s); npm run build:cli || true; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| build-cli | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"
e2e:
name: full-e2e ${{ matrix.shard }}
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
strategy: { fail-fast: false, matrix: { shard: [1, 2, 3] } }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: e2e shard (timed, builds on demand)
id: t
run: |
s=$(date +%s); npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/3 || true; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| e2e ${{ matrix.shard }}/3 | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"
integration:
name: full-integration ${{ matrix.shard }}
runs-on: ${{ fromJSON(github.event.inputs.target == 'github' && '"ubuntu-latest"' || '["self-hosted","omni-release"]') }}
strategy: { fail-fast: false, matrix: { shard: [1, 2] } }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with: { node-version: "24" }
- run: npm ci
- name: integration shard (timed)
id: t
run: |
s=$(date +%s); npm run test:integration -- --test-shard=${{ matrix.shard }}/2 || true; echo "sec=$(( $(date +%s)-s ))" >> "$GITHUB_OUTPUT"
- run: echo "| integration ${{ matrix.shard }}/2 | \`${RUNNER_NAME:-github}\` | ${{ steps.t.outputs.sec }}s |" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,91 +0,0 @@
name: VPS Runner Benchmark
# Mede o tempo das fases pesadas do release (unit shards + build) para comparar
# o runner self-hosted da VPS (label omni-release) com o GitHub-hosted ubuntu-latest.
#
# - push na branch chore/vps-runner-benchmark -> roda na VPS (self-hosted)
# - workflow_dispatch (target=github) -> roda no ubuntu-latest (baseline)
#
# Segurança: o label omni-release só existe aqui; PRs de fork NUNCA usam a VPS.
on:
push:
branches: [chore/vps-runner-benchmark]
workflow_dispatch:
inputs:
target:
description: "Onde rodar (vps = self-hosted, github = ubuntu-latest)"
type: choice
options: [vps, github]
default: vps
permissions:
contents: read
concurrency:
group: vps-benchmark-${{ github.event.inputs.target || 'vps' }}
cancel-in-progress: true
jobs:
unit:
name: bench-unit shard ${{ matrix.shard }}
runs-on: >-
${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'github')
&& 'ubuntu-latest'
|| fromJSON('["self-hosted","omni-release"]') }}
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: "24"
- name: npm ci (timed)
id: install
run: |
start=$(date +%s)
npm ci
echo "seconds=$(( $(date +%s) - start ))" >> "$GITHUB_OUTPUT"
- name: unit shard ${{ matrix.shard }}/8 (timed)
id: shard
env:
TEST_SHARD: ${{ matrix.shard }}/8
run: |
start=$(date +%s)
npm run test:unit:ci:shard || true
echo "seconds=$(( $(date +%s) - start ))" >> "$GITHUB_OUTPUT"
- name: summary line
run: |
echo "| unit ${{ matrix.shard }}/8 | \`${RUNNER_NAME:-github}\` | ${{ steps.install.outputs.seconds }}s | ${{ steps.shard.outputs.seconds }}s |" >> "$GITHUB_STEP_SUMMARY"
build:
name: bench-build
runs-on: >-
${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'github')
&& 'ubuntu-latest'
|| fromJSON('["self-hosted","omni-release"]') }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: "24"
- name: npm ci (timed)
id: install
run: |
start=$(date +%s)
npm ci
echo "seconds=$(( $(date +%s) - start ))" >> "$GITHUB_OUTPUT"
- name: build (timed)
id: build
run: |
start=$(date +%s)
npm run build
echo "seconds=$(( $(date +%s) - start ))" >> "$GITHUB_OUTPUT"
- name: summary line
run: |
echo "## Benchmark — \`${RUNNER_NAME:-github}\`" >> "$GITHUB_STEP_SUMMARY"
echo "| fase | runner | npm ci | tempo |" >> "$GITHUB_STEP_SUMMARY"
echo "| --- | --- | --- | --- |" >> "$GITHUB_STEP_SUMMARY"
echo "| build | \`${RUNNER_NAME:-github}\` | ${{ steps.install.outputs.seconds }}s | ${{ steps.build.outputs.seconds }}s |" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -7,10 +7,9 @@ on:
- "v*"
workflow_dispatch:
# Least-privilege default: read-only at the top level; the build job that pushes to
# GHCR grants packages: write itself (Scorecard TokenPermissions).
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
@@ -20,14 +19,9 @@ jobs:
name: Build and Push Fork Image
if: github.repository == 'kang-heewon/OmniRoute'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,6 @@ on:
pull_request_review:
types: [submitted]
# Least-privilege default: no token permissions at the top level; the `claude` job
# grants exactly what it needs below (Scorecard TokenPermissions).
permissions: {}
jobs:
claude:
if: |
@@ -30,9 +26,8 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 1
- name: Run Claude Code

View File

@@ -1,31 +0,0 @@
name: CodeQL
# OWNER ACTION REQUIRED before enabling auto-triggers: advanced CodeQL conflicts with
# GitHub "default setup" — the analyze step fails with "CodeQL analyses from advanced
# configurations cannot be processed when the default setup is enabled". Switch repo
# Settings → Code security → CodeQL from Default to Advanced, THEN restore the
# push/pull_request/schedule triggers below. Until then this only runs on manual dispatch
# so it never produces a red check on PRs. (The codeqlAlerts ratchet keeps working via the
# default setup's alerts in the meantime.)
on:
workflow_dispatch:
permissions:
contents: read
jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
category: "/language:javascript-typescript"

View File

@@ -1,62 +0,0 @@
name: DAST smoke (PR)
on:
pull_request:
branches: ["main", "release/**"]
permissions:
contents: read
jobs:
dast-smoke:
runs-on: ubuntu-latest
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
continue-on-error: true
timeout-minutes: 12
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
run: npm run build:cli
- name: Start OmniRoute
env:
PORT: "20128"
INJECTION_GUARD_MODE: block
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for _ in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- run: pip install schemathesis
- name: Schemathesis smoke (high-risk endpoints, blocking)
run: |
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
--request-timeout 20 --suppress-health-check all --no-color
- name: promptfoo injection-guard (blocking)
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: dast-smoke-logs
path: server.log
retention-days: 7

View File

@@ -25,10 +25,9 @@ on:
type: boolean
default: false
# Least-privilege default: read-only at the top level; the build and merge jobs that
# push to GHCR grant packages: write themselves (Scorecard TokenPermissions).
permissions:
contents: read
packages: write
jobs:
prepare:
@@ -42,9 +41,8 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
# Need full tag history for semver comparison when deciding :latest.
fetch-depth: 0
@@ -98,13 +96,11 @@ jobs:
PROMOTE="${PROMOTE_INPUT:-false}"
else
git fetch --tags --quiet || true
# Decide via the extracted helper, which folds VERSION into the
# candidate set so the result is independent of git-tag sync timing
# on `release` events (#5301). Without that, the freshly-created tag
# is often not yet visible here and :latest stays a release behind.
PROMOTE=$(git tag -l 'v[0-9]*' | bash scripts/ci/should-promote-latest.sh "$VERSION")
if [ "$PROMOTE" != "true" ]; then
echo "Version $VERSION is not the highest stable semver. Not promoting :latest."
HIGHEST=$(git tag -l 'v[0-9]*' | sed 's/^v//' | grep -vE -- '-(rc|alpha|beta|pre|next)' | sort -V | tail -1 || echo "")
if [ -n "$HIGHEST" ] && [ "$VERSION" = "$HIGHEST" ]; then
PROMOTE="true"
else
echo "Version $VERSION is not the highest semver tag (highest=${HIGHEST:-<none>}). Not promoting :latest."
fi
fi
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
@@ -127,9 +123,6 @@ jobs:
needs: prepare
if: needs.prepare.outputs.skip != 'true'
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -145,9 +138,8 @@ jobs:
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -234,10 +226,6 @@ jobs:
- build
if: needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
env:
IMAGE_NAME: diegosouzapw/omniroute
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
@@ -245,9 +233,8 @@ jobs:
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
fetch-depth: 0
@@ -334,67 +321,6 @@ jobs:
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}"
- name: Generate CycloneDX SBOM (image, advisory)
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: anchore/sbom-action@v0
with:
image: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: cyclonedx-json
output-file: sbom-image.cdx.json
artifact-name: sbom-image.cdx.json
# Visibility scan: reports HIGH + CRITICAL into the SARIF (Security tab) but
# never blocks (exit-code 0). The blocking gate below narrows to CRITICAL.
#
# ignore-unfixed mirrors the blocking gate: the Security tab must surface only
# ACTIONABLE vulnerabilities — ones with a published fix we can pull by rebuilding
# on a patched base or bumping the dep. Without it the advisory upload floods the
# tab with unfixable base-image OS CVEs (Debian trixie packages with no upstream
# patch yet, overwhelmingly local-only and not reachable from the proxy request
# surface), which is noise an operator cannot act on. trivyignores points at the
# repo-root .trivyignore so accepted-risk fixable CVEs have one auditable home.
# See docs/security/SUPPLY_CHAIN.md.
- name: Trivy image scan (SARIF, advisory)
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
ignore-unfixed: true
trivyignores: .trivyignore
exit-code: "0"
# BLOCKING gate (v3.8.27 cycle-end): fail the release on a CRITICAL CVE in the
# published image. Narrowed to severity CRITICAL (HIGH stays visible in the
# SARIF step above, not blocking). ignore-unfixed:true so an unfixable base-image
# CVE with no upstream patch does not red the release (reduces false-blocks);
# a fixable CRITICAL still blocks. Per docs/security/SUPPLY_CHAIN.md. NB: Trivy
# scans against a CVE DB that grows continuously — a newly-disclosed CRITICAL on
# an unchanged base image can red this gate; the fix is to rebuild on a patched
# base, bump the dep, or add a justified .trivyignore entry (see the CVE-variance
# note in docs/security/SUPPLY_CHAIN.md).
- name: Trivy CRITICAL gate (blocking)
if: needs.prepare.outputs.version != 'main'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
format: table
severity: CRITICAL
ignore-unfixed: true
exit-code: "1"
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
category: trivy-image
- name: Update Docker Hub description
# Only refresh README/description when we actually promote :latest
# (avoids overwriting from main pushes or back-fill builds).

View File

@@ -11,56 +11,43 @@ on:
required: true
type: string
# Least-privilege default: read-only at the top level; each job grants the writes it
# needs (build/release upload assets, publish-npm forwards npm provenance / packages
# to the reusable workflow) — Scorecard TokenPermissions.
permissions:
contents: read
contents: write
id-token: write
packages: write
jobs:
validate:
name: Validate version
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Validate version format
id: validate
env:
# Pass workflow context via env (never interpolate ${{ ... }} straight
# into the run: script body) so the shell receives variables, not
# inlined text — zizmor template-injection mitigation. INPUT_VERSION is
# the operator-supplied value and is regex-validated below before use.
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "$EVENT_NAME" == "push" ]]; then
if [[ "${{ github.event_name }}" == "push" ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="$INPUT_VERSION"
VERSION="${{ inputs.version }}"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format. Expected: v1.6.8"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "✓ Valid version: $VERSION"
build:
name: Build Electron (${{ matrix.platform }})
needs: validate
runs-on: ${{ matrix.runner }}
permissions:
contents: write # electron-builder may publish artifacts with GH_TOKEN
strategy:
fail-fast: false
matrix:
@@ -84,9 +71,7 @@ jobs:
deb_ext: .deb
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -94,7 +79,7 @@ jobs:
cache: npm
- name: Cache node_modules
uses: actions/cache@v6.1.0
uses: actions/cache@v5
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -114,7 +99,7 @@ jobs:
# that cause EPERM errors during Next.js standalone build glob scans.
# Create a clean temp profile directory to avoid this.
mkdir -p "$RUNNER_TEMP/home"
echo "USERPROFILE=$RUNNER_TEMP/home" >> "$GITHUB_ENV"
echo "USERPROFILE=$RUNNER_TEMP/home" >> $GITHUB_ENV
- name: Build Next.js standalone
env:
@@ -124,13 +109,8 @@ jobs:
- name: Sync version in electron/package.json
shell: bash
env:
# Pass the validated version via env (never interpolate ${{ ... }}
# straight into the run: script body) — zizmor template-injection
# mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in
# the `validate` job, so it cannot carry shell metacharacters.
VERSION: ${{ needs.validate.outputs.version }}
run: |
VERSION="${{ needs.validate.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
node -e "
const fs = require('fs');
@@ -212,13 +192,10 @@ jobs:
name: Create Release
needs: [validate, build]
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release creates the GitHub Release
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Download all artifacts
@@ -228,20 +205,14 @@ jobs:
merge-multiple: true
- name: Create source archives
env:
# Pass the validated version via env (never interpolate ${{ ... }}
# straight into the run: script body) — zizmor template-injection
# mitigation. Already regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in
# the `validate` job, so it cannot carry shell metacharacters.
VERSION: ${{ needs.validate.outputs.version }}
run: |
# Create source code archives (excluding dev dependencies and build artifacts)
export TARBALL="OmniRoute-${VERSION}.source.tar.gz"
export ZIPBALL="OmniRoute-${VERSION}.source.zip"
export TARBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.tar.gz"
export ZIPBALL="OmniRoute-${{ needs.validate.outputs.version }}.source.zip"
# Use git archive for clean source export
git archive --format=tar.gz --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$TARBALL"
git archive --format=zip --prefix="OmniRoute-${VERSION}/" HEAD -o "release-assets/$ZIPBALL"
git archive --format=tar.gz --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$TARBALL"
git archive --format=zip --prefix=OmniRoute-${{ needs.validate.outputs.version }}/ HEAD -o "release-assets/$ZIPBALL"
echo "✓ Created source archives:"
ls -lh "release-assets/$TARBALL" "release-assets/$ZIPBALL"
@@ -271,14 +242,6 @@ jobs:
publish-npm:
name: Publish to npm
needs: [validate, release]
permissions:
# Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose
# `publish` job needs `contents: write` (gh release upload — attach the SBOM, #3874).
# A reusable workflow's job cannot request more permission than the caller grants,
# so a `read` here makes GitHub reject the run at startup (startup_failure).
contents: write
id-token: write # npm provenance (forwarded to the reusable workflow)
packages: write # publish to npm.pkg.github.com
uses: ./.github/workflows/npm-publish.yml
with:
version: ${{ needs.validate.outputs.version }}

View File

@@ -1,63 +0,0 @@
name: Mutation Redundancy (disableBail, on-demand)
# One-off measurement to UNBLOCK R1 (test-redundancy prune). The nightly mutation run
# (nightly-mutation.yml) bails on the first kill, so `killedBy` lists only the FIRST
# killer — 🟠 redundant is understated and 🟢 unique overstated (see the caveat in
# scripts/quality/mutation-radiography.mjs). This workflow re-runs the SAME combo +
# chatCore leaf batches with stryker.disablebail.json (disableBail:true, incremental:false)
# so `killedBy` lists EVERY killer. Feed the uploaded reports to
# `node scripts/quality/mutation-radiography.mjs --candidates mutation-nobail-*/mutation.json`
# to get the accurate R1 prune-candidate list (🔴 empty 🟠 redundant) for human review.
#
# Batches mirror the nightly's leaf decomposition (d/e/f/g/h/i) rather than 2 mega-batches:
# disableBail is MORE expensive than bail (it never stops early), and Stryker only writes
# mutation.json on a SUCCESSFUL finish — a batch cancelled at the cap produces NO data — so
# smaller batches each fit the 300min headroom and run in parallel. auth/accountFallback and
# the security quartet are out of scope: R1 targets the combo/chatCore leaves.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
stryker-nobail:
name: Stryker disableBail (batch ${{ matrix.batch.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
batch:
- name: d
mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts"
- name: e
mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts"
- name: f
mutate: "open-sse/services/combo/quotaScoring.ts,open-sse/services/combo/quotaStrategies.ts"
- name: g
mutate: "open-sse/handlers/chatCore/comboContextCache.ts,open-sse/handlers/chatCore/idempotency.ts,open-sse/handlers/chatCore/passthroughHelpers.ts,open-sse/handlers/chatCore/responseHeaders.ts,open-sse/handlers/chatCore/sanitization.ts,open-sse/handlers/chatCore/upstreamTimeouts.ts"
- name: h
mutate: "open-sse/handlers/chatCore/headers.ts,open-sse/handlers/chatCore/logTruncation.ts,open-sse/handlers/chatCore/memoryExtraction.ts,open-sse/handlers/chatCore/nonStreamingSse.ts,open-sse/handlers/chatCore/passthroughToolNames.ts,open-sse/handlers/chatCore/executorHelpers.ts"
- name: i
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
timeout-minutes: 300
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Run Stryker (disableBail)
env:
BATCH_MUTATE: ${{ matrix.batch.mutate }}
run: npx stryker run --config-file stryker.disablebail.json --mutate "$BATCH_MUTATE"
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v7
with:
name: mutation-nobail-${{ matrix.batch.name }}
path: reports/mutation/
if-no-files-found: warn
retention-days: 14

View File

@@ -1,126 +0,0 @@
name: Nightly Node Compat
# Plano mestre testes+CI (Eixo D2, aprovado 2026-07-04): as matrizes de compatibilidade
# Node 24/26 custavam ~28% de CADA run do CI pesado (2 execuções completas da suíte por
# sync da release-PR) para pegar uma classe de quebra que raramente nasce num PR típico.
# Elas rodam aqui 1×/dia contra o tip da release ativa (mesmo alvo do nightly-release-green)
# e continuam obrigatórias no gate de release via workflow_dispatch do ci.yml se preciso.
# fail-fast desligado: numa quebra queremos saber TODAS as versões afetadas de uma vez.
on:
schedule:
- cron: "47 6 * * *" # 06:47 UTC diário — slot distinto dos demais nightlies
workflow_dispatch:
inputs:
branch:
description: "Branch to validate (default: highest release/vX.Y.Z)"
required: false
type: string
permissions:
contents: read
issues: write
concurrency:
group: nightly-compat
cancel-in-progress: true
jobs:
resolve-branch:
name: Resolve active release branch
runs-on: ubuntu-latest
outputs:
target: ${{ steps.branch.outputs.target }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
else
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
case "$TARGET" in
release/v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Refusing non-canonical branch name: $TARGET"; exit 1 ;;
esac
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
compat-build-26:
name: Node 26 Compatibility Build
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "26"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run build
compat-tests:
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
strategy:
fail-fast: false
matrix:
node: [24, 26]
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
TEST_SHARD: ${{ matrix.shard }}/4
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run test:unit:ci:shard
report:
name: Open / update tracking issue on failure
runs-on: ubuntu-latest
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
needs: [resolve-branch, compat-build-26, compat-tests]
permissions:
issues: write
steps:
- name: Open or update issue
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ needs.resolve-branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TITLE="🌙 nightly-compat: Node 24/26 failures on $TARGET"
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE in:title" --json number --jq '.[0].number')
BODY="Nightly Node-compat run failed on \`$TARGET\`: $RUN_URL — triage which Node version/shard broke (fail-fast off, all versions reported)."
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
fi

View File

@@ -1,102 +0,0 @@
name: Nightly LLM Security
on:
schedule:
- cron: "53 5 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
promptfoo-guard:
name: promptfoo — injection guard (block mode, no secret)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute (block mode)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
INJECTION_GUARD_MODE: block
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- name: promptfoo guard-validation
run: npx --yes promptfoo@latest eval -c promptfooconfig.yaml --no-cache
env:
OMNIROUTE_URL: http://localhost:20128
OMNIROUTE_API_KEY: not-needed-blocked-before-upstream
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
garak:
name: garak probes (skip without provider secret)
runs-on: ubuntu-latest
# NOTE: the `secrets` context is NOT available in a job-level `if:` — referencing
# it there makes GitHub reject the file on push (startup_failure on every push).
# Map the secret into a job-level env and gate each step on a presence check, so
# the job stays green and simply skips the probes when the secret is absent.
env:
PROMPTFOO_PROVIDER_KEY: ${{ secrets.PROMPTFOO_PROVIDER_KEY }}
steps:
- name: Gate on provider secret
id: gate
run: |
if [ -n "$PROMPTFOO_PROVIDER_KEY" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
fi
- uses: actions/checkout@v7
with:
persist-credentials: false
if: steps.gate.outputs.run == 'true'
- uses: actions/setup-node@v6
if: steps.gate.outputs.run == 'true'
with: { node-version: "24", cache: npm }
- run: npm ci
if: steps.gate.outputs.run == 'true'
- name: Build CLI bundle
if: steps.gate.outputs.run == 'true'
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute
if: steps.gate.outputs.run == 'true'
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
sleep 2
done
- uses: actions/setup-python@v6
if: steps.gate.outputs.run == 'true'
with: { python-version: "3.12" }
- run: pip install garak
if: steps.gate.outputs.run == 'true'
- name: garak limited probes
if: steps.gate.outputs.run == 'true'
env:
OPENAI_API_KEY: ${{ secrets.PROMPTFOO_PROVIDER_KEY }}
OPENAI_BASE_URL: http://localhost:20128/v1
run: garak --model_type openai --model_name gpt-4o-mini --probes promptinject,dan,leakreplay --report_prefix garak-omniroute || true
- name: Stop server
if: always() && steps.gate.outputs.run == 'true'
run: kill "$(cat server.pid)" || true

View File

@@ -1,163 +0,0 @@
name: Nightly Mutation
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stryker:
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
runs-on: ubuntu-latest
# Mutation testing is expensive. History of the budget:
# - Full 8-module set TIMED OUT at the 180min cap (run 27705123780 = exactly 180min).
# The two god-files chatCore.ts/combo.ts dominated ~2/3 of the mutants and were
# removed from stryker.conf.json `mutate`.
# - The remaining 6 modules in 3 batches: auth.ts and accountFallback.ts are large; the
# a (auth+publicCreds) and b (accountFallback+error) batches still ran near the 180min
# cap (the perTest dry-run over ~130 covering test files is itself costly), so the two
# big modules are now ISOLATED into their own batches (a=auth, b=accountFallback).
# - Onda 3 / Fase 9 T5 re-add: combo.ts was split into 11 leaves; the 8 well-covered
# combo/* leaves are back in `mutate` (covered by the 24 combo-*.test.ts), grouped into
# 2 batches (d=heavy, e=light). After #4204 (D7b) merged, the reset-aware quota pair
# quotaScoring/quotaStrategies was added as batch f. A covering-test audit then added the
# 6 chatCore/* leaves with direct unit coverage as batch g. A follow-up then wrote dedicated
# unit tests for 6 more leaves and added them as batch h. A final follow-up added dedicated
# tests (no mock.module — fetch-override + crafted inputs + temp-DATA_DIR) for telemetryHelpers
# + memorySkillsInjection + semanticCache (its cache-HIT block now has a setCachedResponse
# fixture) as batch i — ALL 15/15 chatCore leaves are now mutated. See
# _mutate_godfiles_excluded_comment in stryker.conf.json.
# 9 PARALLEL batches, each overriding the mutate set via `--mutate` (Stryker 9 CLI:
# `-m, --mutate <comma-list>`; the conf's `mutate[]` remains the local-run default/union).
# - Cold-seeding budget (per-batch `timeout-minutes: ${{ matrix.batch.timeout || 180 }}`):
# a COLD run must COMPLETE once to write stryker-incremental.json (Stryker writes it only on
# a successful finish); a job cancelled at the cap writes nothing, so the next run is cold
# again — an infinite never-seeds loop. actions/cache is also branch-scoped, so each branch
# (incl. release) must seed its OWN cache via a run with enough headroom. Measured cold runs
# Measured cold totals (run 27801802713, extrapolated from the % at the 180/350 cancel point):
# auth ~375min (2301 mutants — EXCEEDS the 360min job max even isolated), accountFallback
# ~358min (1441), the c security quartet ~348min (1163), d ~197min (1316), g=142, h=132, e=66,
# f=45, i=33. The widely-covered modules blow the budget because the tap-runner re-runs every
# covering test file per mutant (a perTest fixed cost over ~138 test files, times thousands of
# mutants). A flat timeout bump cannot rescue auth (>360min max) — so the three over-budget
# batches are SPLIT so each half fits: auth->a1/a2 and accountFallback->b1/b2 by mutation range
# (`file:startLine-endLine`), the c quartet->c1/c2 by module pair. Splitting also seeds each
# sub-batch's own incremental cache, after which nightlies re-test only changed mutants.
# Full coverage every night in parallel; wall-clock = the slowest batch's cold run until seeded.
# Runs at stryker concurrency=4 with per-process DATA_DIR isolation
# (tests/_setup/isolateDataDir.ts) — see _concurrency_comment in stryker.conf.json.
strategy:
fail-fast: false
matrix:
batch:
# Per-batch `timeout` (minutes) tiers the cold-seeding budget by measured cost; batches
# without the key default to 180. Cold-run profiling (run 27801802713) showed the
# widely-covered modules need FAR more than the 180 cap because the tap-runner re-runs every
# covering test file per mutant: auth ~375min (2301 mutants, EXCEEDS the 360min GitHub job
# max even isolated), accountFallback ~358min, the c security quartet ~348min — none fit a
# single job. So auth/accountFallback are split by MUTATION RANGE (`file:startLine-endLine`,
# ~half the mutants each) into a1/a2, b1/b2; the c quartet is split by MODULE pair into
# c1/c2. d (3 combo modules, ~197min) stays whole. Split-heavy batches get 300min headroom
# for their cold seeding run; once each batch completes once and writes
# stryker-incremental.json, later runs re-test only changed mutants and finish far faster.
# g/h (142/132min cold) keep a 240 buffer; e/f/i (33-66min) keep the 180 default.
- name: a1
mutate: "src/sse/services/auth.ts:1-1109"
timeout: 300
- name: a2
mutate: "src/sse/services/auth.ts:1110-2218"
timeout: 300
- name: b1
mutate: "open-sse/services/accountFallback.ts:1-863"
timeout: 300
- name: b2
mutate: "open-sse/services/accountFallback.ts:864-1726"
timeout: 300
- name: c1
mutate: "src/server/authz/routeGuard.ts,src/shared/utils/circuitBreaker.ts"
timeout: 300
- name: c2
mutate: "open-sse/utils/error.ts,open-sse/utils/publicCreds.ts"
timeout: 300
- name: d
mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts"
timeout: 300
- name: e
mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts"
- name: f
mutate: "open-sse/services/combo/quotaScoring.ts,open-sse/services/combo/quotaStrategies.ts"
- name: g
mutate: "open-sse/handlers/chatCore/comboContextCache.ts,open-sse/handlers/chatCore/idempotency.ts,open-sse/handlers/chatCore/passthroughHelpers.ts,open-sse/handlers/chatCore/responseHeaders.ts,open-sse/handlers/chatCore/sanitization.ts,open-sse/handlers/chatCore/upstreamTimeouts.ts"
timeout: 240
- name: h
mutate: "open-sse/handlers/chatCore/headers.ts,open-sse/handlers/chatCore/logTruncation.ts,open-sse/handlers/chatCore/memoryExtraction.ts,open-sse/handlers/chatCore/nonStreamingSse.ts,open-sse/handlers/chatCore/passthroughToolNames.ts,open-sse/handlers/chatCore/executorHelpers.ts"
timeout: 240
- name: i
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
# Per-batch budget: split-heavy batches (a1/a2/b1/b2/c1/c2/d) override to 300min, g/h to 240min;
# the rest default to 180min. `matrix.batch.timeout` is null for batches without the key -> `|| 180`.
# NOTE: a1+a2 both mutate auth.ts (disjoint line ranges) and b1+b2 both mutate accountFallback.ts;
# when merging the per-batch mutation.json for radiography/scores, same-file mutants from sibling
# ranges must be UNIONED (scripts/check/check-mutation-ratchet.mjs::measureMutationScores and
# scripts/quality/mutation-radiography.mjs both merge per file).
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Restore Stryker incremental cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: reports/mutation/stryker-incremental.json
key: stryker-incremental-${{ matrix.batch.name }}-${{ github.run_id }}
restore-keys: stryker-incremental-${{ matrix.batch.name }}-
- name: Run Stryker (advisory)
id: stryker
continue-on-error: true
env:
BATCH_MUTATE: ${{ matrix.batch.mutate }}
run: npx stryker run --mutate "$BATCH_MUTATE"
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v7
with:
name: mutation-report-${{ matrix.batch.name }}
path: reports/mutation/
if-no-files-found: warn
retention-days: 14
# Aggregation gate (T3): each split batch emits a PARTIAL view of a mutated file
# (auth.ts lives in a1+a2, accountFallback in b1+b2), so a PER-BATCH ratchet would
# only ever see half a file vs the whole-file baseline. This job runs AFTER every
# batch, downloads all reports, and ratchets the MERGED per-module scores
# (check-mutation-ratchet UNIONS same-file mutants across reports) against the
# dedicatedGate `mutationScore.*` floors in quality-baseline.json (seeded ~2pt below
# the first full measurement). Blocking: a module dropping below its floor fails the
# run. Missing reports (e.g. an artifact-upload flake) are skipped, never failed.
mutation-ratchet:
name: Mutation score ratchet (blocking)
needs: stryker
if: always()
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
- name: Download all mutation reports
uses: actions/download-artifact@v8
with:
pattern: mutation-report-*
path: reports/all
- name: Ratchet merged per-module mutation scores
run: node scripts/check/check-mutation-ratchet.mjs reports/all/*/mutation.json --ratchet

View File

@@ -1,35 +0,0 @@
name: Nightly Property Discovery
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
property-random-seed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: fast-check random seed (high runs)
id: prop
run: FC_SEED=random FC_NUM_RUNS=2000 npm run test:property
- name: Open issue on failure
if: failure()
uses: actions/github-script@v9
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "Nightly property-test failure (Fase 8 B)",
body: "fast-check found a counterexample with a random seed. Check the run logs for the reproducible seed + minimal case, then add it as a fixture.\n\nRun: " + context.serverUrl + "/" + context.repo.owner + "/" + context.repo.repo + "/actions/runs/" + context.runId,
labels: ["quality-gate-finding"],
});

View File

@@ -1,137 +0,0 @@
name: Nightly Release-Green
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
#
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
# accrue silently on release/** and explode — in layers — at release time. This
# nightly reproduces the release-equivalent validation on the active release branch
# HEAD and, when there are HARD failures, opens/updates a single tracking issue.
#
# It is NOT a required status check and never touches a contributor PR — it only
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
# expected mid-cycle and is reported but never raises the alarm on its own; only
# real defects (typecheck / lint errors / unit / vitest / db-rules / public-creds /
# package-artifact) flip the issue open.
on:
schedule:
- cron: "23 5 * * *" # 05:23 UTC daily — off-peak, distinct from other nightlies
workflow_dispatch:
inputs:
branch:
description: "Release branch to validate (default: highest release/vX.Y.Z)"
required: false
type: string
permissions:
contents: read
issues: write
concurrency:
group: nightly-release-green
cancel-in-progress: true
jobs:
release-green:
name: Validate active release branch
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
else
# highest release/vX.Y.Z by semver among remote branches
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
# Strict format guard — reject anything that isn't release/vX.Y.Z (blocks
# ref/command injection via the workflow_dispatch input).
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Refusing non-canonical branch name: $TARGET"; exit 1
fi
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "Active release branch: $TARGET"
- name: Checkout the release branch
env:
TARGET: ${{ steps.branch.outputs.target }}
run: |
set -euo pipefail
git checkout "$TARGET"
git log -1 --oneline
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Release-green validation (full)
id: validate
run: |
set +e
node scripts/quality/validate-release-green.mjs --json --with-build \
1> release-green.json 2> release-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
cat release-green.log
- name: Open / update tracking issue on HARD failure
if: steps.validate.outputs.exit != '0'
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ steps.branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TITLE="🔴 Release branch not green: ${TARGET}"
{
echo "The nightly **release-green** validation found HARD failures on \`${TARGET}\`."
echo "These are real defects that would block the release PR — fix them in the"
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
echo ""
echo "**Run:** ${RUN_URL}"
echo ""
echo '```'
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
echo '```'
echo ""
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) listed above is expected mid-cycle and is rebaselined at release — it is NOT a contributor concern and did not, on its own, open this issue._"
} > issue-body.md
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: release-green-report
path: |
release-green.json
release-green.log
if-no-files-found: ignore

View File

@@ -1,110 +0,0 @@
name: Nightly Resilience
on:
schedule:
- cron: "41 4 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
heap:
name: Heap-growth gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:heap
chaos:
name: Resilience chaos (fault injection)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:chaos
k6-soak:
name: k6 load/soak
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
run: npm run build:cli
- name: Start OmniRoute (background)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo "server up"; break; fi
sleep 2
done
- name: Install k6
uses: grafana/setup-k6-action@v1
- name: Run k6 soak
run: k6 run tests/load/k6-soak.js
env:
BASE_URL: http://localhost:20128
SOAK_DURATION: "3m"
SOAK_VUS: "10"
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
a11y:
name: A11y axe (nightly, freeze-and-alert)
runs-on: ubuntu-latest
# The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and
# boots the standalone server itself (waits on /api/monitoring/health, 15min webServer
# timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact,
# so it self-builds — hence the generous job timeout. REQUIRE_AXE=1 makes the suite run
# the real axe analysis (the 4 page tests are gated to nightly so per-PR e2e stays fast)
# and makes the meta-test fail loudly if @axe-core/playwright ever goes missing.
timeout-minutes: 30
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
REQUIRE_AXE: "1"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: playwright-chromium-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- name: Run axe a11y suite (self-building webServer)
run: npx playwright test tests/e2e/a11y.spec.ts

View File

@@ -1,72 +0,0 @@
name: Nightly Schemathesis
on:
schedule:
- cron: "23 4 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
schemathesis:
name: Schemathesis — OpenAPI contract fuzz (advisory)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation }
run: npm run build:cli
- name: Start OmniRoute (background)
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
PORT: "20128"
run: |
node dist/server.js > server.log 2>&1 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo "server up"; break; fi
sleep 2
done
- uses: actions/setup-python@v6
with: { python-version: "3.12" }
- name: Install schemathesis
run: pip install schemathesis
- name: Schemathesis contract fuzz (advisory)
# Advisory gate: never fails the job. `continue-on-error` covers a crash of the
# step itself; `|| true` covers schemathesis exiting non-zero when it finds spec
# violations / upstream 500s — both are expected here (most /v1 endpoints proxy an
# upstream that has no provider configured in CI). The point of the nightly is to
# PROVE the contract is fuzzable and surface regressions, not to gate the build.
continue-on-error: true
run: |
schemathesis run docs/openapi.yaml \
--url http://localhost:20128 \
--max-examples 20 \
--workers 4 \
--checks all \
--max-response-time 30 \
--request-timeout 30 \
--suppress-health-check all \
--report junit \
--report-junit-path schemathesis-report/junit.xml \
--no-color \
|| true
- name: Stop server
if: always()
run: kill "$(cat server.pid)" || true
- name: Upload schemathesis report
if: always()
uses: actions/upload-artifact@v7
with:
name: schemathesis-report
path: |
schemathesis-report/
server.log
if-no-files-found: warn
retention-days: 14

View File

@@ -37,11 +37,10 @@ on:
NPM_TOKEN:
required: true
# Least-privilege default: read-only at the top level; each publish job grants the
# id-token (npm provenance) / packages (GitHub Packages) writes it needs (Scorecard
# TokenPermissions).
permissions:
contents: read
id-token: write
packages: write
env:
NPM_PUBLISH_NODE_VERSION: "24"
@@ -49,15 +48,10 @@ env:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write # gh release upload (attach SBOM to the GitHub Release)
id-token: write # npm provenance
packages: write # publish to npm.pkg.github.com
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
# Need full tag history to compare against highest semver when
# deciding whether this release should claim dist-tag `latest`.
fetch-depth: 0
@@ -147,25 +141,6 @@ jobs:
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-artifact
- name: Generate CycloneDX SBOM (npm)
if: steps.resolve.outputs.skip != 'true'
run: npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --output-format JSON --output-file sbom-npm.cdx.json
- name: Upload SBOM (npm) as workflow artifact
if: steps.resolve.outputs.skip != 'true'
uses: actions/upload-artifact@v7
with:
name: sbom-npm
path: sbom-npm.cdx.json
if-no-files-found: error
- name: Attach SBOM to GitHub Release
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
- name: Publish to npm
if: steps.resolve.outputs.skip != 'true'
env:
@@ -177,7 +152,7 @@ jobs:
# Always pass --tag explicitly. Defense in depth: even if VERSION is
# accidentally an older release, `npm publish --tag historic` will
# NOT promote it to `@latest`.
npm publish --provenance --access public --tag "$TAG"
npm publish --access public --tag "$TAG"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
- name: Publish to GitHub Packages
@@ -197,16 +172,9 @@ jobs:
publish-opencode-plugin:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm provenance
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
# Full history needed for auto-bump: git diff against previous release tag
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -214,47 +182,6 @@ jobs:
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
registry-url: https://registry.npmjs.org
- name: Auto-bump plugin version if plugin changed since last release
id: bump
working-directory: "@omniroute/opencode-plugin"
env:
CURRENT_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
PKG_VERSION=$(node -p "require('./package.json').version")
PKG_NAME=$(node -p "require('./package.json').name")
# 1) Skip if current version is not yet published (no bump needed)
PUBLISHED="$(npm view "${PKG_NAME}@${PKG_VERSION}" version 2>/dev/null || true)"
if [ "$PUBLISHED" != "$PKG_VERSION" ]; then
echo "✅ ${PKG_NAME}@${PKG_VERSION} is new — no bump needed."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 2) Find the previous release tag (exclude the current one)
PREV_TAG=$(git tag -l 'v*' --sort=-version:refname \
| grep -v "^${CURRENT_TAG}$" | head -1 || echo "")
if [ -z "$PREV_TAG" ]; then
echo "No previous tag to compare — skipping bump."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 3) Check if plugin dir actually changed since that tag
if git diff --quiet "$PREV_TAG" -- "@omniroute/opencode-plugin/"; then
echo "⏭️ No plugin changes since $PREV_TAG — nothing to publish."
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# 4) Auto-bump patch version
npm version patch --no-git-tag-version --allow-same-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "bumped=true" >> "$GITHUB_OUTPUT"
echo "📦 Auto-bumped ${PKG_NAME} from ${PKG_VERSION} to ${NEW_VERSION}"
- name: Install plugin dependencies
working-directory: "@omniroute/opencode-plugin"
run: npm install --no-audit --no-fund
@@ -281,5 +208,5 @@ jobs:
echo "⚠️ ${PKG_NAME}@${PKG_VERSION} is already published on npm — skipping."
exit 0
fi
npm publish --provenance --access public --ignore-scripts
npm publish --access public --ignore-scripts
echo "✅ Published ${PKG_NAME}@${PKG_VERSION}"

View File

@@ -32,9 +32,7 @@ jobs:
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -49,9 +47,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: "22"

View File

@@ -32,9 +32,7 @@ jobs:
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
@@ -48,9 +46,7 @@ jobs:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: "20"

View File

@@ -1,176 +0,0 @@
name: Quality Gates
on:
pull_request:
branches: ["release/**"]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CI_NODE_VERSION: "24"
jobs:
fast-gates:
name: Fast Quality Gates
runs-on: ubuntu-latest
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
- run: npm run check:openapi-routes
- run: npm run check:docs-symbols
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:test-runner-api
# Guards tap.testFiles drift: a covering unit test absent from stryker.conf.json
# tap.testFiles makes its module's mutants survive on a cold nightly-mutation run,
# false-failing the blocking mutationScore ratchet. See check-mutation-test-coverage.mjs.
- run: npm run check:mutation-test-coverage
- run: npm run check:any-budget:t11
# Build-scope guard: fails if worktrees/cruft leak into the tsconfig include
# scope (would OOM `next build`). Instant. See incident 2026-06-25 / #5031.
- run: npm run check:build-scope
# Pack-policy (unexpected-files allowlist) WITHOUT a build — catches a stray file
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity ratchets on the fast-path (PR→release) so
# cycle drift is rebaselined PER-PR instead of cascading onto the release PR's
# Quality Ratchet (v3.8.36: +30 complexity / +15 cognitive surfaced only post-merge).
- run: npm run check:complexity
- run: npm run check:cognitive-complexity
- name: Typecheck (core)
run: npm run typecheck:core
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. Fail-safe runs the FULL
# unit suite on hub/unmapped changes — TIA accelerates, never replaces, the net.
#
# BLOCKING (flipped 2026-06-17). The pre-existing release unit test-debt that kept
# this advisory was cleared: #4030 (16 Zod/registry reds, lossless restore) and
# #4063 (the last red — the LiveWS boot test — root-caused as a real event-loop
# stall in the WS sidecar, fixed + relocated to the integration suite). A full
# ci.yml run on release/v3.8.28 then showed all 8 unit shards green, so PR->release
# now blocks on unit-test regressions in the impacted set (typecheck:core already
# blocked above). Fail-safe still runs the FULL unit suite on hub/unmapped changes.
- name: Impacted unit tests (TIA, fail-safe full; blocking)
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
node scripts/quality/build-test-impact-map.mjs
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
if [ -z "$SEL" ]; then echo "No source/test changes — skipping unit tests"; exit 0; fi
# CI runners are 4-vCPU; run at --test-concurrency=4 (matching the ci.yml unit
# job) rather than test:unit's local-tuned concurrency=20. Oversubscribing the
# runner makes timing-sensitive tests (db-backup, upstream-timeout, ...) flake,
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "Fail-safe: running FULL unit suite (CI concurrency)"; npm run test:unit:ci; exit $?
fi
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}"
fast-vitest:
name: Vitest (fast-path)
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm run test:vitest
fast-unit:
name: Unit Tests fast-path (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2]
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/2
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
# config/quality/eslint-suppressions.json congela as violações EXISTENTES por
# arquivo+regra; qualquer warning NOVO aparece e o --max-warnings 0 falha o job — o
# drift de +41/+88 warnings por ciclo passa a morrer no PR que o introduz, em vez de
# ser rebaselinado às cegas na release. Aperto do baseline (na reconciliação da
# release): npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json
#
# Princípio Zero: bloqueante SÓ para branches internas (as campanhas/sessões são a
# origem do drift). PR de FORK roda em modo report (continue-on-error → o job fica
# verde com anotação; a campanha /green-prs aplica o fix via co-autoria — o
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: ESLint (baseline congelado — warning novo = vermelho)
run: npx eslint . --suppressions-location config/quality/eslint-suppressions.json --max-warnings 0

View File

@@ -1,40 +0,0 @@
name: OpenSSF Scorecard
on:
branch_protection_rule:
schedule:
- cron: "27 7 * * 1"
push:
branches: ["main"]
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# security-events: write removed — Scorecard findings are advisory and no longer
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
# not code vulnerabilities, and drowned out real CodeQL alerts). The run still
# produces the OpenSSF badge (publish_results) and a downloadable SARIF artifact.
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: SARIF file
path: results.sarif
retention-days: 5

View File

@@ -1,29 +0,0 @@
name: semgrep
on:
pull_request:
branches: ["main", "release/**"]
push:
branches: ["main"]
permissions:
contents: read
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run semgrep (advisory)
continue-on-error: true
run: |
semgrep scan --config p/owasp-top-ten --config p/secrets \
--sarif --output semgrep.sarif --metrics off || true
python -c "import json; d=json.load(open('semgrep.sarif')); print('semgrepFindings=%d' % len(d['runs'][0]['results']))" || echo "semgrepFindings=SKIP"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: semgrep-sarif
path: semgrep.sarif
retention-days: 14

View File

@@ -1,69 +0,0 @@
name: Wiki Sync
# Keeps the GitHub wiki in sync with docs/ on every release that lands on main.
# The wiki has no native generator and historically drifts (it sat at "212+ providers /
# 14 strategies / 37 MCP tools" while code was at 226 / 15 / 87, and new docs like
# SUPPLY_CHAIN never appeared). This runs scripts/docs/sync-wiki.mjs, which:
# - ADDS any docs/ page missing from the wiki (curated; internal reports excluded),
# - syncs the four cover-page counts on Home.md.
# It does NOT overwrite existing wiki pages by default: several docs sources still carry
# stale counts (e.g. ARCHITECTURE.md says "177 providers" while the wiki cover is 226),
# so blind overwrite would regress the wiki. Full content parity (--update-existing) is
# gated on regenerating those sources first.
on:
push:
branches: [main]
paths:
- "docs/**"
- "README.md"
- "AGENTS.md"
- "src/shared/constants/routingStrategies.ts"
- "config/i18n.json"
- "open-sse/mcp-server/server.ts"
- "scripts/docs/sync-wiki.mjs"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: wiki-sync
cancel-in-progress: false
jobs:
sync-wiki:
name: Sync wiki with docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Clone wiki
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.wiki.git" wiki
- name: Sync wiki (add missing pages + cover counts)
run: node scripts/docs/sync-wiki.mjs --wiki-dir wiki
- name: Commit & push if changed
run: |
cd wiki
if [ -n "$(git status --porcelain)" ]; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "docs(wiki): auto-sync pages + cover counts with docs"
git push
echo "Wiki updated."
else
echo "Wiki already in sync — nothing to push."
fi

30
.gitignore vendored
View File

@@ -23,11 +23,6 @@ coverage/
.build/**
.out/**
# Stryker mutation testing — ephemeral sandbox + generated reports (never commit)
.stryker-tmp/
reports/mutation/
stryker-output-*.json
# Memory Bank and Cursor rules (local-only AI agent context)
memory-bank/
@@ -49,10 +44,6 @@ docs/new-features/
# dependencies
node_modules/
# Also ignore a root node_modules SYMLINK (worktree setups symlink it from the main
# checkout). The trailing-slash pattern above only matches a directory, so without this
# a symlink named node_modules could be staged by `git add -A` and committed.
/node_modules
*.map
.DS_Store
@@ -161,11 +152,6 @@ typescript
# Superpowers plans/specs (internal tooling, not project code)
docs/superpowers/
# Superpowers visual-companion brainstorm mockups (ephemeral)
.superpowers/
# TIA test-impact map — generated at runtime in CI (build-test-impact-map.mjs), never committed (~21MB)
config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
@@ -217,18 +203,8 @@ pr_reviews*.json
# internal setup prompts with personal credentials — never commit
CODEX-SETUP-PROMPT.md
# Quality ratchet — métricas efêmeras (baseline commitado em config/quality/; métricas não)
config/quality/quality-metrics.json
# Runtime logs (diretório local, nunca versionado)
/logs/
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260504 .txt
PLANO-QUALITY-GATES.md
quality-metrics.json
RELATORIO-QUALITY-GATES.md
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260410 .txt
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute-mim.md
docs/prompts/AGENT-OWNERSHIP-PROTOCOL.omniroute-mid.md
omniroute.md
# mise configuration
mise.toml

View File

@@ -1,76 +0,0 @@
# .gitleaks.toml — Configuração do gitleaks para OmniRoute
# Task 7.18 — PLANO-QUALITY-GATES-FASE7.md
#
# Estende as regras padrão do gitleaks com allowlists específicas do projeto.
#
# INSTRUÇÕES para allowlists:
# Findings legítimos (fixtures de teste, creds OAuth públicas já cobertas pelo
# check-public-creds.mjs, valores de exemplo em docs) devem ser registrados abaixo
# em [[allowlist]] com um comentário explicativo obrigatório.
#
# NÃO adicione uma entrada de allowlist sem justificativa. Cada entrada é revisada
# a cada release (stale-enforcement). Regra: se o finding é um valor real que o
# sistema usa em produção, é um verdadeiro positivo — não allowliste, corrija.
#
# Referência: docs/security/PUBLIC_CREDS.md (credenciais OAuth públicas conhecidas)
# CLAUDE.md Hard Rule #11 (resolvePublicCred obrigatório)
# Herdar TODAS as regras padrão do gitleaks. ATENÇÃO: um config customizado
# SEM [extend].useDefault = true (e sem [[rules]] próprias) resulta em ZERO
# regras — o gitleaks SUBSTITUI o ruleset padrão pelo arquivo, não o estende
# automaticamente. Sem esta seção, `gitleaks --config .gitleaks.toml` nunca
# detecta nada (todo finding vira 0), tornando o gate inerte. Com useDefault,
# a allowlist abaixo é aplicada POR CIMA das ~170 regras padrão.
[extend]
useDefault = true
# Para desabilitar uma regra específica, usar:
# [[rules]]
# id = "rule-id"
# [rules.allowlist]
# description = "..."
# ---------------------------------------------------------------------------
# Allowlist global do projeto
# Entradas aqui são ignoradas em TODAS as varreduras.
# ---------------------------------------------------------------------------
[allowlist]
description = "OmniRoute project-level allowlist — fixtures, test vectors, public OAuth creds"
# Paths a ignorar completamente (node_modules, builds, etc.)
paths = [
'''node_modules''',
'''\.next''',
'''dist''',
'''\.git''',
'''coverage''',
'''\.nyc_output''',
]
# Commits específicos a ignorar (ex: commit que introduziu fixtures de teste)
# commits = []
# Regexes de stopwords — linhas que contêm estes padrões são ignoradas.
# Usar apenas para falsos positivos comprovados com justificativa abaixo.
# stopwords = []
# Regexes de targets (paths de arquivos) que podem ser allowlistados por regra.
# Ver [[allowlist]] por-regra abaixo para granularidade.
# ---------------------------------------------------------------------------
# Allowlist por-regra (adicionar conforme necessário durante o stale review)
# ---------------------------------------------------------------------------
#
# Exemplo (REMOVER / SUBSTITUIR por entradas reais quando necessário):
#
# [[rules]]
# # Allowlistar fixtures de teste que contêm tokens OAuth de exemplo/inválidos
# # Adicionado: 2026-06-13 | Revisar em: v3.9.0 | Justificativa: valores não-reais de teste
# id = "github-fine-grained-pat"
# [rules.allowlist]
# description = "Test fixture PATs — valores sintéticos, não funcionais"
# paths = [
# '''tests/fixtures/''',
# '''tests/unit/''',
# ]
#

View File

@@ -1,13 +1,31 @@
#!/usr/bin/env sh
if ! command -v npx >/dev/null 2>&1; then
echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
exit 0
fi
# #!/usr/bin/env sh
# if ! command -v npx >/dev/null 2>&1; then
# echo "⚠️ npx not found in PATH — skipping pre-commit hooks"
# echo " Run 'npm run lint && npm run check:any-budget:t11' manually before pushing."
# exit 0
# fi
# Cheap, deterministic local gates (re-enabled). Slower checks (i18n drift,
# openapi coverage/security-tiers, env-doc sync) run in CI to keep commits fast.
npx lint-staged
node scripts/check/check-docs-sync.mjs
npm run check:any-budget:t11
node scripts/check/check-tracked-artifacts.mjs
# npx lint-staged
# node scripts/check/check-docs-sync.mjs
# npm run check:any-budget:t11
# # Strict env-doc sync (FASE 2)
# node scripts/check/check-env-doc-sync.mjs
# # CLI i18n consistency check — all t() keys must exist in en.json (FASE 8.3)
# node scripts/check/check-cli-i18n.mjs
# # i18n docs drift advisory (FASE 5) — warn-only on pre-commit; CI enforces strict.
# node scripts/i18n/check-translation-drift.mjs --warn || \
# echo "⚠️ i18n drift detected. Run 'npm run i18n:run' to update locale mirrors."
# # i18n UI coverage advisory (FASE 6) — pre-commit warns; CI enforces strict.
# node scripts/i18n/check-ui-keys-coverage.mjs --threshold=80 || \
# echo "⚠️ UI i18n coverage below 80% for at least one locale."
# # OpenAPI coverage check — fails if coverage < 99% (FASE 08 content audit)
# node scripts/check/check-openapi-coverage.mjs
# # OpenAPI security tier consistency check — fails if x-loopback-only / x-always-protected
# # annotations diverge from routeGuard.ts compile-time constants (FASE 08 content audit)
# node scripts/check/check-openapi-security-tiers.mjs

View File

@@ -1,12 +1,8 @@
#!/usr/bin/env sh
# .husky/pre-push — fast deterministic gates (<10s total)
# Intentionally excludes test:unit (slow; covered by CI pre-push remote run).
# Activated: 2026-06-13 (6A.12 — replaced commented-out test:unit stub)
#if ! command -v npm >/dev/null 2>&1; then
# echo "⚠️ npm not found in PATH — skipping pre-push hooks"
# echo " Run 'npm test' manually before pushing."
# exit 0
#fi
if ! command -v npm >/dev/null 2>&1; then
echo "⚠️ npm not found in PATH — skipping pre-push hooks"
echo " Run 'npm run check:any-budget:t11 && npm run check:tracked-artifacts' manually before pushing."
exit 0
fi
npm run check:any-budget:t11 && npm run check:tracked-artifacts
#npm run test:unit

View File

@@ -1,14 +0,0 @@
{
"_comment": "Advisory markdown lint for docs/ + root *.md. Rules that conflict with the existing doc style (heavy inline HTML, long lines, centered headings) are disabled so the gate stays signal-not-noise. Run: npm run lint:md",
"default": true,
"MD013": false,
"MD033": false,
"MD041": false,
"MD024": { "siblings_only": true },
"MD026": false,
"MD036": false,
"MD040": false,
"MD029": false,
"MD007": { "indent": 2 },
"MD046": false
}

View File

@@ -9,16 +9,6 @@ app/vscode-extension/
**/db.json
# Source code (pre-built app/ is published instead)
#
# NOTE (#3578 / #3821-review): package.json "files" is the source of truth for what
# ships. It now allowlists the backend source closure the MCP server needs at runtime
# (open-sse/, src/lib, src/server, ...) and OVERRIDES the broad src/ + open-sse/ excludes
# below — npm honors files[] over .npmignore for inclusion. These lines are kept only as
# intent/back-stop: if files[] is ever trimmed back to specific paths, they must NOT be
# allowed to re-hide the MCP closure (that would silently reintroduce the --mcp
# ERR_MODULE_NOT_FOUND #3578 fixed). The closure gate in
# tests/unit/mcp-published-files-closure-3578.test.ts asserts the real `npm pack` output
# in both directions (closure present + zero test files), catching such a regression.
src/
open-sse/
docs/
@@ -28,17 +18,6 @@ images/
logs/
scripts/
# Co-located tests must never ship even when their parent dir is allowlisted by files[].
# (Primary guard is the "!**/*.test.*" negations in package.json files[]; this is defense
# in depth for any nested dir the allowlist pulls in.)
**/__tests__/
**/*.test.ts
**/*.test.tsx
**/*.test.js
**/*.test.mjs
**/*.spec.ts
**/*.spec.tsx
# Config/dev files
*.md
!README.md

10
.npmrc
View File

@@ -2,13 +2,3 @@
# Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid
# back into the tree and reopening npm audit findings for unused packages.
legacy-peer-deps=true
# Network resilience: enlarge npm's fetch retry budget so a transient registry
# socket reset (ECONNRESET) mid-download retries instead of failing the job.
# npm defaults to only 2 retries with short timeouts; `npm ci` in
# electron-release.yml hit ECONNRESET during v3.8.41 publish. Applies to every
# CI workflow (electron / docker / unit) and local installs.
fetch-retries=5
fetch-retry-factor=4
fetch-retry-mintimeout=20000
fetch-retry-maxtimeout=120000

View File

@@ -1,2 +0,0 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md

View File

@@ -1,22 +0,0 @@
[
{
"name": "CLI entry (omniroute.mjs)",
"path": "bin/omniroute.mjs",
"limit": "15 KB"
},
{
"name": "MCP server entry (mcp-server.mjs)",
"path": "bin/mcp-server.mjs",
"limit": "5 KB"
},
{
"name": "Node runtime support (nodeRuntimeSupport.mjs)",
"path": "bin/nodeRuntimeSupport.mjs",
"limit": "8 KB"
},
{
"name": "Reset password entry (reset-password.mjs)",
"path": "bin/reset-password.mjs",
"limit": "6 KB"
}
]

View File

@@ -4,6 +4,7 @@ var docs = defineDocs({
dir: "docs",
docs: {
files: [
"./getting-started/**/*.md",
"./architecture/**/*.md",
"./guides/**/*.md",
"./reference/**/*.md",

View File

@@ -1,22 +0,0 @@
# .trivyignore — accepted-risk suppressions for the container image scan
#
# Policy (see docs/security/SUPPLY_CHAIN.md):
# - The Trivy steps in .github/workflows/docker-publish.yml run with
# `ignore-unfixed: true`, so vulnerabilities WITHOUT a published fix are
# already excluded from both the blocking CRITICAL gate and the advisory
# Security-tab upload. You do NOT need an entry here for an unfixable
# base-image OS CVE — it will not be reported.
# - This file is the single auditable home for the rare case where a *fixable*
# CVE must be temporarily accepted (e.g. the upstream fix is not yet in the
# pinned base tag, or the affected package/binary is provably unreachable
# from the proxy request surface and rebuilding now is not justified).
#
# Format — one CVE id per line, each with a justification comment and, where
# possible, an expiry, e.g.:
# # CVE-XXXX-YYYY — <why accepted>; revisit on next base-image bump (YYYY-MM-DD)
# CVE-XXXX-YYYY
#
# Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a
# patched base / bump the dep) over suppressing. Stale entries are debt.
#
# (No accepted-risk suppressions at present — ignore-unfixed covers the noise.)

View File

@@ -1,15 +0,0 @@
# Advisory prose lint for OmniRoute docs (Vale — https://vale.sh).
# Warning-first: surfaces vague language, passive voice, and terminology drift without
# blocking CI. The Microsoft style is pulled by the CI step (vale sync / vale-action).
# Project terms live in the OmniRoute vocabulary so they never read as misspellings.
StylesPath = .vale/styles
MinAlertLevel = warning
Packages = Microsoft
Vocab = OmniRoute
[*.md]
BasedOnStyles = Vale, Microsoft
# Code-heavy reference docs and auto-generated catalogs are noisy under prose rules.
[docs/reference/PROVIDER_REFERENCE.md]
BasedOnStyles = Vale

View File

@@ -1,50 +0,0 @@
OmniRoute
combo
combos
[Cc]ombo
[Aa]uto-[Cc]ombo
MCP
A2A
ACP
RTK
Caveman
Troglodita
Qdrant
FTS5
SQLite
LowDB
OAuth
PKCE
SSE
JSON-RPC
LKGP
P2C
Antigravity
Codex
Kiro
Qoder
Pollinations
LongCat
Cerebras
DeepSeek
Groq
Cline
OpenCode
Termux
Electron
Fumadocs
Notion
Obsidian
WebDAV
IPv4
IPv6
SOCKS5
loopback
backoff
changelog
i18n
locale
locales
roadmap
[Ww]ildcard
upstream

99
.vscode/settings.json vendored
View File

@@ -19,79 +19,30 @@
}
},
"git.ignoreLimitWarning": true,
// ─── Git: não adicionar os ~44 repos aninhados (9 worktrees + _references/* +
// _mono_repo/* + _ideia/_tasks/.agents) ao Source Control. Era a causa do
// "validando muito": um git status + watcher por repo. Só o repo raiz fica. ───
"git.autoRepositoryDetection": false,
"git.repositoryScanMaxDepth": 0,
"git.detectSubmodules": false,
"git.autofetch": false,
"git.autorefresh": true,
// ─── Performance: não seguir o symlink de node_modules criado pelas worktrees ───
// As worktrees em .worktrees/ apontam node_modules para o checkout principal;
// sem isto a busca atravessa o symlink mesmo com node_modules excluído.
"search.followSymlinks": false,
// ─── TypeScript server (monorepo grande: ~6,6k arquivos rastreados, Next.js 16) ───
"typescript.tsserver.maxTsServerMemory": 4096,
"typescript.disableAutomaticTypeAcquisition": true,
"typescript.tsc.autoDetect": "off",
"npm.autoDetect": "off",
// O tsserver tem o próprio watcher (independente de files.watcherExclude).
// excludeDirectories evita que ele vigie as árvores pesadas.
"typescript.tsserver.watchOptions": {
"excludeDirectories": [
"**/node_modules",
"**/.next",
"**/.build",
"**/dist",
"**/coverage",
"**/.worktrees"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,
// "**/dist": true,
// "**/.build": true,
// "**/.next": true,
// "**/_references": true,
// "**/_mono_repo": true,
// "**/_tasks": true,
// "**/omniroute-*.tgz": true
// "**/electron": true,
// "**/node_modules": true,
// "**/.next": true,
// "**/coverage": true,
// "**/omniroute-*.tgz": true,
// "**/_tasks": true
// },
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/_references/**": true,
"**/_mono_repo/**": true,
"**/electron/**": true,
"**/node_modules/**": true,
"**/.next/**": true,
"**/.build/**": true,
"**/coverage/**": true,
"**/_tasks/**": true,
"**/.git/objects/**": true,
"**/dist/**": true,
"**/build/**": true,
"**/out/**": true,
"**/coverage/**": true,
"**/.coverage/**": true,
"**/.nyc_output/**": true,
"**/.cache/**": true,
"**/.turbo/**": true,
"**/.swc/**": true,
"**/.stryker-tmp/**": true,
"**/stryker-output-*/**": true,
"**/playwright-report/**": true,
"**/test-results/**": true,
"**/.worktrees/**": true,
"**/.claude/worktrees/**": true,
"**/electron/**": true,
"**/_references/**": true,
"**/_mono_repo/**": true,
"**/_tasks/**": true,
"**/logs/**": true,
"**/*.tgz": true,
"**/*.tsbuildinfo": true,
"**/OmniRoute-*/**": true,
"**/*-merge-*/**": true,
"**/*-worktree-*/**": true,
@@ -99,32 +50,16 @@
"**/*-reorg*/**": true
},
"search.exclude": {
"**/_references": true,
"**/_mono_repo": true,
"**/electron": true,
"**/node_modules": true,
"**/.next": true,
"**/.build": true,
"**/coverage": true,
"**/_tasks": true,
"**/dist": true,
"**/build": true,
"**/out": true,
"**/coverage": true,
"**/.coverage": true,
"**/.nyc_output": true,
"**/.cache": true,
"**/.turbo": true,
"**/.swc": true,
"**/.stryker-tmp": true,
"**/stryker-output-*": true,
"**/playwright-report": true,
"**/test-results": true,
"**/.worktrees": true,
"**/.claude/worktrees": true,
"**/electron": true,
"**/_references": true,
"**/_mono_repo": true,
"**/_tasks": true,
"**/logs": true,
"**/*.tgz": true,
"**/*.tsbuildinfo": true,
"**/package-lock.json": true,
"**/OmniRoute-*": true,
"**/*-merge-*": true,
"**/*-worktree-*": true,

View File

@@ -1,104 +0,0 @@
# .zizmor.yml — zizmor security audit configuration
# https://github.com/woodruffw/zizmor
#
# zizmor audits GitHub Actions workflows for security anti-patterns:
# • Unpinned third-party actions (supply-chain risk)
# • Script injection via ${{ github.* }} in run: steps
# • pull_request_target misuse (allows untrusted code to reach secrets)
# • Excessive permissions / overly broad GITHUB_TOKEN scopes
# • Cache poisoning via actions/cache with unguarded keys
# • … 20+ additional audits
#
# MOTIVATION (2026-03 incident): the trivy-action/LiteLLM supply-chain attack
# exploited exactly the kind of pull_request_target misconfiguration that zizmor
# detects statically. OmniRoute's npm/Docker/Electron publish workflows are high-
# value targets that warrant proactive audit.
#
# BASELINE STRATEGY: this file starts with an EMPTY ignores list. As zizmor is
# first run against the existing workflows, findings will be reviewed:
# • True positives → fix the workflow (preferred)
# • Accepted risk → add an entry below with a justification comment
# This enforces the same stale-enforcement discipline as other allowlists in
# this project: every ignore requires human sign-off and is subject to review
# at each release cycle.
#
# RATCHET: zizmorFindings metric in quality-baseline.json (direction: down)
# starts advisory; current baseline is frozen at first green run; new findings
# must be remediated or explicitly ignored here.
# ── Global settings ──────────────────────────────────────────────────────────
# Uncomment to pin to a specific minimum severity level.
# min-severity: low # low | medium | high | critical (default: low = all)
# ── Per-rule ignores ─────────────────────────────────────────────────────────
# zizmor ≥1.0 replaced the old top-level `ignores:` list with a `rules:` map
# keyed by audit id. Per-rule config lives under `rules.<audit-id>.ignore`,
# where each entry is `filename` or `filename:line` (line optional, column
# further-optional). See: https://docs.zizmor.sh/configuration/
#
# Format:
# rules:
# <zizmor-audit-id>: # e.g. "unpinned-uses", "template-injection"
# ignore:
# - <workflow-filename> # ignore this audit across the file
# - <workflow-filename>:<line> # or scope to a specific line
#
# Example (do not uncomment without real justification):
#
# rules:
# unpinned-uses:
# # actions/checkout@v6 is pinned at the major-version tag intentionally:
# # GitHub-managed first-party action; SHA pinning buys little against a
# # GitHub-side compromise and adds significant maintenance burden.
# ignore:
# - ci.yml
# Every entry below is an explicit, justified ignore (same stale-review
# discipline as the other allowlists in this project). New ignores require a
# per-rule entry with a justification comment.
rules:
dangerous-triggers:
# deploy-vps.yml uses `on: workflow_run` (after "Publish to Docker Hub").
# zizmor flags workflow_run as "almost always used insecurely", but this one
# is guarded and not exploitable:
# • The deploy job is gated on `github.event.workflow_run.conclusion ==
# 'success'` (deploy-vps.yml job `if:`, L15), so it only runs after a
# successful, trusted upstream run — never on a forked/PR-triggered
# failure.
# • It does NOT checkout or execute untrusted code: the deploy runs over SSH
# using repository secrets; there is no `actions/checkout` of an attacker
# ref in the privileged context.
# Accepted risk, re-review at the next release cycle (added 2026-06-15).
ignore:
- deploy-vps.yml
cache-poisoning:
# zizmor's cache-poisoning audit flags `actions/setup-node` (cache: npm) and
# `actions/cache` steps that populate a cache which a later artifact-publishing
# job could consume — the attack is: a fork PR seeds a poisoned cache that a
# trusted publish run then restores and ships. None of the four entries below
# are exploitable in that way:
#
# • electron-release.yml / npm-publish.yml — PUBLISH workflows that trigger
# ONLY on trusted events: `push: tags v*` + `workflow_dispatch` (electron)
# and `release: [released]` + `workflow_dispatch` + `workflow_call`
# (npm). They NEVER run on a `pull_request` from a fork, so an untrusted
# ref can never populate the npm/node_modules cache they restore. The
# cache key is `hashFiles('package-lock.json')` / `runner.os-node-…`, a
# first-party, lockfile-pinned key on trusted events.
#
# • opencode-plugin-ci.yml / opencode-provider-ci.yml — CI-only workflows
# (lint/test of the two opencode packages). They DO run on `pull_request`,
# but (a) they publish NO artifacts — there is no downstream publish job
# that restores their cache, and (b) GitHub Actions cache is branch-scoped:
# a fork-PR run writes to a PR-isolated cache that base-branch / release
# runs cannot read. The `cache: npm` here only speeds up `npm ci` within
# the same ephemeral CI run.
#
# Accepted risk (first-party caching on trusted/isolated events), re-review at
# the next release cycle (added 2026-06-16).
ignore:
- electron-release.yml
- npm-publish.yml
- opencode-plugin-ci.yml
- opencode-provider-ci.yml

View File

@@ -18,50 +18,23 @@ This plugin solves that by:
## Install
The plugin ships **pre-built inside the `omniroute` npm package** since v3.8.23.
If you have OmniRoute installed, the plugin is already on disk:
Once published to npm:
```sh
# 1. One command — copy the plugin into OpenCode and update opencode.json
omniroute setup opencode --auth
# 2. Follow the interactive prompt to enter your OmniRoute API key
# 3. Restart OpenCode — /models lists the full live catalog
npm install @omniroute/opencode-plugin
```
The `--auth` flag runs `opencode auth login --provider omniroute` automatically.
Use `--base-url` to point at a non-default OmniRoute address:
```sh
omniroute setup opencode --base-url https://or.example.com --auth
```
### What it does
1. Locates the bundled plugin inside the omniroute installation
2. Copies `dist/` + `package.json` to `~/.config/opencode/plugins/omniroute/`
3. Writes/updates `opencode.json` with the plugin entry (idempotent, replaces legacy entries)
4. (With `--auth`) runs `opencode auth login` so the API key is stored
Re-run any time to update the plugin or change the base URL. Older entries for
`@omniroute/opencode-provider` or the legacy `opencode-omniroute-auth` package are
automatically cleaned up.
### Manual install (without omniroute CLI)
If you cannot run `omniroute setup opencode` (local dev, CI, air-gapped), reference
the built artifact directly:
Until then (or for local development), reference the built artifact directly. Either extract the package into your OpenCode plugins dir and point at the extracted `dist/index.js`:
```sh
# from inside the OmniRoute repo
cd @omniroute/opencode-plugin && npm run build && npm pack
# then extract into ~/.config/opencode/plugins/omniroute-opencode-plugin/
```
And add the entry to `opencode.json` manually (see Quick Start below).
Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
## Quick start (single instance, manual)
## Quick start (single instance)
```jsonc
// opencode.json
@@ -69,7 +42,7 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"./plugins/omniroute-opencode-plugin/dist/index.js",
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
@@ -196,7 +169,7 @@ Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.
| `combos` | `boolean` | `true` | Discover `/api/combos` and surface them as pseudo-models with LCD capabilities. Combos are keyed under the `combo/<slug>` namespace and labelled `Combo: <name>` in the model picker so they're distinguishable from raw provider/model pairs. |
| `enrichment` | `boolean` | `true` | Pull display names from `/api/pricing/models` AND per-million-token pricing (`input`, `output`, `cached``cacheRead`, `cache_creation``cacheWrite`) from `/api/pricing`, then overlay both onto the live catalog (so the UI shows `Claude 4.7 Opus` with `cost.input: 5`, `cost.output: 25` instead of raw IDs and zeroed cost). |
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini``GEMINI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini-cli``GEMINI-CLI`). Idempotent. Combos intentionally skipped (the `Combo: ` prefix already conveys multi-upstream). |
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |

View File

@@ -1,12 +1,12 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"
@@ -26,9 +26,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -43,9 +43,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -60,9 +60,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -77,9 +77,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -94,9 +94,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
@@ -128,9 +128,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -145,9 +145,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
@@ -162,9 +162,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -179,9 +179,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -196,9 +196,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
@@ -213,9 +213,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -230,9 +230,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -247,9 +247,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
@@ -264,9 +264,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
@@ -281,9 +281,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -298,9 +298,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
@@ -315,9 +315,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -332,9 +332,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
@@ -349,9 +349,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -366,9 +366,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -383,9 +383,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -400,9 +400,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
@@ -417,9 +417,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -434,9 +434,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -451,9 +451,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -1165,9 +1165,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1178,32 +1178,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/fast-check": {
@@ -1843,6 +1843,490 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -1,18 +1,21 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.0",
"version": "0.1.0",
"description": "OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @opencode-ai/plugin contract.",
"type": "module",
"main": "./dist/index.js",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./runtime": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
@@ -23,7 +26,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
@@ -66,8 +69,5 @@
"tsup": "^8.5.1",
"tsx": "^4.22.3",
"typescript": "^5.9.3"
},
"overrides": {
"esbuild": "^0.28.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,12 @@ import { createOmniRouteAuthHook } from "../src/index.js";
test("createOmniRouteAuthHook: default providerId is 'omniroute'", () => {
const hook = createOmniRouteAuthHook();
assert.equal(hook.provider, "opencode-omniroute");
assert.equal(hook.provider, "omniroute");
});
test("createOmniRouteAuthHook: custom providerId binds to hook.provider (multi-instance)", () => {
const hook = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(hook.provider, "opencode-omniroute-preprod");
assert.equal(hook.provider, "omniroute-preprod");
});
test("createOmniRouteAuthHook: methods[0] is type 'api' with label including displayName", () => {
@@ -30,7 +30,7 @@ test("createOmniRouteAuthHook: methods[0] is type 'api' with label including dis
assert.equal(m.label, "OmniRoute API Key");
const custom = createOmniRouteAuthHook({ providerId: "omniroute-preprod" });
assert.equal(custom.methods[0].label, "OmniRoute (opencode-omniroute-preprod) API Key");
assert.equal(custom.methods[0].label, "OmniRoute (omniroute-preprod) API Key");
});
test("createOmniRouteAuthHook: prompts[0] uses key='apiKey' per @opencode-ai/plugin contract", () => {

View File

@@ -1,74 +0,0 @@
/**
* TDD regression — auto combos must never advertise `limit.context: 0`.
*
* opencode's overflow guard (packages/opencode/src/session/overflow.ts)
* short-circuits when `model.limit.context === 0`:
*
* if (input.model.limit.context === 0) return false // never overflow
*
* so a zero context silently DISABLES opencode's smart auto-compaction for
* auto combos. The session then grows unbounded until OmniRoute's
* server-side purifyHistory() destructively drops old messages — the
* "coding agent keeps forgetting things" bug.
*
* Fix under test: mapAutoComboToStaticEntry consumes the context_length /
* max_output_tokens now served by GET /api/combos/auto, and falls back to a
* safe positive default (128000 / 8192) for older servers that do not send
* the fields yet.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mapAutoComboToStaticEntry } from "../src/index.ts";
import type { OmniRouteRawAutoCombo } from "../src/index.ts";
test("uses server-provided context_length and max_output_tokens", () => {
const raw = {
id: "auto/coding",
name: "Auto Coding",
variant: "coding",
candidateCount: 5,
context_length: 1048576,
max_output_tokens: 65536,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.equal(entry.limit?.context, 1048576);
assert.equal(entry.limit?.output, 65536);
});
test("falls back to a safe positive default when the server omits limits (old servers)", () => {
const raw = {
id: "auto",
name: "Auto",
candidateCount: 3,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.ok(
typeof entry.limit?.context === "number" && entry.limit.context > 0,
`context must be a positive number (never 0 — zero disables opencode auto-compaction), got ${entry.limit?.context}`
);
assert.ok(
typeof entry.limit?.output === "number" && entry.limit.output > 0,
`output must be a positive number, got ${entry.limit?.output}`
);
});
test("ignores non-positive server values and keeps the safe fallback", () => {
const raw = {
id: "auto/fast",
name: "Auto Fast",
variant: "fast",
candidateCount: 2,
context_length: 0,
max_output_tokens: -1,
} as OmniRouteRawAutoCombo;
const entry = mapAutoComboToStaticEntry(raw);
assert.ok(
typeof entry.limit?.context === "number" && entry.limit.context > 0,
"zero/negative server values must not propagate"
);
assert.ok(typeof entry.limit?.output === "number" && entry.limit.output > 0);
});

View File

@@ -447,14 +447,14 @@ test("models() returns combo entries merged into the map", async () => {
// 3 raw models + 1 combo = 4 entries
assert.equal(Object.keys(out).length, 4);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
assert.ok(out["opencode-omniroute/claude-tier"]);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
assert.ok(out["gemini-3-flash"]);
assert.ok(out["combo/claude-tier"]);
const combo = out["opencode-omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.providerID, "opencode-omniroute");
const combo = out["combo/claude-tier"];
assert.equal(combo.name, "Combo: Claude Tier");
assert.equal(combo.providerID, "omniroute");
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
assert.equal(combo.limit.context, 100_000);
assert.equal(combo.capabilities.reasoning, false);
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["opencode-omniroute/phantom-combo"]);
assert.ok(out["combo/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
assert.equal(out["combo/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["combo/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["combo/phantom-combo"].limit.context, 0);
});
test("models(): hidden combos are excluded from the map", async () => {
@@ -505,11 +505,11 @@ test("models(): hidden combos are excluded from the map", async () => {
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["opencode-omniroute/visible"]);
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
assert.ok(out["combo/visible"]);
assert.ok(!out["combo/hidden"], "hidden combo must be omitted");
});
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
test("models(): combo name exactly matches raw model id → raw deleted, combo lives at combo/ key, no warn", async () => {
// Combo.name === raw model id triggers the dedup deletion. This mirrors
// the real OmniRoute payload where /v1/models pre-mirrors combos as
// no-slash raw entries whose ids match /api/combos friendly names.
@@ -529,9 +529,10 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
return hook.models!({} as never, { auth: apiAuth("sk-z") as never });
});
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
// Raw model deleted by combo-name dedup; combo surfaces under combo/<slug>.
assert.equal(out["claude-primary"], undefined, "raw deleted by combo-name dedup");
assert.ok(out["combo/claude-primary"], "combo surfaces under combo/ namespace");
assert.equal(out["combo/claude-primary"].name, "Combo: claude-primary");
// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
@@ -542,7 +543,7 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
});
test("models(): two combos with same slug → second gets disambiguator suffix", async () => {
// Both combos slug to `claude` — second must get `claude-<id-prefix>`.
// Both combos slug to `claude` — second must get `combo/claude-<id-prefix>`.
const combos: OmniRouteRawCombo[] = [
{
id: "uuid-a",
@@ -565,8 +566,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
// First combo gets the bare slug; second gets disambiguated.
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["combo/claude"], "first combo at bare slug");
assert.ok(out["combo/claude-uuid"], "second combo disambiguated by id prefix");
});
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
@@ -583,8 +584,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
// Catalog includes the models but NOT any combo entries.
assert.equal(Object.keys(out).length, 2);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
assert.ok(out["claude-primary"]);
assert.ok(out["claude-secondary"]);
// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
@@ -609,7 +610,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
assert.ok(second["opencode-omniroute/claude-tier"]);
assert.ok(second["combo/claude-tier"]);
});
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
@@ -638,74 +639,3 @@ test("models(): combos fetcher receives the resolved baseURL + apiKey", async ()
await hook.models!({} as never, { auth: apiAuth("sk-spy") as never });
assert.deepEqual(combosFetcher.callsBy()[0], ["https://or.example.com/v1", "sk-spy"]);
});
test("models(): nested combo-ref context is the min of nested + raw members", async () => {
// Top-level combo MASTER-LIGHT has 1 raw model (claude-primary, 200k)
// and 2 combo-refs: OldLLM (8k member) and KIRO (32k member). The OLD
// plugin would advertise 200k (only the raw model); the fix should
// make it advertise 8k (the bottleneck across the member graph).
const modelsFetcher = stubModelsFetcher([
MODEL_PRIMARY,
{
id: "oldllm-member-1",
context_length: 8_000,
max_output_tokens: 4_000,
capabilities: {
tool_calling: false,
reasoning: false,
vision: false,
thinking: false,
temperature: true,
},
input_modalities: ["text"],
output_modalities: ["text"],
},
{
id: "kiro-member-1",
context_length: 32_000,
max_output_tokens: 8_000,
capabilities: {
tool_calling: true,
reasoning: false,
vision: false,
thinking: false,
temperature: true,
},
input_modalities: ["text"],
output_modalities: ["text"],
},
]);
const combosFetcher = stubCombosFetcher([
{
id: "oldllm",
name: "OldLLM",
models: [{ id: "s1", kind: "model", model: "oldllm-member-1", weight: 100 }],
},
{
id: "kiro",
name: "KIRO",
models: [{ id: "s1", kind: "model", model: "kiro-member-1", weight: 100 }],
},
{
id: "master-light",
name: "MASTER-LIGHT",
models: [
{ id: "r1", kind: "model", model: "claude-primary", weight: 50 },
{ id: "r2", kind: "combo-ref", comboName: "OldLLM", weight: 25 },
{ id: "r3", kind: "combo-ref", comboName: "KIRO", weight: 25 },
],
},
]);
const hook = createOmniRouteProviderHook(
{ baseURL: "https://or.example.com/v1" },
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
const masterLight = out["opencode-omniroute/master-light"];
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
assert.equal(
masterLight.limit.context,
8_000,
`expected 8_000 (OldLLM bottleneck), got ${masterLight.limit.context}`
);
});

View File

@@ -203,7 +203,7 @@ function makeInput(initialProvider: Record<string, unknown> = {}): Config {
test("config: with valid auth.json + apiKey + baseURL → mutates input.provider[id] with stripped models block", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test-1", baseURL: "https://or.example.com/v1" },
omniroute: { type: "api", key: "sk-test-1", baseURL: "https://or.example.com/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -217,8 +217,8 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
await hook(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
const entry = provider["opencode-omniroute"];
assert.ok(entry, "input.provider['opencode-omniroute'] set");
const entry = provider.omniroute;
assert.ok(entry, "input.provider.omniroute set");
assert.equal(entry.npm, "@ai-sdk/openai-compatible");
assert.equal(entry.name, "OmniRoute");
assert.equal(entry.options.baseURL, "https://or.example.com/v1");
@@ -227,7 +227,7 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
// Stripped per-model shape: name + cap flags + modalities + (optional)
// cost. OC's SDK static schema accepts only `limit.{context,output}` —
// `limit.input` is NOT in the SDK shape and gets dropped silently.
const claude = entry.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = entry.models["claude-sonnet-4-6"];
assert.ok(claude, "claude model surfaced");
assert.equal(claude.name, "claude-sonnet-4-6");
assert.equal(claude.attachment, true);
@@ -246,75 +246,16 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
// Combo surfaces under bare key + LCD'd
// Combo surfaces under `combo/<friendly-name>` namespace + LCD'd
// (gemini's reasoning=false → combo reasoning=false).
const combo = entry.models["opencode-omniroute/claude-tier"];
assert.ok(combo, "combo surfaced under bare key");
assert.equal(combo.name, "Claude Tier");
const combo = entry.models["combo/claude-tier"];
assert.ok(combo, "combo surfaced under combo/ namespace");
assert.equal(combo.name, "Combo: Claude Tier");
assert.equal(combo.reasoning, false, "LCD: any member reasoning=false → combo reasoning=false");
assert.equal(combo.tool_call, true);
assert.equal(combo.limit?.context, 200_000, "LCD: min(200_000, 1_000_000)");
});
// ────────────────────────────────────────────────────────────────────────────
// 1b. Dual-key fallback (#5027) — auth.json stored under the BARE providerId
// (pre-auto-prefix login) must still resolve when the active providerId is
// prefixed (`opencode-omniroute`). Without the fallback the lookup misses
// the stored key and the user is forced to re-auth.
// ────────────────────────────────────────────────────────────────────────────
test("config: auth.json under bare key (pre-prefix login) resolves via dual-key fallback", async () => {
// Stored under bare `omniroute` (the key OC wrote before the auto-prefix fix),
// but the resolved providerId is now `opencode-omniroute`.
const readAuthJson = stubReadAuthJson({
omniroute: { type: "api", key: "sk-bare-1", baseURL: "https://or.example.com/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" }, // resolves to opencode-omniroute internally
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput();
await hook(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
const entry = provider["opencode-omniroute"];
assert.ok(entry, "provider entry published from bare-key apiKey");
assert.equal(entry.options.apiKey, "sk-bare-1", "apiKey resolved from the bare auth.json key");
assert.equal(entry.options.baseURL, "https://or.example.com/v1");
});
test("config: prefixed key wins over bare key when both present (dual-key precedence)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-prefixed", baseURL: "https://pref.example/v1" },
omniroute: { type: "api", key: "sk-bare", baseURL: "https://bare.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute" },
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(entry);
assert.equal(
entry.options.apiKey,
"sk-prefixed",
"prefixed key takes precedence (looked up first)"
);
assert.equal(entry.options.baseURL, "https://pref.example/v1");
});
// ────────────────────────────────────────────────────────────────────────────
// 2. Missing auth.json → no-op, no throw, no mutation
// ────────────────────────────────────────────────────────────────────────────
@@ -382,7 +323,7 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
models: { "manual-model": { name: "manual-model" } },
};
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -392,11 +333,11 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
{ providerId: "omniroute" },
{ readAuthJson, fetcher, combosFetcher, logger }
);
const input = makeInput({ "opencode-omniroute": manual });
const input = makeInput({ omniroute: manual });
await hook(input);
const provider = (input as { provider: Record<string, unknown> }).provider;
assert.equal(provider["opencode-omniroute"], manual, "manual override preserved by reference");
assert.equal(provider.omniroute, manual, "manual override preserved by reference");
assert.equal(fetcher.callCount(), 0, "no fetch — short-circuited before I/O");
assert.equal(readAuthJson.callCount(), 0, "no auth.json read either");
assert.ok(
@@ -411,7 +352,7 @@ test("config: existing input.provider[id] → no overwrite (respect manual overr
test("config: fetchers throw → warn + emit stub entry with models: {}", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = throwingCombosFetcher();
@@ -427,9 +368,8 @@ test("config: fetchers throw → warn + emit stub entry with models: {}", async
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry, "stub provider entry published even when fetchers fail");
assert.equal(entry.npm, "@ai-sdk/openai-compatible");
assert.deepEqual(entry.models, {}, "models stub is empty object");
@@ -452,7 +392,7 @@ test("config: fetchers throw → warn + emit stub entry with models: {}", async
test("config: combos fetcher throws → emit models-only catalog (no combos in models block)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = throwingCombosFetcher();
@@ -465,16 +405,12 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, [
"opencode-omniroute/claude-sonnet-4-6",
"opencode-omniroute/gemini-3-flash",
]);
assert.equal(entry.models["opencode-omniroute/claude-tier"], undefined, "no combo entry");
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
assert.equal(entry.models["combo-claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
"combos-fetch breadcrumb emitted"
@@ -487,7 +423,7 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
test("config: baseURL from auth.json takes precedence when opts.baseURL absent", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -501,15 +437,14 @@ test("config: baseURL from auth.json takes precedence when opts.baseURL absent",
await hook(input);
assert.equal(fetcher.callsBy()[0][0], "https://creds.example/v1");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.options.baseURL, "https://creds.example/v1");
});
test("config: opts.baseURL wins over auth.json's stored baseURL", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://creds.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -523,15 +458,14 @@ test("config: opts.baseURL wins over auth.json's stored baseURL", async () => {
await hook(input);
assert.equal(fetcher.callsBy()[0][0], "https://opts.example/v1");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.options.baseURL, "https://opts.example/v1");
});
test("config: no baseURL resolvable (no opts, no auth.json baseURL) → no-op", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test" }, // NO baseURL on the credential
omniroute: { type: "api", key: "sk-test" }, // NO baseURL on the credential
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -559,12 +493,12 @@ test("config: no baseURL resolvable (no opts, no auth.json baseURL) → no-op",
test("config: multi-instance — two plugins with different providerIds publish to their own keys without collision", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute-prod": {
"omniroute-prod": {
type: "api",
key: "sk-prod",
baseURL: "https://prod.example/v1",
},
"opencode-omniroute-preprod": {
"omniroute-preprod": {
type: "api",
key: "sk-preprod",
baseURL: "https://preprod.example/v1",
@@ -588,18 +522,15 @@ test("config: multi-instance — two plugins with different providerIds publish
await hookB(input);
const provider = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider;
assert.ok(provider["opencode-omniroute-prod"], "prod block present");
assert.ok(provider["opencode-omniroute-preprod"], "preprod block present");
assert.equal(provider["opencode-omniroute-prod"].options.apiKey, "sk-prod");
assert.equal(provider["opencode-omniroute-preprod"].options.apiKey, "sk-preprod");
assert.equal(provider["opencode-omniroute-prod"].options.baseURL, "https://prod.example/v1");
assert.equal(
provider["opencode-omniroute-preprod"].options.baseURL,
"https://preprod.example/v1"
);
assert.ok(provider["omniroute-prod"], "prod block present");
assert.ok(provider["omniroute-preprod"], "preprod block present");
assert.equal(provider["omniroute-prod"].options.apiKey, "sk-prod");
assert.equal(provider["omniroute-preprod"].options.apiKey, "sk-preprod");
assert.equal(provider["omniroute-prod"].options.baseURL, "https://prod.example/v1");
assert.equal(provider["omniroute-preprod"].options.baseURL, "https://preprod.example/v1");
assert.notEqual(
provider["opencode-omniroute-prod"],
provider["opencode-omniroute-preprod"],
provider["omniroute-prod"],
provider["omniroute-preprod"],
"blocks are distinct references"
);
});
@@ -611,7 +542,7 @@ test("config: multi-instance — two plugins with different providerIds publish
test("config + provider share cache: second call uses cached fetch result (single fetch per TTL)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -643,7 +574,7 @@ test("config + provider share cache: second call uses cached fetch result (singl
test("provider → config order also dedupes (cache populated by provider, consumed by config)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-reverse", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-reverse", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -707,7 +638,6 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
"cost",
"limit",
"modalities",
"providerID",
]);
for (const [id, entry] of Object.entries(block.models)) {
for (const key of Object.keys(entry)) {
@@ -723,7 +653,7 @@ test("buildStaticProviderEntry: stripped per-model shape matches sibling @omniro
}
// Sanity: claude entry has all expected stripped fields.
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(typeof claude.name, "string");
assert.equal(typeof claude.attachment, "boolean");
assert.equal(typeof claude.reasoning, "boolean");
@@ -748,8 +678,8 @@ test("buildStaticProviderEntry: hidden combos are excluded", () => {
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["opencode-omniroute/claude-tier"], undefined);
assert.ok(block.models["opencode-omniroute/claude-sonnet-4-6"]);
assert.equal(block.models["combo-claude-tier"], undefined);
assert.ok(block.models["claude-sonnet-4-6"]);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -765,7 +695,7 @@ test("buildStaticProviderEntry: emits modalities.input from raw.input_modalities
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.deepEqual(claude.modalities?.input, ["text", "image"]);
assert.deepEqual(claude.modalities?.output, ["text"]);
});
@@ -779,7 +709,7 @@ test("buildStaticProviderEntry: never emits limit.input (OC SDK rejects it)", ()
"https://or.example/v1",
"sk-test"
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal((claude.limit as Record<string, unknown>).input, undefined);
assert.equal(typeof claude.limit?.context, "number");
assert.equal(typeof claude.limit?.output, "number");
@@ -807,7 +737,7 @@ test("buildStaticProviderEntry: emits cost when enrichment carries pricing", ()
"sk-test",
enrichment
);
const claude = block.models["opencode-omniroute/claude-sonnet-4-6"];
const claude = block.models["claude-sonnet-4-6"];
assert.equal(claude.cost?.input, 3);
assert.equal(claude.cost?.output, 15);
assert.equal(claude.cost?.cache_read, 0.3);
@@ -828,8 +758,8 @@ test("buildStaticProviderEntry: emits release_date when raw carries it; omits wh
"https://or.example/v1",
"sk-test"
);
assert.equal(block.models["opencode-omniroute/claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["opencode-omniroute/gemini-3-flash"].release_date, undefined);
assert.equal(block.models["claude-with-date"].release_date, "2026-02-19");
assert.equal(block.models["gemini-3-flash"].release_date, undefined);
});
test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)", () => {
@@ -858,7 +788,7 @@ test("buildStaticProviderEntry: combo modalities = intersection of members (LCD)
"https://or.example/v1",
"sk-test"
);
const combo = block.models["opencode-omniroute/mixed-tier"];
const combo = block.models["combo/mixed-tier"];
assert.ok(combo, "combo emitted under slug key");
// claude has text+image, text-only has text → intersection drops image.
assert.deepEqual(combo.modalities?.input, ["text"]);
@@ -882,7 +812,7 @@ test("OmniRoutePlugin factory exposes config hook alongside auth + provider", as
test("config: auth.json entry of wrong type (oauth) → no-op", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "oauth", refresh: "r", access: "a", expires: 0 },
omniroute: { type: "oauth", refresh: "r", access: "a", expires: 0 },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -919,7 +849,7 @@ test("config: readAuthJson throws → treat as missing file (silent fallback)",
test("config: initialises input.provider when undefined", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -934,7 +864,7 @@ test("config: initialises input.provider when undefined", async () => {
await hook(input);
const provider = (input as { provider?: Record<string, unknown> }).provider;
assert.ok(provider, "provider bag initialised");
assert.ok(provider!["opencode-omniroute"]);
assert.ok(provider!.omniroute);
});
// ────────────────────────────────────────────────────────────────────────────
@@ -944,7 +874,7 @@ test("config: initialises input.provider when undefined", async () => {
test("config: enrichment fetched + name overlaid on raw-model entries", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -963,20 +893,19 @@ test("config: enrichment fetched + name overlaid on raw-model entries", async ()
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini 3 Flash");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini 3 Flash");
// Combo names still come from /api/combos — enrichment overlay does NOT touch combos.
assert.equal(entry.models["opencode-omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
assert.equal(enrichmentFetcher.callCount(), 1);
});
test("config: features.enrichment=false skips enrichment fetch + keeps raw-id names", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -994,21 +923,16 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(
entry.models["opencode-omniroute/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 () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1022,15 +946,10 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(
entry.models["opencode-omniroute/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")),
@@ -1071,7 +990,7 @@ const MODEL_NV_LLAMA: OmniRouteRawModelEntry = {
test("config: usableOnly=false → no filter (existing behavior)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CC_OPUS, MODEL_NV_LLAMA]);
const combosFetcher = stubCombosFetcher([]);
@@ -1093,9 +1012,8 @@ test("config: usableOnly=false → no filter (existing behavior)", async () => {
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"], "claude kept");
assert.ok(entry.models["nvidia/llama-3-70b"], "nvidia kept (filter off)");
assert.equal(providersFetcher.callCount(), 0, "providers fetch not called when feature off");
@@ -1103,7 +1021,7 @@ test("config: usableOnly=false → no filter (existing behavior)", async () => {
test("config: usableOnly=true → drops models for non-usable providers, keeps usable + unknown", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([
MODEL_CC_OPUS,
@@ -1143,9 +1061,8 @@ test("config: usableOnly=true → drops models for non-usable providers, keeps u
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"], "claude kept (active)");
assert.equal(entry.models["nvidia/llama-3-70b"], undefined, "nvidia dropped (error status)");
assert.ok(entry.models["agentrouter/synthetic-1"], "unknown prefix kept (subtract-filter)");
@@ -1154,7 +1071,7 @@ test("config: usableOnly=true → drops models for non-usable providers, keeps u
test("config: usableOnly=true + providers fetch fails → soft-fail keeps everything", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CC_OPUS, MODEL_NV_LLAMA]);
const combosFetcher = stubCombosFetcher([]);
@@ -1175,9 +1092,8 @@ test("config: usableOnly=true + providers fetch fails → soft-fail keeps everyt
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["cc/claude-opus-4-7"]);
assert.ok(entry.models["nvidia/llama-3-70b"], "soft-fail keeps both");
assert.ok(
@@ -1188,7 +1104,7 @@ test("config: usableOnly=true + providers fetch fails → soft-fail keeps everyt
test("config: diskCache hydrates stale snapshot when /v1/models throws", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
@@ -1225,15 +1141,11 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(
entry.models["opencode-omniroute/claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry.models["claude-sonnet-4-6"], "stale snapshot hydrated into static block");
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
"stale enrichment also reused"
);
@@ -1246,7 +1158,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
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" },
omniroute: { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1278,10 +1190,9 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
await configHook(input);
assert.equal(enrichmentFetcher.callCount(), 1, "config reused cached enrichment");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
// ─────────────────────────────────────────────────────────────────────
@@ -1292,7 +1203,7 @@ test("config: cached rawEnrichment from earlier provider hook is reused (no refe
test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-model names", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE, MODEL_GEMINI]);
const combosFetcher = stubCombosFetcher([COMBO_CLAUDE_TIER]);
@@ -1311,9 +1222,9 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
"gemini-3-flash",
{
name: "Gemini 3 Flash",
providerAlias: "gemini",
providerCanonical: "gemini",
providerDisplayName: "Gemini",
providerAlias: "gemini-cli",
providerCanonical: "gemini-cli",
providerDisplayName: "Gemini-cli",
},
],
])
@@ -1327,22 +1238,18 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.ok(entry);
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entry.models["opencode-omniroute/gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini-cli - Gemini 3 Flash");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["opencode-omniroute/claude-tier"].name, "Claude Tier");
assert.equal(entry.models["combo/claude-tier"].name, "Combo: Claude Tier");
});
test("config: providerTag=false suppresses the suffix", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1360,11 +1267,10 @@ test("config: providerTag=false suppresses the suffix", async () => {
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(
entry.models["opencode-omniroute/claude-sonnet-4-6"].name,
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6",
"enriched name kept, provider tag suppressed"
);
@@ -1372,7 +1278,7 @@ test("config: providerTag=false suppresses the suffix", async () => {
test("config: providerTag falls back to UPPER(alias) when providerDisplayName missing", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1393,15 +1299,14 @@ test("config: providerTag falls back to UPPER(alias) when providerDisplayName mi
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "CC - Claude Sonnet 4.6");
});
test("config: providerTag skipped entirely when neither providerDisplayName nor providerAlias set", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1420,15 +1325,14 @@ test("config: providerTag skipped entirely when neither providerDisplayName nor
const input = makeInput();
await hook(input);
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(entry.models["opencode-omniroute/claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude Sonnet 4.6");
});
test("config: providerTag is idempotent — second hook call doesn't double-suffix", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
omniroute: { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = stubModelsFetcher([MODEL_CLAUDE]);
const combosFetcher = stubCombosFetcher([]);
@@ -1447,76 +1351,14 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const inputA = makeInput();
await hook(inputA);
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryA.models["opencode-omniroute/claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
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();
await hook(inputB);
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryB.models["opencode-omniroute/claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
});
// ────────────────────────────────────────────────────────────────────────────
// T-NN: nested combo-ref resolution in the static catalog
// (mirrors the dynamic-catalog fix in combos.test.ts)
// ────────────────────────────────────────────────────────────────────────────
test("buildStaticProviderEntry: nested combo-ref context is the bottleneck across the graph", () => {
const resolved = resolveOmniRoutePluginOptions({ providerId: "omniroute" });
const rawModels: OmniRouteRawModelEntry[] = [
{
id: "raw-big",
context_length: 200_000,
max_output_tokens: 64_000,
capabilities: { tool_calling: true, reasoning: true, vision: false, temperature: true },
input_modalities: ["text"],
output_modalities: ["text"],
},
{
id: "raw-tiny",
context_length: 8_000,
max_output_tokens: 4_000,
capabilities: { tool_calling: false, reasoning: false, vision: false, temperature: true },
input_modalities: ["text"],
output_modalities: ["text"],
},
];
const rawCombos: OmniRouteRawCombo[] = [
{
id: "tiny-combo",
name: "TinyCombo",
models: [{ id: "m1", kind: "model", model: "raw-tiny", weight: 100 }],
},
{
id: "parent",
name: "Parent",
models: [
{ id: "p1", kind: "model", model: "raw-big", weight: 50 },
{ id: "p2", kind: "combo-ref", comboName: "TinyCombo", weight: 50 },
],
},
];
const block = buildStaticProviderEntry(
rawModels,
rawCombos,
resolved,
"https://or.example/v1",
"sk-test"
);
// Pre-fix: Parent would advertise 200_000 (only raw-big counted).
// Post-fix: Parent should advertise 8_000 (TinyCombo bottleneck).
const parent = block.models["opencode-omniroute/parent"];
assert.ok(parent, "Parent combo must be in the static catalog");
assert.equal(parent.limit?.context, 8_000);
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider
.omniroute;
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
});

View File

@@ -376,7 +376,7 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "enrichment fetcher called once");
const m = out["opencode-omniroute/claude-sonnet-4-6"];
const m = out["claude-sonnet-4-6"];
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
assert.equal(m.cost.input, 3, "enrichment pricing applied");
assert.equal(m.cost.output, 15);
@@ -401,11 +401,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
assert.equal(
out["opencode-omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id preserved"
);
assert.equal(out["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id preserved");
});
test("provider hook: compression metadata fetcher NOT called by default (opt-in)", async () => {
@@ -463,7 +459,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "compression metadata fetcher called");
const combo = out["opencode-omniroute/claude-primary"];
const combo = out["combo/claude-primary"];
assert.ok(combo, "combo entry present");
assert.match(
combo.name,
@@ -477,7 +473,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
// ─────────────────────────────────────────────────────────────────────────
const stubAuthJson = (apiKey: string) => async () => ({
"opencode-omniroute": { type: "api" as const, key: apiKey },
omniroute: { type: "api" as const, key: apiKey },
});
test("config hook: MCP auto-emit OFF by default (no mcp entry)", async () => {
@@ -492,7 +488,7 @@ test("config hook: MCP auto-emit OFF by default (no mcp entry)", async () => {
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
assert.ok(input.provider?.["opencode-omniroute"], "provider block written");
assert.ok(input.provider?.omniroute, "provider block written");
assert.equal(input.mcp, undefined, "no mcp block written");
});
@@ -512,7 +508,7 @@ test("config hook: features.mcpAutoEmit:true writes mcp entry with provider apiK
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute"] as
const entry = input.mcp?.omniroute as
| { type: string; url: string; enabled: boolean; headers: Record<string, string> }
| undefined;
assert.ok(entry, "mcp entry written");
@@ -542,7 +538,7 @@ test("config hook: features.mcpToken overrides provider apiKey in mcp Bearer", a
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute"] as { headers: Record<string, string> };
const entry = input.mcp?.omniroute as { headers: Record<string, string> };
assert.equal(
entry.headers.Authorization,
"Bearer sk-mcp-narrower",
@@ -565,11 +561,11 @@ test("config hook: existing operator mcp.<providerId> wins (no overwrite)", asyn
}
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {
mcp: { "opencode-omniroute": { type: "custom-user-entry", url: "https://manual.example/mcp" } },
mcp: { omniroute: { type: "custom-user-entry", url: "https://manual.example/mcp" } },
};
await hook(input as never);
assert.deepEqual(
input.mcp?.["opencode-omniroute"],
input.mcp?.omniroute,
{ type: "custom-user-entry", url: "https://manual.example/mcp" },
"operator override preserved"
);
@@ -584,7 +580,7 @@ test("config hook: features.mcpAutoEmit:true with /v1 in baseURL → strips corr
},
{
readAuthJson: async () => ({
"opencode-omniroute-preprod": { type: "api" as const, key: "sk-preprod" },
"omniroute-preprod": { type: "api" as const, key: "sk-preprod" },
}),
fetcher: async () => SAMPLE_RAW,
combosFetcher: async () => [],
@@ -593,7 +589,7 @@ test("config hook: features.mcpAutoEmit:true with /v1 in baseURL → strips corr
);
const input: { provider?: Record<string, unknown>; mcp?: Record<string, unknown> } = {};
await hook(input as never);
const entry = input.mcp?.["opencode-omniroute-preprod"] as { url: string };
const entry = input.mcp?.["omniroute-preprod"] as { url: string };
assert.equal(
entry.url,
"https://or-preprod.example.com/api/mcp/stream",

View File

@@ -212,8 +212,8 @@ test("shouldSanitizeForGemini: google-vertex/gemini-1.5-flash → true", () => {
assert.equal(shouldSanitizeForGemini({ model: "google-vertex/gemini-1.5-flash" }), true);
});
test("shouldSanitizeForGemini: gemini/gemini-2.5-pro → true", () => {
assert.equal(shouldSanitizeForGemini({ model: "gemini/gemini-2.5-pro" }), true);
test("shouldSanitizeForGemini: gemini-cli/gemini-2.5-pro → true (real OmniRoute alias)", () => {
assert.equal(shouldSanitizeForGemini({ model: "gemini-cli/gemini-2.5-pro" }), true);
});
test("shouldSanitizeForGemini: claude-sonnet-4 → false", () => {

View File

@@ -38,8 +38,8 @@ test("multi-instance: two plugin invocations bind to their own providerId", asyn
baseURL: "https://b.example/v1",
});
assert.equal(a.auth?.provider, "opencode-omniroute-prod");
assert.equal(b.auth?.provider, "opencode-omniroute-preprod");
assert.equal(a.auth?.provider, "omniroute-prod");
assert.equal(b.auth?.provider, "omniroute-preprod");
});
test("multi-instance: hook objects + nested arrays are independent references", async () => {
@@ -70,8 +70,8 @@ test("multi-instance: identical opts twice still yield independent objects", asy
assert.notEqual(first.auth, second.auth);
assert.notEqual(first.auth?.methods, second.auth?.methods);
// Same provider id is fine — what matters is no shared mutable state.
assert.equal(first.auth?.provider, "opencode-twin");
assert.equal(second.auth?.provider, "opencode-twin");
assert.equal(first.auth?.provider, "twin");
assert.equal(second.auth?.provider, "twin");
});
test("multi-instance: mutating instance A's auth.methods does not affect instance B", async () => {
@@ -132,5 +132,5 @@ test("multi-instance: invalid opts on one instance does not poison the other", a
providerId: "recovered",
baseURL: "https://ok.example/v1",
});
assert.equal(ok.auth?.provider, "opencode-recovered");
assert.equal(ok.auth?.provider, "recovered");
});

View File

@@ -75,7 +75,7 @@ const apiAuth = (key: string, baseURL?: string): unknown =>
test("createOmniRouteProviderHook: default providerId is 'omniroute'", () => {
const hook = createOmniRouteProviderHook(undefined, { combosFetcher: async () => [] });
assert.equal(hook.id, "opencode-omniroute");
assert.equal(hook.id, "omniroute");
});
test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-instance)", () => {
@@ -87,8 +87,8 @@ test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-ins
{ providerId: "omniroute-local" },
{ combosFetcher: async () => [] }
);
assert.equal(a.id, "opencode-omniroute-preprod");
assert.equal(b.id, "opencode-omniroute-local");
assert.equal(a.id, "omniroute-preprod");
assert.equal(b.id, "omniroute-local");
});
test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it", async () => {
@@ -101,7 +101,7 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
assert.equal(fetcher.callCount(), 1);
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
assert.equal(Object.keys(out).length, 3);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["claude-primary"]);
});
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
@@ -152,13 +152,11 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
{ fetcher, combosFetcher: async () => [] }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
const claude = out["opencode-omniroute/claude-primary"];
const claude = out["claude-primary"];
assert.ok(claude, "claude-primary present");
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
// static-catalog reader resolves `(providerID, modelID)` from the key.
assert.equal(claude.id, "opencode-omniroute/claude-primary");
assert.equal(claude.id, "claude-primary");
assert.equal(claude.name, "claude-primary");
assert.equal(claude.providerID, "opencode-omniroute");
assert.equal(claude.providerID, "omniroute");
assert.equal(claude.api.id, "openai-compatible");
assert.equal(claude.api.url, "https://or.example.com/v1");
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");

View File

@@ -1,5 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import {
OmniRoutePlugin,
OMNIROUTE_PROVIDER_KEY,
@@ -26,7 +27,7 @@ test("scaffold: default export is v1 plugin shape { id, server: OmniRoutePlugin
test("resolveOmniRoutePluginOptions: defaults", () => {
const r = resolveOmniRoutePluginOptions();
assert.equal(r.providerId, "opencode-omniroute");
assert.equal(r.providerId, "omniroute");
assert.equal(r.displayName, "OmniRoute");
assert.equal(r.modelCacheTtl, 300_000);
assert.equal(r.baseURL, undefined);
@@ -34,8 +35,8 @@ test("resolveOmniRoutePluginOptions: defaults", () => {
test("resolveOmniRoutePluginOptions: custom providerId derives displayName", () => {
const r = resolveOmniRoutePluginOptions({ providerId: "omniroute-preprod" });
assert.equal(r.providerId, "opencode-omniroute-preprod");
assert.equal(r.displayName, "OmniRoute (opencode-omniroute-preprod)");
assert.equal(r.providerId, "omniroute-preprod");
assert.equal(r.displayName, "OmniRoute (omniroute-preprod)");
});
test("resolveOmniRoutePluginOptions: explicit displayName wins", () => {
@@ -62,12 +63,11 @@ test("OmniRoutePlugin: returns an empty hooks object (scaffold)", async () => {
assert.notEqual(hooks, null);
});
test("scaffold: built ESM default export resolves with the v1 plugin shape", async () => {
// The plugin is ESM-only now — the CJS bundle was dropped to fix the OpenCode
// loader (#3883), so there is no more ../dist/index.cjs. Validate that the built
// distributable's default export still carries the OpenCode v1 { id, server } shape.
const mod = await import("../dist/index.js");
assert.strictEqual(typeof mod.default, "object");
assert.strictEqual(mod.default.id, "@omniroute/opencode-plugin");
assert.strictEqual(typeof mod.default.server, "function");
test("scaffold: CJS default export resolves via require() with v1 shape", () => {
const require_ = createRequire(import.meta.url);
const cjs = require_("../dist/index.cjs");
// after cjsInterop:true, default export is on cjs.default
assert.strictEqual(typeof cjs.default, "object");
assert.strictEqual(cjs.default.id, "@omniroute/opencode-plugin");
assert.strictEqual(typeof cjs.default.server, "function");
});

View File

@@ -2,7 +2,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: false,
@@ -11,7 +11,7 @@ export default defineConfig({
target: "node22",
outDir: "dist",
minify: false,
cjsInterop: false,
cjsInterop: true,
// Bundle runtime deps so the .tgz / npm install is self-contained.
// `zod` is required at runtime by the options schema and would otherwise
// need a peer install when the plugin is loaded directly from a file path

View File

@@ -1,23 +1,5 @@
# @omniroute/opencode-provider
> ## ⚠️ Deprecated — use [`@omniroute/opencode-plugin`](https://www.npmjs.com/package/@omniroute/opencode-plugin) instead
>
> This package writes a **static** `provider.omniroute` block to `opencode.json` from a hardcoded default model list, so it **drifts behind your live OmniRoute catalog** — adding a model in OmniRoute won't show up in OpenCode until you re-run the generator, and OpenCode Desktop/Web only surfaces a subset of the static models.
>
> **`@omniroute/opencode-plugin`** solves this by fetching `GET /v1/models` from your OmniRoute instance at OpenCode startup, so the model list is always live (see [#3419](https://github.com/diegosouzapw/OmniRoute/issues/3419)). It is now the recommended path.
>
> **One-line migration** — replace the static `provider.omniroute` block in `opencode.json` with a single plugin entry:
>
> ```jsonc
> // opencode.json
> {
> "$schema": "https://opencode.ai/config.json",
> "plugin": ["@omniroute/opencode-plugin"]
> }
> ```
>
> This package is **not removed** and still works for static/offline config generation, but it is no longer actively recommended and won't track new models automatically.
Helper for connecting [OpenCode](https://opencode.ai) to a running [OmniRoute](https://github.com/diegosouzapw/OmniRoute) AI gateway.
The package emits a **schema-valid entry** for `opencode.json` (`https://opencode.ai/config.json`) that delegates the actual runtime to [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible). It does not ship any new HTTP client — OmniRoute already exposes an OpenAI-compatible surface, and OpenCode already speaks it through the AI SDK.

View File

@@ -19,9 +19,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -36,9 +36,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -53,9 +53,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -70,9 +70,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -87,9 +87,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -104,9 +104,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
@@ -121,9 +121,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -138,9 +138,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
@@ -155,9 +155,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -172,9 +172,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -189,9 +189,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
@@ -206,9 +206,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -223,9 +223,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -240,9 +240,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
@@ -257,9 +257,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
@@ -274,9 +274,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -291,9 +291,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
@@ -308,9 +308,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -325,9 +325,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
@@ -342,9 +342,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -359,9 +359,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -376,9 +376,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -393,9 +393,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
@@ -410,9 +410,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -427,9 +427,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -444,9 +444,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -974,9 +974,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -987,32 +987,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/fdir": {
@@ -1482,6 +1482,490 @@
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@omniroute/opencode-provider",
"version": "0.1.0",
"description": "DEPRECATED — use @omniroute/opencode-plugin instead (it fetches the live OmniRoute /v1/models catalog at startup, so models never drift). This static-config generator still works but is no longer the recommended path. OpenCode provider helper for the OmniRoute AI Gateway.",
"description": "OpenCode provider helper for the OmniRoute AI Gateway. Generates a schema-valid provider entry for opencode.json that delegates the runtime to @ai-sdk/openai-compatible.",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
@@ -56,8 +56,5 @@
"tsup": "^8.5.1",
"tsx": "^4.22.3",
"typescript": "^5.9.3"
},
"overrides": {
"esbuild": "^0.28.1"
}
}

View File

@@ -2,9 +2,6 @@ import test from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import type { Server } from "node:http";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import {
buildOmniRouteOpenCodeConfig,
@@ -670,17 +667,3 @@ test("createOmniRouteModesBlock honours numeric overrides limited to OC schema",
assert.equal(block.build.temperature, 0.7);
assert.equal(block.build.top_p, 0.9);
});
// #3419 — soft-deprecation in favour of @omniroute/opencode-plugin. Guard the
// deprecation notice so it can't be silently dropped while the package is kept
// publishing (it still works; it is just no longer the recommended path).
test("package is marked deprecated in favour of @omniroute/opencode-plugin (#3419)", () => {
const here = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
assert.match(pkg.description, /DEPRECATED/);
assert.match(pkg.description, /@omniroute\/opencode-plugin/);
const readme = readFileSync(join(here, "..", "README.md"), "utf8");
assert.match(readme, /Deprecated/i);
assert.match(readme, /@omniroute\/opencode-plugin/);
});

View File

@@ -3,14 +3,14 @@
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
with **229 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
with **MCP Server** (69 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.43)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
> **Live counts (v3.8.16)**: providers 229 · MCP tools 69 · MCP scopes 13 · A2A skills 6 ·
> open-sse services 111 · routing strategies 15 · auto-combo scoring factors 12 ·
> DB modules 76 · DB migrations 94 · base tables 17 · search providers 12 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
## Doc Accuracy Discipline (read before writing any doc)
@@ -44,8 +44,8 @@ codebase. Run it locally before pushing docs; it runs in CI via `npm run check:d
## Stack
- **Runtime**: Next.js 16 (App Router), Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Language**: TypeScript 6.0 (`src/`) + JavaScript (`open-sse/`, `electron/`)
- **Runtime**: Next.js 16 (App Router), Node.js `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24.0.0 <25`, ES Modules (`"type": "module"`)
- **Language**: TypeScript 5.9 (`src/`) + JavaScript (`open-sse/`, `electron/`)
- **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/`
- **Streaming**: SSE via `open-sse` internal workspace package
- **Styling**: Tailwind CSS v4
@@ -178,7 +178,7 @@ Always run `prettier --write` on changed files.
### Data Layer (`src/lib/db/`)
All persistence uses SQLite through **95 domain-specific modules** in `src/lib/db/`. Top modules:
All persistence uses SQLite through **76 domain-specific modules** in `src/lib/db/`. Top modules:
- Core: `core.ts`, `migrationRunner.ts`, `encryption.ts`, `stateReset.ts`
- Providers / catalog: `providers.ts`, `models.ts`, `providerLimits.ts`, `compressionAnalytics.ts`
@@ -188,8 +188,8 @@ All persistence uses SQLite through **95 domain-specific modules** in `src/lib/d
- Storage: `backup.ts`, `cleanup.ts`, `jsonMigration.ts`, `healthCheck.ts`, `databaseSettings.ts`
- Extension modules: `evals.ts`, `webhooks.ts`, `reasoningCache.ts`, `readCache.ts`, `tierConfig.ts`, `compressionCombos.ts`, `compressionScheduler.ts`, `batches.ts`, `files.ts`, `syncTokens.ts`, `proxies.ts`, `oneproxy.ts`, `upstreamProxy.ts`, `versionManager.ts`, `cliToolState.ts`, `prompts.ts`, `detailedLogs.ts`, `contextHandoffs.ts`, `compression.ts`, `stats.ts`
Live count: `ls src/lib/db/*.ts | wc -l` (currently 95). Drift detection: `npm run check:docs-counts`.
Schema migrations live in `db/migrations/` (**110 files** as of v3.8.43) and run via `migrationRunner.ts`.
Live count: `ls src/lib/db/*.ts | wc -l` (currently 76). Drift detection: `npm run check:docs-counts`.
Schema migrations live in `db/migrations/` (**94 files** as of v3.8.16) and run via `migrationRunner.ts`.
`src/lib/localDb.ts` is a **re-export layer only** — never add logic there.
#### DB Internals
@@ -198,7 +198,7 @@ Schema migrations live in `db/migrations/` (**110 files** as of v3.8.43) and run
journaling. `SCHEMA_SQL` defines **17 base tables** (verify with `grep -c "CREATE TABLE" src/lib/db/core.ts` minus 1 for the bookkeeping `_omniroute_migrations` table). Helpers: `rowToCamel`, `encryptConnectionFields`.
- **`migrationRunner.ts`**: Applies versioned SQL files from `db/migrations/` inside transactions.
Tracks applied migrations in `_omniroute_migrations` table.
- **Migrations**: 110 files (`001_initial_schema.sql` → `110_*.sql`).
- **Migrations**: 94 files (`001_initial_schema.sql` → `094_*.sql`).
Each migration is idempotent and runs in a transaction. Live count: `ls src/lib/db/migrations/*.sql | wc -l`.
- **Domain modules** import `getDbInstance()` from `core.ts` for all CRUD operations.
Each module owns a specific table/set of tables (e.g., `providers.ts` → `provider_connections`,
@@ -267,7 +267,7 @@ Zod schemas, and unit tests aligned when editing.
### Provider Categories
- **Free** (3): Qoder AI, Qwen Code, Kiro AI
- **Free** (4): Qoder AI, Qwen Code, Gemini CLI (deprecated), Kiro AI
- **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Kilo Code, Cline, Qwen (⚠️ free tier discontinued 2026-04-15), Kiro, Qoder, Gemini, Windsurf (v3.8), GitLab Duo (v3.8)
- **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Perplexity,
Together, Fireworks, Cerebras, Cohere, NVIDIA, Nebius, SiliconFlow, Hyperbolic,
@@ -291,7 +291,7 @@ Providers are registered in `src/shared/constants/providers.ts` with Zod validat
### Executors (`open-sse/executors/`)
Provider-specific request executors: `base.ts`, `default.ts`, `cursor.ts`, `codex.ts`,
`antigravity.ts`, `github.ts`, `kiro.ts`, `qoder.ts`, `vertex.ts`,
`antigravity.ts`, `github.ts`, `gemini-cli.ts`, `kiro.ts`, `qoder.ts`, `vertex.ts`,
`cloudflare-ai.ts`, `opencode.ts`, `pollinations.ts`, `puter.ts`.
#### Executor Internals
@@ -336,7 +336,7 @@ Includes request/response translators with helpers for image handling.
### Services (`open-sse/services/`)
134 service modules in `open-sse/services/` (top-level only; more including sub-dirs like `autoCombo/` and `compression/`). Refresh: `ls open-sse/services/*.ts | wc -l`. Key modules:
111 service modules in `open-sse/services/` (top-level only; 171 including sub-dirs like `autoCombo/` and `compression/`). Refresh: `ls open-sse/services/*.ts | wc -l`. Key modules:
`combo.ts` (routing engine), `usage.ts`, `tokenRefresh.ts`,
`rateLimitManager.ts`, `accountFallback.ts`, `sessionManager.ts`, `wildcardRouter.ts`,
`autoCombo/`, `intentClassifier.ts`, `taskAwareRouter.ts`, `thinkingBudget.ts`,
@@ -378,8 +378,8 @@ Modular prompt compression that runs proactively before the existing reactive co
and iterates through targets in order until one succeeds or all fail.
- **`resolveComboTargets()`**: Expands a combo configuration into an ordered array of
`ResolvedComboTarget[]`, each specifying provider + model + account + credentials.
- **Strategies** (17): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
reset-window, cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay, headroom, fusion. Source: `ROUTING_STRATEGY_VALUES` in `src/shared/constants/routingStrategies.ts`.
- **Strategies** (15): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
reset-window, cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay. Source: `ROUTING_STRATEGY_VALUES` in `src/shared/constants/routingStrategies.ts`.
- Each target calls **`handleSingleModel()`** which wraps `handleChatCore()` with
per-target error handling and circuit breaker checks.
@@ -391,7 +391,7 @@ Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
### MCP Server (`open-sse/mcp-server/`)
**94 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 34-entry base registry (`MCP_TOOLS` in `schemas/tools.ts`, bundling the core / cache / compression / 1proxy / advanced tools) **plus** standalone module sets — memory (3), skill (4), agentSkill (3), pool (6), gamification (8), plugin (8), notion (6), obsidian (22). 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (30 scopes — see `OMNIROUTE_MCP_SCOPES`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
69 tools across 10 tool files (advancedTools: 5, agentSkillTools: 3, compressionTools: 5, gamificationTools: 8, memoryTools: 3, notionTools: 6, obsidianTools: 22, pluginTools: 8, poolTools: 5, skillTools: 4), 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (13 scopes — see `OMNIROUTE_MCP_SCOPES` in `open-sse/mcp-server/README.md:51`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
**Core tools** (20): get_health, list_combos, get_combo_metrics, switch_combo, check_quota,
route_request, cost_report, list_models_catalog, web_search, simulate_route, set_budget_guard,
@@ -409,14 +409,6 @@ list_compression_combos, compression_combo_stats.
**Skill tools** (4): skills_list, skills_enable, skills_execute, skills_executions.
**Agent-skill tools** (3): A2A skill discovery / invocation bridges.
**Gamification tools** (8): levels, badges, leaderboard, and community-federation queries.
**Plugin tools** (8): plugin marketplace listing, install/enable/disable, and runtime inspection.
**Notion tools** (6) + **Obsidian tools** (22): knowledge-base read/write integrations (the largest tool family — vault search, note CRUD, WebDAV-backed file ops).
#### MCP Internals
- **Tool registration**: Each tool is an object with `{ name, description, inputSchema: ZodSchema,
@@ -425,7 +417,7 @@ handler: async (args) => {...} }`. Zod validates inputs before the handler fires
`createMcpServer()` wires all tool sets; `startMcpStdio()` launches the stdio transport.
- **Transports**: stdio (CLI `omniroute --mcp`), SSE (`/api/mcp/sse`), Streamable HTTP
(`/api/mcp/stream`). All share the same tool/scope engine.
- **Scopes** (30): Control which tool categories an API key can access. Enforcement happens
- **Scopes** (13): Control which tool categories an API key can access. Enforcement happens
before handler dispatch.
- **Audit**: Every tool invocation is logged to SQLite (`mcp_audit` table) with tool name,
args, success/failure, API key attribution, and timestamp.
@@ -489,7 +481,7 @@ Request middleware including `promptInjectionGuard.ts`.
### Guardrails (`src/lib/guardrails/`)
Hot-reloadable guardrails framework (3 built-in: pii-masker, prompt-injection, vision-bridge). Fail-open. The `pii-masker` guardrail is registered and runs on every request, but its data-mutating logic is **opt-in** and OFF by default — it only redacts when `PII_REDACTION_ENABLED` (request) / `PII_RESPONSE_SANITIZATION` (response + streaming) are enabled (both `defaultValue: "false"`); with them off, payloads pass through untouched. A request can additionally opt OUT of any guardrail via header (`x-omniroute-disabled-guardrails`). Never make PII default-on (Hard Rule #20). See [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md).
Hot-reloadable guardrails framework (3 built-in: pii-masker, prompt-injection, vision-bridge). Fail-open; per-request opt-out via header. See [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md).
### Cloud Agents (`src/lib/cloudAgent/`)
@@ -527,6 +519,7 @@ Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md
## Subdirectory AGENTS.md Files
- **[`open-sse/AGENTS.md`](open-sse/AGENTS.md)** — Streaming engine, request pipeline, handlers, and executors
- **[`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md)** — SQLite persistence, domain modules, migrations
- **[`open-sse/services/AGENTS.md`](open-sse/services/AGENTS.md)** — Routing engine, combo resolution, strategy selection
@@ -534,33 +527,31 @@ Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/openapi.yaml`](docs/openapi.yaml) |
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
| Area | Doc |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 15 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/reference/openapi.yaml`](docs/reference/openapi.yaml) |
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
---

File diff suppressed because it is too large Load Diff

207
CLAUDE.md
View File

@@ -35,22 +35,22 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 160+ LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| 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 (95 files, 110 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 94 tools (34 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 30 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------ |
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| 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 (45+ files, 55 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 43 tools (30 base + 3 memory + 4 skills + 6 notion), 3 transports, ~13 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 17 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 14 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, strict-random, auto, lkgp, context-optimized, context-relay). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. See `docs/routing/AUTO-COMBO.md` for the 9-factor Auto-Combo scoring and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -311,7 +311,7 @@ connection continue serving other models.
4. Create 7 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
5. Verify `/api/services/` is in `LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`; add a test asserting `isLocalOnlyPath()` returns `true` for the new prefix if you add one (hard rule #17).
6. Add a UI tab in `src/app/(dashboard)/dashboard/providers/services/tabs/` reusing `ServiceStatusCard`, `ServiceLifecycleButtons`, `ServiceLogsPanel`.
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/openapi.yaml`.
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/reference/openapi.yaml`.
8. Write tests: unit (`tests/unit/services/`), integration (`tests/integration/services/`, gated by `RUN_SERVICES_INT=1`), and update `docs/ops/RELEASE_CHECKLIST.md` smoke section.
### Adding a New Guardrail / Eval / Skill / Webhook event
@@ -327,33 +327,32 @@ connection continue serving other models.
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| --------------------------------------------- | ------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (12-factor scoring, 17 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
| Evals | `docs/frameworks/EVALS.md` |
| Compliance / audit | `docs/security/COMPLIANCE.md` |
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
| MCP server | `docs/frameworks/MCP-SERVER.md` |
| A2A server | `docs/frameworks/A2A-SERVER.md` |
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` |
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
| Quality gates (~48 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
| Area | Doc |
| -------------------------------------------- | ----------------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (9-factor scoring, 14 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
| Evals | `docs/frameworks/EVALS.md` |
| Compliance / audit | `docs/security/COMPLIANCE.md` |
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
| MCP server | `docs/frameworks/MCP-SERVER.md` |
| A2A server | `docs/frameworks/A2A-SERVER.md` |
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/reference/openapi.yaml` |
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
---
@@ -374,10 +373,9 @@ For any non-trivial change, read the matching deep-dive first:
**Test layer preference**: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both are wired in CI (jobs `test-unit` and `test-vitest`) and must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
**Bug fix / issue triage protocol (Hard Rule #18)**: Every fix for a reported issue must be validated by one of the following — no exceptions:
1. **TDD (preferred)** — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
2. **Real-environment test (when TDD is not possible)** — deploy to the production VPS (`root@192.168.0.15`) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.
@@ -388,31 +386,6 @@ Why this matters: fixing bug A while opening bug B is worse than not fixing at a
---
## Planning & Research Artifacts (superpowers, deep-research)
`_tasks/` is a **separate, isolated git repository** that is gitignored by the main
repo (`.gitignore``_tasks/`). It is the canonical home for working artifacts —
plans, specs/designs, research, hand-offs — so they stay **versioned in their own
repo** instead of polluting the main OmniRoute tree.
**Hard rule — never write superpowers / planning / research output under `docs/` or
the repo root.** The superpowers skills ship with defaults that point at `docs/…`
(`writing-plans``docs/superpowers/plans/`, `brainstorming``docs/superpowers/specs/`).
Those defaults are **overridden here**. Whenever you invoke superpowers (or any
plan/spec/research generator) in this project, save to `_tasks/` instead, using the
same filename convention:
| Artifact (skill) | Default (do NOT use) | Save here instead |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------- |
| Plans (`writing-plans`) | `docs/superpowers/plans/` | `_tasks/superpowers/plans/YYYY-MM-DD-<feature>.md` |
| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md` |
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
rewrite it to the `_tasks/…` equivalent before writing. Commit those artifacts inside
the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
## Git Workflow
```bash
@@ -429,60 +402,14 @@ git push -u origin feat/your-feature
**Husky hooks**:
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
- **pre-push**: fast deterministic gates (`check:any-budget:t11` + `check:tracked-artifacts`); intentionally excludes `test:unit` (slow — covered by the CI `test-unit` job). Activated 2026-06-13 (Quality Gates Fase 6A.12).
### Worktree isolation (MANDATORY for every development task)
Multiple sessions/agents work this repo in parallel. The main checkout is **shared**, so a
`git checkout`/branch switch in it silently discards another session's uncommitted work and
yanks the branch out from under whatever else is running (incidents: 2026-06-05, 2026-06-13).
**Rule: never develop on the shared main checkout. Every task gets its own git worktree on its
own dedicated branch, and you MUST confirm the base branch with the operator before creating it.**
1. **Ask first — which base branch?** Before creating anything, ask the operator (via
`AskUserQuestion`, unless they already told you) from which branch the new worktree/branch
should be cut. Do NOT assume `main` or "whatever I'm on" — the answer is usually the active
`release/vX.Y.Z`, but it can be another feature/release branch. Get the base explicitly.
2. **Create an isolated worktree + branch off that base** (never reuse the main checkout).
**🔴 MANDATORY PATH: every worktree lives under `.claude/worktrees/` — and nowhere else.**
This is the single canonical location (the same dir the native `EnterWorktree` tool uses). It
is gitignored AND in the `tsconfig.json` / `.dockerignore` excludes, so worktrees never leak
into the build scope. **Never** use `.worktrees/`, repo-root, or any other path — a worktree
outside `.claude/worktrees/` (a) escapes the build-scope excludes and poisons `next build` (the
`tsconfig` `include: **/*` globs ~70× the codebase → OOM; incident 2026-06-25) and (b) scatters
worktrees across two dirs.
```bash
BASE_BRANCH="release/vX.Y.Z" # ← the branch the operator confirmed in step 1
TASK="feat/your-feature" # feat/ fix/ refactor/ docs/ test/ chore/
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:
`git worktree remove .claude/worktrees/<dir>` then `git branch -D <task>`. Never blanket-delete
`fix/*`/`feat/*` — other sessions keep their own; delete only the branches you created, by name.
5. **Never touch another session's worktree, branch, or uncommitted changes.** If `git worktree
list` shows worktrees you didn't create, leave them alone. End every session with the main
checkout back on the branch it started on (the active `release/vX.Y.Z`, never `main`).
- **pre-push**: `npm run test:unit`
---
## Environment
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only** 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.
- **Bun (build/dev script runner only)**: Bun `1.3.10` 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`. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the published runtime, or the test runners — those stay on Node. Any new Bun-invoking 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 5 scripts with `bun: not found`).
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
- **Runtime**: Node.js ≥20.20.2 <21 || ≥22.22.2 <23 || ≥24 <25, ES Modules
- **TypeScript**: 5.9+, 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)
- **Data directory**: `DATA_DIR` env var, defaults to `~/.omniroute/`
@@ -491,33 +418,6 @@ list` shows worktrees you didn't create, leave them alone. End every session wit
---
## Quality Gates & Ratchets
OmniRoute has **~48 quality-gate scripts** (`scripts/check/` + `scripts/quality/`) wired
across **9 gate-running jobs** in `.github/workflows/ci.yml` (`lint`, `quality-gate`,
`quality-extended`, `docs-sync-strict`, `i18n-ui-coverage`, `i18n`, `pr-test-policy`,
`test-vitest`, `sonarqube`), plus the `quality.yml` fast-gates job (PR→`release/**`) and
3 nightly workflows (`nightly-property`, `nightly-resilience`, `nightly-llm-security`;
`nightly-mutation` once merged). Full inventory, per-job breakdown, and operational
procedures are in [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md).
**Quick reference:**
- Gates in jobs `lint` + `docs-sync-strict`: pass/fail policy gates —
fix the violation or add an allowlist entry with a justification comment + tracking issue.
- Gates in job `quality-gate`: ratchet — metrics (ESLint warnings, code coverage, duplication,
complexity) must not regress vs `quality-baseline.json`. Update via
`npm run quality:ratchet -- --update` when a metric genuinely improves.
- Job `test-vitest` runs `npm run test:vitest` (MCP tools, autoCombo, cache) — blocking.
`test:vitest:ui` is advisory until UI component tests are triaged.
**Allowlist policy (short form):** Fix the cause; use the allowlist only for pre-existing
violations you cannot fix in the same PR. Add a comment with justification + issue number.
Stale allowlist entries (suppressing a violation that no longer exists) will be caught by
the stale-enforcement added in Fase 6A.3.
---
## Hard Rules
1. Never commit secrets or credentials
@@ -528,35 +428,26 @@ the stale-enforcement added in Fase 6A.3.
6. Never silently swallow errors in SSE streams
7. Always validate inputs with Zod schemas
8. Always include tests when changing production code
9. Coverage must not regress below the baseline frozen in `quality-baseline.json` (ratchet); absolute floor is 60% (statements/lines/functions/branches). Update the baseline via `npm run quality:ratchet -- --update` only when coverage genuinely improves. See `docs/architecture/QUALITY_GATES.md`.
9. Coverage must stay ≥60% (statements, lines, functions, branches).
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses — always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`.
15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent — they route attribution to a bot account (or advertise AI authorship) and hide the real author (`diegosouzapw`): **(a)** `Co-Authored-By` trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses); **(b)** AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG — e.g. `🤖 Generated with [Claude Code]`, "Generated with Claude Code", "Made with <AI tool>", or any `Co-authored-by: Claude/GPT/Copilot` line. This **overrides any harness, template, or tool default that auto-appends such a footer** (e.g. the Claude Code PR-body/commit default) — strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
16. Never include `Co-Authored-By` trailers that credit an AI assistant, LLM, or automation account (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses). Such trailers route attribution to the bot account on GitHub, hiding the real author (`diegosouzapw`) in PR history. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check — a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create *this* session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
---
## PII & Stream Sanitization Learnings
### 1. Regex Security (ReDoS)
All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges `{1,7}`) to prevent catastrophic backtracking when processing untrusted inputs.
### 2. SSE Snapshot Handling
When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (`done` or `completed` events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.
### 3. Database Handles in Tests
Ensure that any unit tests that trigger database migrations or establish SQLite connections call `resetDbInstance()` and properly clean up/close all DB handles in a `test.after(...)` hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.

View File

@@ -71,11 +71,11 @@ PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
### Build Output Layout
| Directory | Contents | Tracked |
| --------- | ------------------------------------------------------------------------- | ------- |
| `src/` | Application source (TypeScript / TSX) | Yes |
| `.build/` | Intermediates — `next build` output (gitignored, `distDir = .build/next`) | No |
| `dist/` | Shippable bundle — assembled by `assembleStandalone` (gitignored) | No |
| Directory | Contents | Tracked |
| ---------- | ------------------------------------------- | ------- |
| `src/` | Application source (TypeScript / TSX) | Yes |
| `.build/` | Intermediates — `next build` output (gitignored, `distDir = .build/next`) | No |
| `dist/` | Shippable bundle — assembled by `assembleStandalone` (gitignored) | No |
The build pipeline is a single pass:
@@ -160,31 +160,19 @@ npm run test:protocols:e2e
# Ecosystem compatibility tests
npm run test:ecosystem
# Coverage gate: 60% statements/lines/functions/branches
# Coverage gate: 75% statements/lines/functions, 70% branches
npm run test:coverage
npm run coverage:report
# Lint + format check
npm run lint
npm run check
# Gated real-upstream combo smoke (requires VPS access + real provider credits)
# Hits REAL providers — costs a little. NEVER runs in CI. Skips cleanly without the gate.
# Needs: ssh root@192.168.0.15 access (sources a read-only DB snapshot from the VPS).
RUN_COMBO_LIVE=1 npm run test:combo:live
# Phase-3 VPS live smoke — plain Node ESM scripts, hit the live .15 server directly.
# Requires: ssh root@192.168.0.15 access (combos created/torn down via SSH sqlite).
# Hits REAL providers (small cost). Creates/deletes only __live_test__* combos. NEVER runs in CI.
# REQUIRE_API_KEY=false on .15 so no API key needed, but honors COMBO_LIVE_BASE_URL / COMBO_LIVE_API_KEY if set.
npm run test:combo:live:vps # 7 HTTP scenarios (priority/round-robin/weighted/cost/fusion/auto + health)
npm run test:combo:live:vps:failover # adds a real cross-provider failover scenario (8 total)
```
Coverage notes:
- `npm run test:coverage` measures source coverage for the main unit test suite, excludes `tests/**`, and includes `open-sse/**`
- Pull requests must keep the coverage gate at **60%+** statements/lines/functions/branches
- Pull requests must keep the coverage gate at **75%+** statements/lines/functions and **70%+** branches
- If a PR changes production code in `src/`, `open-sse/`, `electron/`, or `bin/`, it must add or update automated tests in the same PR
- `npm run coverage:report` prints the detailed file-by-file report from the latest coverage run
- `npm run test:coverage:legacy` preserves the older metric for historical comparison
@@ -196,7 +184,7 @@ Before opening or merging a PR:
- Run `npm run test:unit`
- Run `npm run test:coverage`
- Ensure the coverage gate stays at **60%+** statements/lines/functions/branches
- Ensure the coverage gate stays at **75%+** statements/lines/functions, **70%+** branches
- Include the changed or added test files in the PR description when production code changed
- Check the SonarQube result on the PR when the project secrets are configured in CI
@@ -264,7 +252,7 @@ open-sse/ # @omniroute/open-sse workspace
electron/ # Electron desktop app (cross-platform)
tests/
├── unit/ # Node.js test runner (1,574 test files)
├── unit/ # Node.js test runner (122 test files)
├── integration/ # Integration tests
├── e2e/ # Playwright tests
├── security/ # Security tests

View File

@@ -2,28 +2,12 @@
FROM node:24-trixie-slim AS base
WORKDIR /app
# `apt-get upgrade` pulls the security-patched versions of the Debian (trixie)
# base-image packages at build time — clears the subset of container-scan CVEs
# (perl / util-linux / systemd / ncurses / zlib / tar / sqlite / shadow / pam …)
# that already have a fix published in trixie. CVEs without an upstream fix yet
# (local-only TOCTOU, etc.) remain until the distro patches them and the image
# is rebuilt; none are reachable from the proxy's request surface at runtime.
RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Refresh the globally-installed npm so its *bundled* node_modules (undici, tar)
# ship the patched versions. These are npm's own internals — not application
# dependencies (our app already resolves undici@8.5.0 / tar@7.5.16, both fixed) —
# but the container scanner flags the stale copies under
# /usr/local/lib/node_modules/npm/node_modules. npm is not invoked at runtime in
# the runner stages, so this is hygiene, not an exploitable runtime path.
RUN npm install -g npm@latest \
&& npm cache clean --force
# ── Builder ────────────────────────────────────────────────────────────────
FROM base AS builder
@@ -36,12 +20,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
# Workspace package manifests MUST be present before `npm ci` so npm materializes
# the workspace and installs its *workspace-only* deps (e.g. safe-regex,
# @toon-format/toon — declared in open-sse/package.json, not hoisted to root).
# Without this, `npm ci` skips them and the application build fails with "Module not
# found" (root cause of the v3.8.39 Docker build break). workspaces = ["open-sse"].
COPY open-sse/package.json ./open-sse/package.json
COPY scripts/build/postinstall.mjs ./scripts/build/postinstall.mjs
COPY scripts/build/postinstallSupport.mjs ./scripts/build/postinstallSupport.mjs
COPY scripts/build/native-binary-compat.mjs ./scripts/build/native-binary-compat.mjs
@@ -60,24 +38,8 @@ RUN --mount=type=cache,target=/root/.npm \
&& npm rebuild better-sqlite3 \
&& node -e "require('better-sqlite3')(':memory:').close()"
# Build with webpack (stable). Turbopack hit a non-recoverable internal panic on this
# Next.js version during the v3.8.27 release build — TurbopackInternalError "entered
# unreachable code: there must be a path to a root" in ImportTracer::get_traces, on both
# linux/amd64 and linux/arm64. Webpack is the proven engine (build:release / VPS / CI Build
# all green). Re-enable Turbopack (=1) once the upstream tracer bug is fixed.
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=0
# Raise the V8 heap ceiling for the build. The webpack production optimization
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
# dies with "FATAL ERROR: ... JavaScript heap out of memory" during the builder
# stage (#4076). NODE_OPTIONS propagates to the spawned `next build` child
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
ARG OMNIROUTE_BUILD_MEMORY_MB=4096
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
# Use Turbopack for significant build speedup
ENV OMNIROUTE_USE_TURBOPACK=1
COPY . ./
RUN --mount=type=cache,target=/app/.build/next/cache \

632
README.md
View File

@@ -6,7 +6,7 @@
# 🚀 OmniRoute — The Free AI Gateway
### Never stop coding. Connect every AI tool to **237 providers** — **90+ free** — through one endpoint.
### Never stop coding. Connect every AI tool to **177 providers** — **50+ free** — through one endpoint.
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
<br/>
@@ -15,104 +15,97 @@
<br/>
**~1.6B documented free tokens/month** — up to **~2.1B in your first month** with signup credits — aggregated across the free tiers, plus a long tail of permanently-free, no-cap providers, and the compression above stretches every one further. ([how we count →](docs/reference/FREE_TIERS.md#tldr--how-much-free-inference-does-omniroute-actually-aggregate))
**~1.9B+ documented free tokens/month** — up to **~2.5B in your first month** with signup credits — aggregated across the free tiers, and the compression above stretches every one further. ([how we count →](docs/reference/FREE_TIERS.md#tldr--how-much-free-inference-does-omniroute-actually-aggregate))
<br/>
<h3>
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier. [![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
</h3>
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![237 AI Providers](https://img.shields.io/badge/237-AI_Providers-6C5CE7?style=for-the-badge)](#-237-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-237-ai-providers--90-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](docs/reference/FREE_TIERS.md)
[![177 AI Providers](https://img.shields.io/badge/177-AI_Providers-6C5CE7?style=for-the-badge)](#-177-ai-providers--50-free)
[![50+ Free](https://img.shields.io/badge/50%2B-Free_Tiers-00B894?style=for-the-badge)](#-177-ai-providers--50-free)
[![1.9B+ Free Tokens/mo](https://img.shields.io/badge/1.9B%2B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](docs/reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![14 Strategies](https://img.shields.io/badge/14-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>
### 💬 Join the community
[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/EkzRkpzKYt)
[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/hmexnhgE)
[![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/omnirouteOficial)
[![WhatsApp Global](https://img.shields.io/badge/WhatsApp_Global-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[![WhatsApp Brasil](https://img.shields.io/badge/WhatsApp_Brasil-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/BTGJXIyjeNIIgExvTMGGhI)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[![WhatsApp Brasil](https://img.shields.io/badge/WhatsApp_Brasil-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/CeGCxdFzqBe5Uki288wOvf)
**Questions, provider tips, roadmap & support → [Discord](https://discord.gg/EkzRkpzKYt) · [Telegram](https://t.me/omnirouteOficial) · WhatsApp [🌍 Global](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) / [🇧🇷 Brasil](https://chat.whatsapp.com/BTGJXIyjeNIIgExvTMGGhI)**
**Questions, provider tips, roadmap & support → [Discord](https://discord.gg/hmexnhgE) · [Telegram](https://t.me/omnirouteOficial) · WhatsApp [🌍 Global](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) / [🇧🇷 Brasil](https://chat.whatsapp.com/CeGCxdFzqBe5Uki288wOvf)**
<br/>
### 🧩 Available
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![npm](https://img.shields.io/npm/v/omniroute?logo=npm&style=flat-square)](https://www.npmjs.com/package/omniroute)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
[![Node](https://img.shields.io/badge/node-%E2%89%A522.22.2-brightgreen?style=flat-square)](package.json)
[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
<div align="center">
[![npm version](https://img.shields.io/npm/v/omniroute?color=cb3837&logo=npm)](https://www.npmjs.com/package/omniroute)
![NPM Monthly](https://img.shields.io/npm/dm/omniroute?label=npm/month&color=cb3837&logo=npm)
[![Docker Hub](https://img.shields.io/docker/v/diegosouzapw/omniroute?label=Docker%20Hub&logo=docker&color=2496ED)](https://hub.docker.com/r/diegosouzapw/omniroute)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
![Docker Pulls](https://img.shields.io/docker/pulls/diegosouzapw/omniroute?label=docker%20pulls&logo=docker&color=2496ED)
![Electron Downloads](https://img.shields.io/github/downloads/diegosouzapw/omniroute/total?style=flat&label=electron%20downloads&logo=electron&color=47848F)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-237-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
</div>
<br/>
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-177-ai-providers--50-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[💥 The Promise](#-the-promise) • [🤔 Why](#-why-omniroute) • [🏆 What Sets Apart](#-what-sets-omniroute-apart) • [🤖 Compatible CLIs](#-compatible-clis--coding-agents) • [🖥️ Where It Runs](#%EF%B8%8F-where-omniroute-runs--anywhere) • [🔒 Private](#-private--local-first) • [🎬 In Action](#-omniroute-in-action) • [📚 Explore More](#-explore-more) • [📧 Support](#-support--community)
</div>
<div align="center">
<b>🌐 In 42+ languages</b>
<table>
<b>🌐 Available in 40+ languages</b>
<table>
<tr>
<td align="center"><a href="README.md">🇺🇸</a></td>
<td align="center"><a href="docs/i18n/pt-BR/README.md">🇧🇷</a></td>
<td align="center"><a href="docs/i18n/pt/README.md">🇵🇹</a></td>
<td align="center"><a href="docs/i18n/es/README.md">🇪🇸</a></td>
<td align="center"><a href="docs/i18n/fr/README.md">🇫🇷</a></td>
<td align="center"><a href="docs/i18n/it/README.md">🇮🇹</a></td>
<td align="center"><a href="docs/i18n/de/README.md">🇩🇪</a></td>
<td align="center"><a href="docs/i18n/nl/README.md">🇳🇱</a></td>
<td align="center"><a href="docs/i18n/ru/README.md">🇷🇺</a></td>
<td align="center"><a href="docs/i18n/uk-UA/README.md">🇺🇦</a></td>
<td align="center"><a href="docs/i18n/pl/README.md">🇵🇱</a></td>
<td align="center"><a href="docs/i18n/cs/README.md">🇨🇿</a></td>
<td align="center"><a href="docs/i18n/sk/README.md">🇸🇰</a></td>
<td align="center"><a href="docs/i18n/ro/README.md">🇷🇴</a></td>
<td align="center"><a href="docs/i18n/hu/README.md">🇭🇺</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/bg/README.md">🇧🇬</a></td>
<td align="center"><a href="docs/i18n/da/README.md">🇩🇰</a></td>
<td align="center"><a href="docs/i18n/fi/README.md">🇫🇮</a></td>
<td align="center"><a href="docs/i18n/no/README.md">🇳🇴</a></td>
<td align="center"><a href="docs/i18n/sv/README.md">🇸🇪</a></td>
<td align="center"><a href="docs/i18n/zh-CN/README.md">🇨🇳</a></td>
<td align="center"><a href="docs/i18n/zh-TW/README.md">🇹🇼</a></td>
<td align="center"><a href="docs/i18n/de/README.md">🇩🇪</a></td>
<td align="center"><a href="docs/i18n/ja/README.md">🇯🇵</a></td>
<td align="center"><a href="docs/i18n/ko/README.md">🇰🇷</a></td>
<td align="center"><a href="docs/i18n/in/README.md">🇮🇳</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/th/README.md">🇹🇭</a></td>
<td align="center"><a href="docs/i18n/vi/README.md">🇻🇳</a></td>
<td align="center"><a href="docs/i18n/id/README.md">🇮🇩</a></td>
<td align="center"><a href="docs/i18n/ms/README.md">🇲🇾</a></td>
<td align="center"><a href="docs/i18n/phi/README.md">🇵🇭</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/in/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/hi/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/gu/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/mr/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/ta/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/te/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/bn/README.md">🇧🇩</a></td>
<td align="center"><a href="docs/i18n/ur/README.md">🇵🇰</a></td>
<td align="center"><a href="docs/i18n/fa/README.md">🇮🇷</a></td>
<td align="center"><a href="docs/i18n/ar/README.md">🇸🇦</a></td>
<td align="center"><a href="docs/i18n/he/README.md">🇮🇱</a></td>
<td align="center"><a href="docs/i18n/tr/README.md">🇹🇷</a></td>
<td align="center"><a href="docs/i18n/az/README.md">🇦🇿</a></td>
<td align="center"><a href="docs/i18n/sw/README.md">🇹🇿</a></td>
<td align="center"><a href="docs/i18n/uk-UA/README.md">🇺🇦</a></td>
<td align="center"><a href="docs/i18n/pl/README.md">🇵🇱</a></td>
<td align="center"><a href="docs/i18n/cs/README.md">🇨🇿</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/nl/README.md">🇳🇱</a></td>
<td align="center"><a href="docs/i18n/bg/README.md">🇧🇬</a></td>
<td align="center"><a href="docs/i18n/da/README.md">🇩🇰</a></td>
<td align="center"><a href="docs/i18n/fi/README.md">🇫🇮</a></td>
<td align="center"><a href="docs/i18n/no/README.md">🇳🇴</a></td>
<td align="center"><a href="docs/i18n/sv/README.md">🇸🇪</a></td>
<td align="center"><a href="docs/i18n/hu/README.md">🇭🇺</a></td>
<td align="center"><a href="docs/i18n/ro/README.md">🇷🇴</a></td>
<td align="center"><a href="docs/i18n/sk/README.md">🇸🇰</a></td>
<td align="center"><a href="docs/i18n/pt/README.md">🇵🇹</a></td>
<td align="center"></td>
</tr>
</table>
</div>
@@ -121,15 +114,14 @@
<div align="center">
# 💰 ~1.6B Free Tokens / Month
# 💰 ~1.9B Free Tokens / Month
</div>
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute aggregates the **documented** free tiers of **40+ provider pools / 500+ models** into one honest number and shows it live 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 aggregates the **documented** free tiers of **50+ provider pools / 530 models** into one honest number and shows it live on the dashboard (`/dashboard/free-tiers`).
- **~1.6B free tokens / month** (steady) — and **up to ~2.1B in your first month** with signup credits.
- **Pool-deduped, honest** — we count each shared free pool **once**, so the headline isn't inflated by rate-limit ceilings the way multi-billion competitor claims are. (Counting every rate limit 24/7 would read ~10B; we don't publish that.)
- **Plus the un-countable** — permanently-free, no-token-cap providers (SiliconFlow, Z.AI GLM-Flash, Kilo, OpenCode Zen…) and a **$10 OpenRouter top-up** that unlocks **+24M/mo**, both surfaced separately so they never inflate the headline.
- **~1.9B free tokens / month** (steady) — and **up to ~2.5B in your first month** with signup credits.
- **Pool-deduped, honest** — we count each shared free pool **once**, so the headline isn't inflated by rate-limit ceilings the way multi-billion competitor claims are. (The naïve per-model sum would read ~8B; we don't publish that.)
- **Per-model breakdown**, **live used / remaining** for the current month, and a transparent **terms flag** per provider.
![Free-Tier Budget card (preview mockup)](docs/screenshots/free-tier-budget-card.svg)
@@ -144,18 +136,18 @@
</div>
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
> One endpoint. **177 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
<table>
<tr>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 177 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 1595% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>90+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>50+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
</tr>
<tr>
<td width="33%" valign="top"><b>🔌 Every tool works</b><br/><sub>24+ coding agents — Claude Code, Codex, Cursor, Cline, Copilot, Antigravity — through one config.</sub></td>
<td width="33%" valign="top"><b>🔌 Every tool works</b><br/><sub>16+ coding agents — Claude Code, Codex, Cursor, Cline, Copilot, Antigravity — through one config.</sub></td>
<td width="33%" valign="top"><b>🧩 One endpoint</b><br/><sub>OpenAI ↔ Claude ↔ Gemini ↔ Responses API translation. Point any tool at <code>/v1</code> and it just works.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (95 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (37 tools), A2A, memory, guardrails, evals. 4,690+ tests.</sub></td>
</tr>
</table>
@@ -189,7 +181,7 @@
┌──────────────────────────────────────────────────────────┐
│ OmniRoute — Smart Router │
│ RTK + Caveman compression · 17 routing strategies │
│ RTK + Caveman compression · 14 routing strategies │
│ Circuit breakers · TLS stealth · MCP · A2A · Guardrails │
└─────────────────────────┬──────────────────────────────────┘
┌─────────────┬────┴────────┬─────────────┐
@@ -227,58 +219,21 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
##
### 🔀 Or build your own — 17 routing strategies
### 🔀 Or build your own — 14 routing strategies
All **17** strategies — mix & match per combo step:
| # | Strategy | What it does |
| --- | ------------------- | ---------------------------------------------------------------- |
| 1 | `priority` | First-target ordered list — drain each before the next 🥇 |
| 2 | `fill-first` | Fill each target's quota fully before moving on |
| 3 | `weighted` | Weighted random by per-target weight |
| 4 | `round-robin` | Cycle through targets in order |
| 5 | `p2c` | Power-of-two-choices random load balancing |
| 6 | `least-used` | Pick the target with the lowest current load |
| 7 | `random` | Uniform random pick (deduplicated) |
| 8 | `strict-random` | Random without de-duplicating repeats 🎲 |
| 9 | `cost-optimized` | Minimize $ per request from live catalog pricing 💸 |
| 10 | `headroom` | Pick the target with the most remaining quota |
| 11 | `reset-window` | Prefer the target whose quota window resets soonest |
| 12 | `reset-aware` | Rank by quota reset time — short windows first 📊 |
| 13 | `context-relay` | Hand off context across targets for long conversations 🧠 |
| 14 | `context-optimized` | Pick the best fit for the current context size |
| 15 | `lkgp` | Last-Known-Good Path — sticky to the last successful target |
| 16 | `auto` | 9-factor live scoring across every connection 🤖 |
| 17 | `fusion` | Fan out to a panel of models + a judge synthesizes one answer 🧬 |
| Goal | Strategy / combo |
| --------------------------------------- | -------------------------------------------------- |
| 🥇 Drain my subscription before paying | `priority` / `fill-first` |
| ⚖️ Spread load across accounts | `round-robin` · `weighted` · `p2c` · `least-used` |
| 💸 Always cheapest viable model | `cost-optimized` · `auto/cheap` |
| 🧠 Hand off long context between models | `context-relay` · `context-optimized` |
| 🎲 Randomized / privacy routing | `random` · `strict-random` |
| 🤖 Just make it smart | `auto` (9-factor scoring) · `lkgp` · `reset-aware` |
<sub>The Auto-Combo engine scores every candidate on **9 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
##
### ⚖️ Quota-Share — split one subscription across a team ✨ NEW
> Running several keys against the **same upstream account** (one Codex Pro plan, one Kimi key, one GLM Coding seat)? A burst on one key can burn the whole 5-hour / hourly quota and lock everyone else out. **Quota-Share** distributes a provider's time-based quota **fairly** across the keys in a pool — and it's _work-conserving_, so an idle member's slice is lent out instead of wasted.
| Knob | What it controls |
| ------------------------ | ------------------------------------------------------------------------------- |
| ⚖️ **Allocation weight** | each key's slice of the pool — e.g. `50 / 30 / 20` |
| 📐 **Dimensions** | track `%` · requests · tokens · `$`, per **5h / 7d / per-model** window |
| 🚦 **Policy** | `hard` (block over share) · `soft` (deprioritize) · `burst` (use idle headroom) |
| 🧱 **Cap** | absolute ceiling per key, independent of mode |
```
Pool "team-codex" · 1 Codex Pro account · 3 keys · 5-hour window
├─ alice weight 50 ██████████░░░░░░░░░░ ≤ 50% of the shared 5h quota
├─ bob weight 30 ██████░░░░░░░░░░░░░░ ≤ 30%
└─ ci-bot weight 20 ████░░░░░░░░░░░░░░░░ ≤ 20%
Generous mode (<50% pool used) → idle shares are lent out
Strict mode (≥50% pool used) → each key held to its fair share
```
<sub>Enforced in the hot path **before** the request leaves OmniRoute, with per-(key, model) caps + session stickiness for prompt-cache integrity. 📖 [Quota Sharing Engine](docs/routing/QUOTA_SHARE.md)</sub>
##
### 🧱 Resilience is built in (3 independent layers)
| Layer | Scope | What it does |
@@ -306,20 +261,20 @@ Result: 4 layers of fallback = zero downtime
</div>
| Feature | OmniRoute | Other routers |
| -------------------------------------- | ------------------------------------------------------------------- | ------------- |
| 🌐 Providers | **237** | 20100 |
| 🆓 Free providers | **90+ (11 free forever)** | 15 |
| 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🗜️ Token compression | **RTK + Caveman stacked (1595%)** | None / 2040% |
| 🧰 Built-in MCP server | **95 tools, 3 transports, 30 scopes** | Rare |
| 🤝 A2A agent protocol | **6 skills, JSON-RPC 2.0** | None |
| 🧠 Memory (FTS5 + vector) | **Yes** | Rare |
| 🛡️ Guardrails (PII, injection, vision) | **Yes** | Rare |
| ☁️ Cloud agents | **Codex, Cursor, Devin, Jules** | None |
| 🥷 TLS fingerprint stealth | **JA3/JA4 via wreq-js** | None |
| 🖥️ Multi-platform | **Web · Desktop · Termux · PWA** | Web only |
| 🌍 i18n | **42 locales** | 04 |
| Feature | OmniRoute | Other routers |
| -------------------------------------- | ----------------------------------------------------------- | ------------- |
| 🌐 Providers | **177** | 20100 |
| 🆓 Free providers | **50+ (11 free forever)** | 15 |
| 🔀 Routing strategies | **14** (priority, weighted, cost-optimized, context-relay…) | 13 |
| 🗜️ Token compression | **RTK + Caveman stacked (1595%)** | None / 2040% |
| 🧰 Built-in MCP server | **37 tools, 3 transports, 13 scopes** | Rare |
| 🤝 A2A agent protocol | **5 skills, JSON-RPC 2.0** | None |
| 🧠 Memory (FTS5 + vector) | **Yes** | Rare |
| 🛡️ Guardrails (PII, injection, vision) | **Yes** | Rare |
| ☁️ Cloud agents | **Codex, Devin, Jules** | None |
| 🥷 TLS fingerprint stealth | **JA3/JA4 via wreq-js** | None |
| 🖥️ Multi-platform | **Web · Desktop · Termux · PWA** | Web only |
| 🌍 i18n | **40+ locales** | 04 |
<sub>📊 Detailed comparison vs LiteLLM, OpenRouter & Portkey → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -327,32 +282,6 @@ Result: 4 layers of fallback = zero downtime
<div align="center">
# ✨ What's New
</div>
> Recent highlights from **v3.8.20 → v3.8.44**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
- **🗜️ Compression hardening** — a default-on **inflation guard** (discard the stacked result and send the verbatim original whenever compression would _grow_ the prompt), completed **Caveman rule packs** for German / French / Japanese (dedup + ultra) plus a new **Chinese (文言 / wényán) input pack** with zh-vs-ja auto-detection, and **RTK filters for Gradle & .NET (`dotnet`)** build output. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **💸 Honest flat-rate cost** — subscription / coding-plan providers (ChatGPT Web, grok-web, the Minimax / Kimi / GLM / Alibaba Coding plans, Xiaomi MiMo…) now read **$0** in cost analytics instead of an inflated per-token estimate, while budget / quota / routing keep estimating unchanged. → [API Reference](docs/reference/API_REFERENCE.md)
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key, model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md)
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **🗜️ Pluggable compression** — an async pipeline of **10 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), a unified panel with named profiles + an active-profile selector, an opt-in **per-engine pipeline circuit-breaker**, an opt-in **LLM-tier engine** (a model pass for higher-ratio semantic compression), a **read-lifecycle engine** that collapses superseded file reads, **usage-observed prefix freeze**, a graduated **CCR retrieval-feedback ramp**, a `preserveSystemPrompt` mode enum, and a **drag-reorder pipeline editor** in the studio. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture & translate traffic from CLIs that ignore proxy env vars, with a per-SNI certificate authority and a trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), opt-in **typed memory decay** (aged low-value memories fade on a per-type schedule), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, and per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only). → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), and Codex account import from a raw ChatGPT access token. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
<br/>
<div align="center">
# 🤖 Compatible CLIs & Coding Agents
> One config — `http://localhost:20128/v1` — and **every** AI IDE or CLI runs on free & low-cost models.
@@ -362,6 +291,7 @@ Result: 4 layers of fallback = zero downtime
<tr>
<td align="center" width="120"><a href="https://github.com/anthropics/claude-code"><img src="./public/providers/claude.svg" width="52" alt="Claude Code"/><br/><b>Claude Code</b></a></td>
<td align="center" width="120"><a href="https://github.com/openai/codex"><img src="./public/providers/codex.svg" width="52" alt="Codex CLI"/><br/><b>Codex CLI</b></a></td>
<td align="center" width="120"><a href="https://github.com/google-gemini/gemini-cli"><img src="./public/providers/gemini-cli.svg" width="52" alt="Gemini CLI"/><br/><b>Gemini CLI</b></a></td>
<td align="center" width="120"><img src="./public/providers/cursor.png" width="52" alt="Cursor"/><br/><b>Cursor</b></td>
<td align="center" width="120"><img src="./public/providers/copilot.png" width="52" alt="Copilot"/><br/><b>Copilot</b></td>
<td align="center" width="120"><img src="./public/providers/continue.png" width="52" alt="Continue"/><br/><b>Continue</b></td>
@@ -381,7 +311,7 @@ Result: 4 layers of fallback = zero downtime
<b> also works with</b> · Cline · Antigravity · Windsurf · AMP · Hermes · Qwen CLI · Roo · Continue · <b>any OpenAI-compatible tool</b>
</div>
<sub>📖 Per-tool setup for all 24+ tools → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
<sub>📖 Per-tool setup for all 16+ tools → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
</div>
@@ -389,60 +319,28 @@ Result: 4 layers of fallback = zero downtime
<div align="center">
# 🌐 237 AI Providers — 90+ Free
# 🌐 177 AI Providers — 50+ Free
</div>
> The most complete catalog of any open-source router: **237 providers**, **90+ with a free tier**, **11 free forever**.
> The most complete catalog of any open-source router: **177 providers**, **50+ with a free tier**, **11 free forever**.
<div align="center">
### 🏢 Every major lab — through one endpoint
<table>
<tr>
<td align="center" width="92"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/openai.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/openai.svg" width="40" alt="OpenAI"/></picture><br/><sub>OpenAI</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/claude-color.svg" width="40" alt="Anthropic"/><br/><sub>Anthropic</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/gemini-color.svg" width="40" alt="Gemini"/><br/><sub>Gemini</sub></td>
<td align="center" width="92"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/grok.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/grok.svg" width="40" alt="xAI Grok"/></picture><br/><sub>xAI Grok</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/deepseek-color.svg" width="40" alt="DeepSeek"/><br/><sub>DeepSeek</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/mistral-color.svg" width="40" alt="Mistral"/><br/><sub>Mistral</sub></td>
</tr>
<tr>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/qwen-color.svg" width="40" alt="Qwen"/><br/><sub>Qwen</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/meta-color.svg" width="40" alt="Meta Llama"/><br/><sub>Meta Llama</sub></td>
<td align="center" width="92"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/groq.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/groq.svg" width="40" alt="Groq"/></picture><br/><sub>Groq</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/nvidia-color.svg" width="40" alt="NVIDIA"/><br/><sub>NVIDIA</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/minimax-color.svg" width="40" alt="MiniMax"/><br/><sub>MiniMax</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/cohere-color.svg" width="40" alt="Cohere"/><br/><sub>Cohere</sub></td>
</tr>
<tr>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/perplexity-color.svg" width="40" alt="Perplexity"/><br/><sub>Perplexity</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/huggingface-color.svg" width="40" alt="Hugging Face"/><br/><sub>HuggingFace</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/together-color.svg" width="40" alt="Together"/><br/><sub>Together</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/fireworks-color.svg" width="40" alt="Fireworks"/><br/><sub>Fireworks</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/cloudflare-color.svg" width="40" alt="Cloudflare"/><br/><sub>Cloudflare</sub></td>
<td align="center" width="92"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/baidu-color.svg" width="40" alt="Baidu"/><br/><sub>Baidu</sub></td>
</tr>
</table>
<sub>…and 220+ more — every icon resolves live from the dashboard's provider catalog. 📖 [Provider Reference](docs/reference/PROVIDER_REFERENCE.md)</sub>
<br/>
### 🆓 Free Forever — $0, no card
<table>
<tr>
<td align="center" width="150"><img src="./public/providers/agentrouter.png" width="44" alt="AgentRouter"/><br/><b>AgentRouter</b><br/><sub>GPT-5, Claude, Gemini<br/>$100 free credits</sub></td>
<td align="center" width="150"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/qoder-color.svg" width="44" alt="Qoder AI"/><br/><b>Qoder AI</b><br/><sub>Kimi-K2, DeepSeek-R1<br/>Unlimited FREE</sub></td>
<td align="center" width="150"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/pollinations.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/pollinations.svg" width="44" alt="Pollinations"/></picture><br/><b>Pollinations</b><br/><sub>GPT-5, Claude, Llama 4<br/>No key needed</sub></td>
<td align="center" width="150"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/longcat-color.svg" width="44" alt="LongCat"/><br/><b>LongCat</b><br/><sub>LongCat-2.0<br/>10M tokens one-time (KYC) 🔑</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/AgentRouter-FF6600?style=flat-square" alt="AgentRouter"/><br/><sub>GPT-5, Claude, Gemini<br/>$100 free credits</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/Qoder_AI-6366F1?style=flat-square" alt="Qoder AI"/><br/><sub>Kimi-K2, DeepSeek-R1<br/>Unlimited FREE</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/Pollinations-10B981?style=flat-square" alt="Pollinations"/><br/><sub>GPT-5, Claude, Llama 4<br/>No key needed</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/LongCat-FF7A00?style=flat-square" alt="LongCat"/><br/><sub>Flash-Lite<br/>50M tokens/day 🔥</sub></td>
</tr>
<tr>
<td align="center" width="150"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/cloudflare-color.svg" width="44" alt="Cloudflare AI"/><br/><b>Cloudflare AI</b><br/><sub>50+ models<br/>10K neurons/day</sub></td>
<td align="center" width="150"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/nvidia-color.svg" width="44" alt="NVIDIA NIM"/><br/><b>NVIDIA NIM</b><br/><sub>129 models<br/>~40 RPM free</sub></td>
<td align="center" width="150"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/cerebras-color.svg" width="44" alt="Cerebras"/><br/><b>Cerebras</b><br/><sub>Qwen3 235B<br/>1M tokens/day</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/Cloudflare_AI-F38020?style=flat-square&logo=cloudflare&logoColor=white" alt="Cloudflare AI"/><br/><sub>50+ models<br/>10K neurons/day</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/Gemini_CLI-8E75B2?style=flat-square&logo=googlegemini&logoColor=white" alt="Gemini CLI"/><br/><sub>gemini-3-flash<br/>180K/mo free</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/NVIDIA_NIM-76B900?style=flat-square&logo=nvidia&logoColor=white" alt="NVIDIA NIM"/><br/><sub>129 models<br/>~40 RPM free</sub></td>
<td align="center" width="150"><img src="https://img.shields.io/badge/Cerebras-F15A29?style=flat-square" alt="Cerebras"/><br/><sub>Qwen3 235B<br/>1M tokens/day</sub></td>
</tr>
</table>
@@ -459,16 +357,16 @@ Result: 4 layers of fallback = zero downtime
> Same app, your machine, your rules. From a global npm install to **your phone** via Termux.
| Platform | Install | Highlights |
| ------------------------- | ---------------------------------------- | --------------------------------------------------------- |
| 📦 **npm (global)** | `npm install -g omniroute` | One command, any OS |
| 🐳 **Docker** | `docker run … diegosouzapw/omniroute` | Multi-arch **AMD64 + ARM64** |
| 🖥️ **Desktop (Electron)** | `npm run electron:build` | Native window + system tray — **Windows / macOS / Linux** |
| 💪 **ARM** | native `arm64` | Raspberry Pi, ARM servers, Apple Silicon |
| 📱 **Android (Termux)** | `pkg install nodejs && npx -y omniroute` | Runs **on your phone**, 24/7, no root |
| 📲 **PWA** | "Add to Home Screen" | Fullscreen, offline, installable from browser |
| 🧩 **OpenCode plugin** | `@omniroute/opencode-provider` | Native OpenCode integration |
| 🛠️ **From source** | `npm install && npm run dev` | Hack on it, contribute |
| Platform | Install | Highlights |
| ------------------------- | -------------------------------------------- | --------------------------------------------------------- |
| 📦 **npm (global)** | `npm install -g omniroute` | One command, any OS |
| 🐳 **Docker** | `docker run … diegosouzapw/omniroute` | Multi-arch **AMD64 + ARM64** |
| 🖥️ **Desktop (Electron)** | `npm run electron:build` | Native window + system tray — **Windows / macOS / Linux** |
| 💪 **ARM** | native `arm64` | Raspberry Pi, ARM servers, Apple Silicon |
| 📱 **Android (Termux)** | `pkg install nodejs-lts && npx -y omniroute` | Runs **on your phone**, 24/7, no root |
| 📲 **PWA** | "Add to Home Screen" | Fullscreen, offline, installable from browser |
| 🧩 **OpenCode plugin** | `@omniroute/opencode-provider` | Native OpenCode integration |
| 🛠️ **From source** | `npm install && npm run dev` | Hack on it, contribute |
<sub>📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) · [Desktop](electron/README.md) · [Termux](docs/guides/TERMUX_GUIDE.md) · [PWA](docs/guides/PWA_GUIDE.md) · [OpenCode](docs/frameworks/OPENCODE.md)</sub>
@@ -498,7 +396,7 @@ Result: 4 layers of fallback = zero downtime
</div>
> OmniRoute isn't just a server — it's a **full command-line cockpit** with **80+ commands**, plus open agent protocols so an AI agent can drive OmniRoute **by itself**.
> OmniRoute isn't just a server — it's a **full command-line cockpit** with **60+ commands**, plus open agent protocols so an AI agent can drive OmniRoute **by itself**.
### ⌨️ A real CLI (not just `start`)
@@ -509,22 +407,6 @@ omniroute setup # guided first-run wizard
omniroute doctor # diagnose providers, ports, native deps
```
### 🛰️ Remote mode — run the CLI here, OmniRoute on a VPS
OmniRoute on a server? Drive it from your laptop with the **same CLI**. Log in once
with a scoped access token; every command then targets the remote.
```bash
omniroute connect 192.168.0.15 # password → scoped token, saved as a context
omniroute models list # ← runs against the REMOTE server
omniroute configure codex # ← picks a remote model, writes a local Codex profile
omniroute tokens create --name ci --scope read # mint narrower tokens for other machines
omniroute contexts use default # ← switch back to the local server
```
Tokens are scoped `read` / `write` / `admin`; process-spawning routes stay loopback-only.
<sub>📖 [Remote Mode](docs/guides/REMOTE-MODE.md)</sub>
<div align="center">
`providers` · `oauth` · `keys` · `combo` · `nodes` · `models` · `cache` · `compression` · `cost` · `usage` · `quota` · `health` · `resilience` · `telemetry` · `logs` · `audit` · `mcp` · `a2a` · `cloud` · `memory` · `skills` · `eval` · `tunnel` · `backup` · `sync` · `webhooks` · `policy` · `pricing` · `translator` · `simulate`
@@ -538,9 +420,9 @@ Expose OmniRoute over **MCP** or **A2A** and any capable agent gets the keys to
| Protocol | Endpoint | Use it for |
| ------------------ | ----------------------------------------------- | ------------------------------------------------------ |
| 🧰 **MCP (stdio)** | `omniroute --mcp` | Plug into Claude Desktop, Cursor, any MCP client |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **95 tools**, 30 scopes, full audit trail |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **37 tools**, 13 scopes, full audit trail |
| 📡 **MCP (SSE)** | `http://localhost:20128/api/mcp/sse` | Streaming MCP transport |
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 6 skills |
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 5 skills |
```bash
# Give Claude Code the full OmniRoute toolset over MCP:
@@ -557,26 +439,7 @@ claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp
</div>
> **Why use many tokens when few tokens do the trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It's now a **stack of 10 composable engines** that run in order and mix & match per routing combo — building on ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 78K+), [LLMLingua-2](https://github.com/microsoft/LLMLingua), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR).
### 🧱 The 10-engine stack
Engines run in pipeline order; each is independently toggleable and configurable per combo:
| # | Engine | What it does |
| --- | ----------------- | ------------------------------------------------------------------- |
| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) |
| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand |
| 3 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) |
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays (~30%+) |
| 5 | **Relevance** | Extractive sentence scoring against the last user query |
| 6 | **Caveman** | Rule-based prose compression (~6575% on output) |
| 7 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async |
| 8 | **Lite** | Whitespace + image-URL trimming (latency-light baseline) |
| 9 | **Aggressive** | Summarization + progressive aging of old turns |
| 10 | **Ultra** | Heuristic token pruning with an optional small-model (SLM) tier |
Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines:
> **Why use many token when few token do trick?** Every request passes through OmniRoute's compression pipeline **transparently** — no client changes. It stacks ideas from [RTK](https://github.com/rtk-ai/rtk), [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 51K+), and [Troglodita](https://github.com/leninejunior/troglodita) (PT-BR).
| Mode | Savings | Best for |
| ------------------------------ | ---------- | --------------------------- |
@@ -605,10 +468,10 @@ Code blocks, URLs and structured data are **always preserved** byte-perfect. **O
<br/>
### 📖 How it works — pipeline, architecture & savings math
### 📖 How it works — pipeline, architecture & savings math</b></summary>
```
Client (10,000 tok) ──▶ OmniRoute Compression (10 engines) ──▶ Provider (~1,080 tok, up to 95% saved)
Client (10,000 tok) ──▶ OmniRoute Compression (7 options) ──▶ Provider (~1,080 tok, up to 95% saved)
```
Default stacked combo runs `RTK → Caveman`. When both act on the same tool/context payload, savings compound:
@@ -619,20 +482,7 @@ average = 1 (1 0.80) × (1 0.46) = 89.2%
range = 78.4 94.6%
```
Code blocks, URLs, JSON and structured data are **always protected** by the preservation engine.
### 🎚️ Beyond the engines — output styles, the adaptive dial & per-request control
The 10 engines above shrink what goes **in**. Three more layers shape **how**, **when**, and what comes **out**:
- **🪄 Output Styles** _(output-axis steering)_ — inject deterministic, cache-safe response-shaping instructions; combinable, each at `lite` / `full` / `ultra` intensity. Adding a style is a one-line registry entry:
- **Terse prose** — drop filler / articles / hedging; keep technical substance exact.
- **Less code** — "lazy senior dev" YAGNI: smallest working change, no unrequested scaffolding.
- **Terse CJK (文言)** — classical-Chinese ultra-terse style (locale-gated to `zh`).
- **🎯 Adaptive context-budget** _(the dial)_ — instead of one on/off token threshold, escalate the cheapest, most-lossless engines only as far as needed to **fit the model's context window**. Policy: `reserve-output` (default, model-aware) · `percentage` · `absolute`. Mode: `floor` (guarantee fit) · `replace-autotrigger` (your explicit choice wins) · `off` (legacy threshold).
- **🎛️ Where compression is decided** _(precedence, high → low)_ — per-request `x-omniroute-compression` header routing-combo override active named profile adaptive / auto-trigger panel default off. The applied plan echoes back in the `X-OmniRoute-Compression: <mode>; source=<source>` response header.
Auto-trigger by token threshold, flip on the adaptive dial, pin a named profile, set a one-off per request, or assign a pipeline per routing combo — whichever fits the workload. An opt-in offline **eval harness** (`npm run eval:compression`) scores fidelity vs. savings on a pinned corpus before you promote a change.
Code blocks, URLs, JSON and structured data are **always protected** by the preservation engine. Auto-trigger compression by token threshold, or assign a compression pipeline per routing combo.
📖 [`COMPRESSION_GUIDE.md`](docs/compression/COMPRESSION_GUIDE.md) · [`RTK_COMPRESSION.md`](docs/compression/RTK_COMPRESSION.md) · [`COMPRESSION_ENGINES.md`](docs/compression/COMPRESSION_ENGINES.md)
@@ -655,7 +505,7 @@ Dashboard at `http://localhost:20128` · API at `http://localhost:20128/v1`.
**2) Connect a FREE provider (no signup)**
Dashboard → **Providers** → connect **Kiro AI** (free Claude, ~50 credits/month per account) or **OpenCode Free** (no auth) → done.
Dashboard → **Providers** → connect **Kiro AI** (free Claude unlimited) or **OpenCode Free** (no auth) → done.
**3) Point your coding tool**
@@ -688,7 +538,7 @@ Use these only for clients that cannot attach `Authorization: Bearer ...`. Heade
<br/>
## 📦 More install methods — Docker, source, pnpm, Arch
## 📦 More install methods — Docker, source, pnpm, Arch</b></summary>
**🐳 Docker**
@@ -707,7 +557,7 @@ PORT=20128 npm run dev
**📦 pnpm**
```bash
pnpm add -g omniroute@latest --allow-build=better-sqlite3 --allow-build=@swc/core && omniroute
pnpm install -g omniroute && pnpm approve-builds -g && omniroute
```
**🐧 Arch Linux (AUR)**
@@ -745,23 +595,7 @@ podman compose --profile base up -d
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
**⚡ Faster / leaner install (skip the native build)**
The native SQLite engine (`better-sqlite3`) is an **optional** dependency, so a global
install never blocks on compiling from source: it uses a prebuilt binary when one matches
your platform/Node, and otherwise falls back transparently to a pure-JS engine
(`node:sqlite` on Node 22+, else the bundled `sql.js` WASM) — no build tools required.
To skip the post-install native warm-up entirely (CI, headless, or slow machines):
```bash
OMNIROUTE_SKIP_POSTINSTALL=1 npm install -g omniroute # CI=1 also skips it
```
For the fastest installs prefer **pnpm** (content-addressed store + hard links — see above).
For a dashboard-free, headless runtime use the Docker `base` profile (above) or the
[Termux guide](docs/guides/TERMUX_GUIDE.md). The CLI and the web dashboard are served by the
same process on one port, so there is no separate CLI-only package today.
</details>
<br/>
@@ -817,16 +651,16 @@ same process on one port, so there is no separate CLI-only package today.
**The $0 Free Stack — combine into one unbreakable combo:**
| Provider | Prefix | Free models | Quota |
| ----------------- | ----------- | ----------------------------------------------- | ------------------ |
| **Kiro** | `kr/` | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | 50 credits/mo |
| **Qoder** | `if/` | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 | ♾️ Unlimited |
| **Qwen** | `qw/` | qwen3-coder-plus/flash/next | ♾️ Unlimited |
| **Pollinations** | `pol/` | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No key needed |
| **LongCat** | `lc/` | LongCat-2.0 | 10M one-time (KYC) |
| **Cloudflare AI** | `cf/` | 50+ models | 10K neurons/day |
| **NVIDIA NIM** | `nvidia/` | 129 models | ~40 RPM |
| **Cerebras** | `cerebras/` | Qwen3 235B, GPT-OSS 120B | 1M tok/day |
| Provider | Prefix | Free models | Quota |
| ----------------- | ----------- | ----------------------------------------------- | ----------------- |
| **Kiro** | `kr/` | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | 50 credits/mo |
| **Qoder** | `if/` | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 | ♾️ Unlimited |
| **Qwen** | `qw/` | qwen3-coder-plus/flash/next | ♾️ Unlimited |
| **Pollinations** | `pol/` | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No key needed |
| **LongCat** | `lc/` | LongCat-Flash-Lite | 50M tokens/day 🔥 |
| **Cloudflare AI** | `cf/` | 50+ models | 10K neurons/day |
| **NVIDIA NIM** | `nvidia/` | 129 models | ~40 RPM |
| **Cerebras** | `cerebras/` | Qwen3 235B, GPT-OSS 120B | 1M tok/day |
> 💡 The dashboard "cost" is a **savings tracker**, not a bill — OmniRoute never charges you. A "$290 total cost" using free models means **$290 saved**.
@@ -842,10 +676,10 @@ same process on one port, so there is no separate CLI-only package today.
**$0 forever:**
```
1. kr/claude-sonnet-4.5 (Kiro — ~50 credits/mo per acct)
1. kr/claude-sonnet-4.5 (Kiro — unlimited)
2. if/kimi-k2-thinking (Qoder — unlimited)
3. pol/gpt-5 (Pollinations — no key)
4. lc/LongCat-2.0 (10M one-time backup, KYC)
4. lc/longcat-flash-lite (50M tok/day backup)
Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
```
@@ -875,13 +709,11 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<br/>
**Routing:** 17 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Routing:** 14 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Compatibility:** OpenAI ↔ Claude ↔ Gemini ↔ Responses API · auto OAuth refresh (PKCE, 8 providers) · multi-account round-robin · Batch + Files API · live OpenAPI 3.0.
**Protocols:** MCP (95 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Plugins:** custom plugin marketplace (system-configured registry URL with SSRF-guarded fetch) · install / enable / disable · Notion + Obsidian knowledge-base integrations (WebDAV file server, vault search, note CRUD).
**Embedded services:** one-click install & lifecycle management of local sidecar services (CLIProxy, NineRouter).
**Protocols:** MCP (37 tools, 3 transports, 13 scopes) · A2A (JSON-RPC 2.0, SSE, skills) · ACP · cloud agents (Codex, Devin, Jules).
**Quality & Ops:** built-in **Evals** (golden-set: exact/contains/regex/custom) · guardrails (PII, injection, vision) · health dashboard · p50/p95/p99 telemetry · webhooks · compliance audit.
**AI Agent Skills:** drop-in markdown manifests — point any agent at a `skills/*/SKILL.md` manifest. 43 skills available.
**AI Agent Skills:** drop-in markdown manifests — point any agent at `skills/omniroute/SKILL.md`. 10 skills available.
📖 [MCP Server](open-sse/mcp-server/README.md) · [A2A Server](src/lib/a2a/README.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) · [Features Gallery](docs/guides/FEATURES.md)
@@ -899,9 +731,9 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| `DATA_DIR` | `~/.omniroute` | Database & config storage |
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
**Are FREE providers really unlimited?** Yes — Kiro, Qoder, Pollinations, LongCat, Cloudflare. No catch.
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 177 providers.
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
@@ -919,7 +751,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
| OAuth token expired | Auto-refreshed; if stuck, delete + re-auth in Providers |
| `unsupported_country_region_territory` | Configure proxy in Settings → Proxy |
| Docker SQLite locks | Use `--stop-timeout 40` for clean WAL checkpoint |
| Node runtime errors | Use Node `>=22.0.0 <23` or `>=24.0.0 <27` |
| Node runtime errors | Use Node `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24 <25` |
🐛 **Reporting a bug?** Run `npm run system-info` and attach `system-info.txt`. 📖 [`docs/guides/TROUBLESHOOTING.md`](docs/guides/TROUBLESHOOTING.md)
@@ -963,15 +795,15 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
</div>
- **Runtime**: Node.js 22.x or 24.x LTS (24 LTS recommended)`>=22.0.0 <23 || >=24.0.0 <27`
- **Language**: TypeScript 6.0**100% TypeScript** across `src/` and `open-sse/` (zero `any` in core modules since v2.0)
- **Runtime**: Node.js 20.20.2+, 22.22.2+, or 24.x LTS (24 LTS recommended)
- **Language**: TypeScript 5.9**100% TypeScript** across `src/` and `open-sse/` (zero `any` in core modules since v2.0)
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
- **Database**: better-sqlite3 (SQLite) + LowDB (JSON legacy) — domain state, proxy logs, MCP audit, routing decisions, memory, skills
- **Schemas**: Zod (MCP tool I/O validation, API contracts)
- **Protocols**: MCP (stdio/HTTP) + A2A v0.3 (JSON-RPC 2.0 + SSE)
- **Streaming**: Server-Sent Events (SSE) + WebSocket bridge (`/v1/ws`)
- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys + MCP Scoped Authorization
- **Testing**: Node.js test runner + Vitest (**21,000+ test cases** across 2,586 files — unit, integration, E2E, security, ecosystem)
- **Testing**: Node.js test runner + Vitest (**4,690+ test cases** across 517 files — unit, integration, E2E, security, ecosystem)
- **Platforms**: Desktop (Electron), Android (Termux), PWA (any browser)
- **CI/CD**: GitHub Actions (auto npm publish + Docker Hub on release)
- **Website**: [omniroute.online](https://omniroute.online)
@@ -989,66 +821,63 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
### 📘 Getting Started
| Document | Description |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
| [Remote Mode](docs/guides/REMOTE-MODE.md) | Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens |
| [Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md) | Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles |
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
| Document | Description |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
### 🔧 Operations & Deployment
| Document | Description |
| -------------------------------------------------------- | -------------------------------------------------------------- |
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
| Document | Description |
| ---------------------------------------------------- | -------------------------------------------------------------- |
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
| [PWA Guide](docs/guides/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
| [PWA Guide](docs/guides/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
### 🧠 Features & Architecture
| Document | Description |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture, data flow, and internals |
| [Compression Guide](docs/compression/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
| [RTK Compression](docs/compression/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
| [Compression Engines](docs/compression/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 9-factor scoring, mode packs, self-healing |
| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
| Document | Description |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture, data flow, and internals |
| [Compression Guide](docs/compression/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
| [RTK Compression](docs/compression/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
| [Compression Engines](docs/compression/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing |
| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
### 🤖 Protocols & APIs
| Document | Description |
| ------------------------------------------------- | --------------------------------------------------- |
| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
| [MCP Server](open-sse/mcp-server/README.md) | 95 MCP tools, IDE configs, Python/TS/Go clients |
| [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference |
| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
| [A2A Server Guide](docs/frameworks/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
| Document | Description |
| ------------------------------------------- | --------------------------------------------------- |
| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
| [OpenAPI Spec](docs/reference/openapi.yaml) | OpenAPI 3.0 specification |
| [MCP Server](open-sse/mcp-server/README.md) | 29 MCP tools, IDE configs, Python/TS/Go clients |
| [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference |
| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
| [A2A Server Guide](docs/frameworks/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
### 📋 Project & Quality
| Document | Description |
| -------------------------------------------------- | ----------------------------------------------- |
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
| [Changelog](CHANGELOG.md) | Full per-version release history |
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
| [i18n Guide](docs/guides/I18N.md) | 40+ language support, translation workflow, RTL |
| Document | Description |
| ---------------------------------------------- | ----------------------------------------------- |
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
| [i18n Guide](docs/guides/I18N.md) | 40+ language support, translation workflow, RTL |
| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 21,000+ test suite |
| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 4,690+ test suite |
<br/>
@@ -1062,23 +891,23 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<tr>
<td align="center" width="160">
<a href="https://github.com/oyi77">
<img src="https://github.com/oyi77.png" width="40" style="border-radius:50%" alt="oyi77"/><br/>
<img src="https://github.com/oyi77.png" width="80" style="border-radius:50%" alt="oyi77"/><br/>
<b>oyi77</b>
</a><br/>
<sub>🥇 189 commits • +155K lines</sub><br/>
<sub>🥇 190 commits • +72K lines</sub><br/>
<sub>Analytics engine, SQL aggregations,<br/>proxy marketplace, test coverage</sub>
</td>
<td align="center" width="160">
<a href="https://github.com/christopher-s">
<img src="https://github.com/christopher-s.png" width="40" style="border-radius:50%" alt="Chris Staley"/><br/>
<img src="https://github.com/christopher-s.png" width="80" style="border-radius:50%" alt="Chris Staley"/><br/>
<b>Chris Staley</b>
</a><br/>
<sub>🥈 70 commits • +5.7K lines</sub><br/>
<sub>🥈 72 commits • +5.7K lines</sub><br/>
<sub>SSE stream hardening, Responses API,<br/>Gemini pagination, test regression fixes</sub>
</td>
<td align="center" width="160">
<a href="https://github.com/zenobit">
<img src="https://github.com/zenobit.png" width="40" style="border-radius:50%" alt="zenobit"/><br/>
<img src="https://github.com/zenobit.png" width="80" style="border-radius:50%" alt="zenobit"/><br/>
<b>zenobit</b>
</a><br/>
<sub>🥉 62 commits • +24K lines</sub><br/>
@@ -1086,28 +915,20 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
</td>
<td align="center" width="160">
<a href="https://github.com/rdself">
<img src="https://github.com/rdself.png" width="40" style="border-radius:50%" alt="R.D. & Randi"/><br/>
<img src="https://github.com/rdself.png" width="80" style="border-radius:50%" alt="R.D. & Randi"/><br/>
<b>R.D. & Randi</b>
</a><br/>
<sub>🏅 108 commits • +30K lines</sub><br/>
<sub>🏅 107 commits • +28K lines</sub><br/>
<sub>Endpoints page, tunnel integrations,<br/>Docker workflows, A2A status, compression UI</sub>
</td>
<td align="center" width="160">
<a href="https://github.com/benzntech">
<img src="https://github.com/benzntech.png" width="40" style="border-radius:50%" alt="benzntech"/><br/>
<img src="https://github.com/benzntech.png" width="80" style="border-radius:50%" alt="benzntech"/><br/>
<b>benzntech</b>
</a><br/>
<sub>🏅 22 commits • +7.5K lines</sub><br/>
<sub>🏅 20 commits • +7.5K lines</sub><br/>
<sub>Electron desktop app, auto-updater,<br/>release build workflows, cross-platform CI</sub>
</td>
<td align="center" width="160">
<a href="https://github.com/herjarsa">
<img src="https://github.com/herjarsa.png" width="40" style="border-radius:50%" alt="herjarsa"/><br/>
<b>herjarsa</b>
</a><br/>
<sub>🏅 21 commits • +6K lines</sub><br/>
<sub>Zero-latency combos, vision-bridge auto-routing,<br/>catalog context-length, resilience 429 hints</sub>
</td>
</tr>
</table>
@@ -1121,11 +942,11 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<div align="center">
## 👥 280+ Contributors
## 👥 Contributors
</div>
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=200&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
[![Contributors](https://contrib.rocks/image?repo=diegosouzapw/OmniRoute&max=100&columns=20&anon=1)](https://github.com/diegosouzapw/OmniRoute/graphs/contributors)
### How to Contribute
@@ -1182,86 +1003,15 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes
</div>
OmniRoute stands on the shoulders of giants. It started as a fork of **[9router](https://github.com/decolua/9router)** and a TypeScript port of the Go project **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — and from there, every subsystem below was inspired by an open-source project that got there first. Each one shaped a concrete piece of OmniRoute. This is our thank-you to all of them. 🙏
Special thanks to **[9router](https://github.com/decolua/9router)** by **[decolua](https://github.com/decolua)** — the original project that inspired this fork. OmniRoute builds upon that incredible foundation with additional features, multi-modal APIs, and a full TypeScript rewrite.
> ⭐ star counts as of June 2026 — go give these projects a star.
Special thanks to **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** by **[router-for-me](https://github.com/router-for-me)** — the original Go implementation that inspired this JavaScript port.
### 🧬 Lineage & gateway
Special thanks to **[Caveman](https://github.com/JuliusBrussee/caveman)** by **[JuliusBrussee](https://github.com/JuliusBrussee)** (⭐ 51K+) — the viral "why use many token when few token do trick" project whose caveman-speak compression philosophy inspired OmniRoute's standard compression mode and 30+ filler/condensation regex rules.
| Project | ⭐ | How it inspired OmniRoute |
| ------------------------------------------------------------------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------- |
| **[9router](https://github.com/decolua/9router)** · decolua | 19.0k | The original project this fork is built on — extended here with multi-modal APIs and a full TypeScript rewrite. |
| **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** · router-for-me | 38.8k | The Go implementation that inspired this JavaScript / TypeScript port. |
| **[LiteLLM](https://github.com/BerriAI/litellm)** · BerriAI | 52.1k | The AI gateway whose public pricing dataset feeds our cost-tracking sync and whose provider-normalization model informed our routing. |
Special thanks to **[RTK - Rust Token Killer](https://github.com/rtk-ai/rtk)** by **[RTK AI](https://github.com/rtk-ai)** — the high-performance command-output compression project whose terminal, build, test, git, and tool-output filtering model inspired OmniRoute's RTK engine, JSON filter DSL, raw-output recovery, and stacked RTK → Caveman compression pipeline.
### 🗜️ Context & token compression — engines
| Project | ⭐ | How it inspired OmniRoute |
| ----------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[Caveman](https://github.com/JuliusBrussee/caveman)** · JuliusBrussee | 78.2k | The viral "why use many token when few token do trick" project — its caveman-speak philosophy powers our standard compression mode and 30+ filler/condensation rules. |
| **[RTK Rust Token Killer](https://github.com/rtk-ai/rtk)** · rtk-ai | 67.3k | High-performance command-output compression — inspired our RTK engine, JSON filter DSL, raw-output recovery and the stacked RTK → Caveman pipeline. |
| **[headroom](https://github.com/headroomlabs-ai/headroom)** · headroomlabs-ai | 54.5k | Reversible context-compression (SmartCrusher) — inspired our `headroom` engine and the `ccr` retrieve-marker pattern. |
| **[LLMLingua](https://github.com/microsoft/LLMLingua)** · Microsoft | 6.4k | Prompt-compression research (LLMLingua / LLMLingua-2) — inspired our async, code-safe, fail-open `llmlingua` engine. |
| **[llmlingua-2-js](https://github.com/atjsh/llmlingua-2-js)** · atjsh | 28 | The JS/ONNX port (MobileBERT / XLM-RoBERTa) used as the worker-thread backend for our LLMLingua engine. |
| **[Troglodita](https://github.com/leninejunior/troglodita)** · Lenine Júnior | 16 | PT-BR token compression — powers our pt-BR language pack: pleonasm reduction and filler removal tuned for Brazilian-Portuguese grammar. |
| **[ponytail](https://github.com/DietrichGebert/ponytail)** · DietrichGebert | 68.8k | The viral "lazy senior dev" YAGNI-coder skill — inspired our **less-code** Output Style: smallest-working-change steering that cuts _generated_ code (the output-axis sibling to Caveman's terse prose). |
### 🧩 Compact formats, token research & code-aware tooling
| Project | ⭐ | How it inspired OmniRoute |
| ---------------------------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.7k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. |
| **[GCF Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. |
| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 421 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. |
| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 1.0k | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. |
| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 110 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. |
| **[token-optimizer](https://github.com/alexgreensh/token-optimizer)** · alexgreensh | 1.5k | "Find the ghost tokens" — its offload + recoverable-handle pattern informed our CCR offload thinking. |
| **[TokenMizer](https://github.com/Shweta-Mishra-ai/tokenmizer)** · Shweta-Mishra-ai | 2 | A session-graph + cross-turn line-dedup blueprint that informed our session-dedup design. |
| **[OmniCompress](https://github.com/jessefreitas/OmniCompress)** · jessefreitas | 2 | Rust columnar-JSON + content-addressed retrieve + cross-message dedup — validated our `headroom`/`ccr`/`session-dedup` engine design and the cache-stable "compressed form is position-independent" invariant. |
| **[mcp-compressor](https://github.com/atlassian-labs/mcp-compressor)** · Atlassian Labs | 89 | MCP tool-schema/description compression — informed our MCP tool-manifest cardinality reduction. |
| **[RepoMapper](https://github.com/pdavis68/RepoMapper)** · pdavis68 | 181 | Aider-style repo-map ranking — informed our repo-map / retrieval-ranking exploration. |
| **[quiet-shell-mcp](https://github.com/mrsimpson/quiet-shell-mcp)** · mrsimpson | 4 | Declarative shell-output reduction over MCP — validated our declarative bash-output compaction. |
| **[ts-morph](https://github.com/dsherret/ts-morph)** · David Sherret | 6.1k | TypeScript Compiler API toolkit — inspired our parser-based comment removal that preserves string, template and regex literals. |
### 🧠 Memory & RAG
| Project | ⭐ | How it inspired OmniRoute |
| ------------------------------------------------------------------ | ----: | ------------------------------------------------------------------------------------------------------------------- |
| **[Mem0](https://github.com/mem0ai/mem0)** · mem0ai | 59.8k | Universal memory layer — its proxy-as-write/read-boundary model shaped our memory architecture. |
| **[Letta (MemGPT)](https://github.com/letta-ai/letta)** · letta-ai | 23.6k | Stateful agents with tiered memory — inspired our Context Control & Recovery (CCR) tiered model. |
| **[WFGY](https://github.com/onestardao/WFGY)** · onestardao | 1.8k | The ProblemMap taxonomy of 16 recurring RAG/LLM failure modes — the shared vocabulary in our troubleshooting guide. |
### 🛰️ Traffic inspection, MITM & transparent proxy
| Project | ⭐ | How it inspired OmniRoute |
| --------------------------------------------------------------------------------- | ---: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[llm-interceptor](https://github.com/chouzz/llm-interceptor)** · chouzz | 48 | MITM interception/analysis of coding-assistant ↔ LLM traffic — our Traffic Inspector ports its SSE merge, conversation normalization, host passthrough and secret masking (MIT). |
| **[ProxyBridge](https://github.com/InterceptSuite/ProxyBridge)** · InterceptSuite | 5.3k | Transparent per-process proxy routing — inspired our crash-safe MITM teardown, socket idle-timeouts, `/proc` process attribution and TPROXY capture. |
### 📚 Model data, observability & UI
| Project | ⭐ | How it inspired OmniRoute |
| -------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------- |
| **[models.dev](https://github.com/anomalyco/models.dev)** · SST / OpenCode | 5.6k | Open database of AI model specs, pricing and capabilities — synced natively into our model catalog. |
| **[React Flow / xyflow](https://github.com/xyflow/xyflow)** · xyflow | 37.4k | The node-based graph library powering our real-time Compression Studio and Combo/Routing Studio. |
| **[LangGraph](https://github.com/langchain-ai/langgraph)** · LangChain | 36.1k | LangGraph Studio's live workflow-graph visualization inspired our Studios' real-time cascade view. |
| **[Langfuse](https://github.com/langfuse/langfuse)** · Langfuse | 30.1k | Its trace → span → generation observability model shaped our Compression Studio waterfall. |
| **[Kiali](https://github.com/kiali/kiali)** · Kiali | 3.6k | Istio service-mesh observability — inspired our circuit-breaker badges and error-edge visuals in the Routing/Combo Studio. |
| **[lobe-icons](https://github.com/lobehub/lobe-icons)** · LobeHub | 2.2k | AI/LLM brand logos that render the provider icons across our dashboard. |
### 🛡️ Security
| Project | ⭐ | How it inspired OmniRoute |
| ------------------------------------------------------------------------------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **[awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)** · tldrsec | 708 | A curated list of secure-by-default libraries that guides our security choices (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink). |
## ❤️ Support
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:
-**Star the repo** — it genuinely helps visibility
- 💖 **[GitHub Sponsors](https://github.com/sponsors/diegosouzapw)** — fund ongoing maintenance and new providers
- 🐛 **Report bugs and share feedback** in [Discussions](https://github.com/diegosouzapw/OmniRoute/discussions)
Special thanks to **[Troglodita](https://github.com/leninejunior/troglodita)** by **[Lenine Júnior](https://github.com/leninejunior)** — the PT-BR token compression project ("por que gastar muitos tokens quando poucos resolve?") whose Portuguese-native rules power OmniRoute's pt-BR language pack: pleonasm reduction, filler removal tuned for Brazilian Portuguese grammar, and technical abbreviations for the dev BR community.
## 📄 License
@@ -1273,7 +1023,7 @@ MIT License - see [LICENSE](LICENSE) for details.
**[⬆ Back to top](#-omniroute)** · Built with ❤️ for the open-source AI community.
<sub>OmniRoute v3.8.43 · Node ≥22.0.0 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
<sub>OmniRoute v3.8.6 · Node ≥22.22.2 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
</div>
<!-- GitHub Discussions enabled for community Q&A -->

View File

@@ -113,14 +113,14 @@ PII_REDACTION_ENABLED=true
### 🌐 Network Security
| Feature | Description |
| ------------------------ | ------------------------------------------------------------------------------ |
| **CORS** | Explicit cross-origin allowlist (`CORS_ALLOWED_ORIGINS`; legacy `CORS_ORIGIN`) |
| **IP Filtering** | Allowlist/blocklist IP ranges in dashboard |
| **Rate Limiting** | Per-provider rate limits with automatic backoff |
| **Anti-Thundering Herd** | Mutex + per-connection locking prevents cascading 502s |
| **TLS Fingerprint** | Browser-like TLS fingerprint spoofing to reduce bot detection |
| **CLI Fingerprint** | Per-provider header/body ordering to match native CLI signatures |
| Feature | Description |
| ------------------------ | ---------------------------------------------------------------- |
| **CORS** | Configurable origin control (`CORS_ORIGIN` env var, default `*`) |
| **IP Filtering** | Allowlist/blocklist IP ranges in dashboard |
| **Rate Limiting** | Per-provider rate limits with automatic backoff |
| **Anti-Thundering Herd** | Mutex + per-connection locking prevents cascading 502s |
| **TLS Fingerprint** | Browser-like TLS fingerprint spoofing to reduce bot detection |
| **CLI Fingerprint** | Per-provider header/body ordering to match native CLI signatures |
### 🔌 Resilience & Availability

View File

@@ -1,70 +0,0 @@
# bin/_ops-common.sh — shared helpers for the OmniRoute ops runbook scripts.
#
# Sourced (not executed) by rollback.sh / snapshot-data.sh / restore-data.sh /
# restore-policies.sh / cold-start-bench.sh — the self-hoster incident-recovery
# and cold-start ops tooling. Each script documents its own contract via --help.
#
# Path resolution mirrors the app (src/lib/db/core.ts): the SQLite store is
# $DATA_DIR/storage.sqlite and managed backups go to $DATA_DIR/db_backups
# (overridable via DB_BACKUPS_DIR), so snapshots created here are interchangeable
# with the ones the server writes on migrations.
# Recompute the data-dir-derived paths. Called once on source, and again by
# scripts that accept a --data-dir override.
ops_set_data_dir() {
OMNIROUTE_DATA_DIR="$1"
OMNIROUTE_SQLITE="${OMNIROUTE_DATA_DIR}/storage.sqlite"
OMNIROUTE_BACKUPS_DIR="${DB_BACKUPS_DIR:-${OMNIROUTE_DATA_DIR}/db_backups}"
}
ops_set_data_dir "${DATA_DIR:-$HOME/.omniroute}"
ops_log() { printf '[%s] %s\n' "${SCRIPT_NAME:-ops}" "$*" >&2; }
ops_die() {
printf '[%s] ERROR: %s\n' "${SCRIPT_NAME:-ops}" "$*" >&2
exit 1
}
ops_require_cmd() {
command -v "$1" >/dev/null 2>&1 || ops_die "required command not found: $1"
}
# ops_confirm "<prompt>" — return 0 to proceed. Honors ASSUME_YES=1 (set by the
# --yes flag) and REFUSES a destructive action on a non-interactive stdin unless
# ASSUME_YES is set, so an unattended/CI invocation can never silently destroy data.
ops_confirm() {
local prompt="$1" reply
if [ "${ASSUME_YES:-0}" = "1" ]; then return 0; fi
if [ ! -t 0 ]; then
ops_die "refusing a destructive action without a TTY; pass --yes to proceed non-interactively"
fi
read -r -p "$prompt [y/N] " reply
case "$reply" in
[yY] | [yY][eE][sS]) return 0 ;;
*) return 1 ;;
esac
}
# ops_find_snapshot <id> — resolve a snapshot identifier (a snapshot dir name,
# a bare timestamp/sha, or an explicit path) to a directory containing
# storage.sqlite. Echoes the resolved dir or dies.
ops_find_snapshot() {
local id="$1" cand
[ -n "$id" ] || ops_die "snapshot id required (a timestamp/sha, dir name, or path)"
for cand in \
"$id" \
"$id/" \
"$OMNIROUTE_BACKUPS_DIR/$id" \
"$OMNIROUTE_BACKUPS_DIR/snapshot_$id"; do
if [ -f "${cand%/}/storage.sqlite" ]; then
printf '%s\n' "${cand%/}"
return 0
fi
done
# Fall back to a prefix match against snapshot_* dirs (e.g. a short sha/date).
if [ -d "$OMNIROUTE_BACKUPS_DIR" ]; then
for cand in "$OMNIROUTE_BACKUPS_DIR"/snapshot_*"$id"*; do
[ -f "$cand/storage.sqlite" ] && { printf '%s\n' "$cand"; return 0; }
done
fi
ops_die "no snapshot matching '$id' under $OMNIROUTE_BACKUPS_DIR (run bin/snapshot-data.sh first)"
}

View File

@@ -1,70 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_agent_skills(parent) {
const tag = parent.command("agent-skills").description("Agent Skills endpoints");
tag.command("get-api-agent-skills")
.description("List agent skills catalog")
.option("--category <category>", "Filter by category (api = REST API skills, cli = CLI skills)")
.option("--area <area>", "Filter by area slug (e.g. \"providers\", \"models\", \"cli-serve\")")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills";
const qs = new URLSearchParams();
if (opts.category != null) qs.set("category", String(opts.category));
if (opts.area != null) qs.set("area", String(opts.area));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-id-")
.description("Get a single agent skill")
.requiredOption("--id <id>", "Canonical skill ID")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-id-raw")
.description("Get raw SKILL.md content")
.requiredOption("--id <id>", "Canonical skill ID")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/{id}/raw";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-coverage")
.description("Get SKILL.md coverage stats")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/coverage";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-agent-skills-generate")
.description("Trigger SKILL.md generator")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/generate";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,155 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_agentbridge(parent) {
const tag = parent.command("agentbridge").description("AgentBridge endpoints");
tag.command("get-api-tools-agent-bridge-agents")
.description("List all 9 IDE agents with current state")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-state")
.description("Get global AgentBridge server state")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/state";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-server")
.description("Control AgentBridge MITM server")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/server";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-agents-agent-id-dns")
.description("Enable or disable DNS for one agent")
.requiredOption("--agent-id <agentId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/dns";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-agents-agent-id-mappings")
.description("Get model mappings for one agent")
.requiredOption("--agent-id <agentId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/mappings";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-tools-agent-bridge-agents-agent-id-mappings")
.description("Update model mappings for one agent")
.requiredOption("--agent-id <agentId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/mappings";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-bypass")
.description("List bypass patterns (hosts never decrypted)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/bypass";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-tools-agent-bridge-bypass")
.description("Update user bypass patterns")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/bypass";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-cert")
.description("Download or regenerate the AgentBridge CA certificate")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/cert";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-upstream-ca")
.description("Get configured upstream CA cert path")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/upstream-ca";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-upstream-ca")
.description("Set upstream CA cert path for corporate TLS environments")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/upstream-ca";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_api_keys(parent) {
const tag = parent.command("api-keys").description("API Keys endpoints");
tag.command("get-api-keys")
tag
.command("get-api-keys")
.description("List API keys")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/keys";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-keys")
tag
.command("post-api-keys")
.description("Create API key")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,16 +32,26 @@ export function register_api_keys(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-keys-id-")
tag
.command("delete-api-keys-id-")
.description("Delete API key")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/keys/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_audio(parent) {
const tag = parent.command("audio").description("Audio endpoints");
tag.command("post-api-v1-audio-speech")
tag
.command("post-api-v1-audio-speech")
.description("Generate speech audio")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_audio(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-audio-transcriptions")
tag
.command("post-api-v1-audio-transcriptions")
.description("Transcribe audio")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -33,7 +40,12 @@ export function register_audio(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_chat(parent) {
const tag = parent.command("chat").description("Chat endpoints");
tag.command("post-api-v1-chat-completions")
tag
.command("post-api-v1-chat-completions")
.description("Create chat completion")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_chat(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-chat-completions")
tag
.command("post-api-v1-providers-provider-chat-completions")
.description("Create chat completion (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
@@ -35,11 +42,17 @@ export function register_chat(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-api-chat")
tag
.command("post-api-v1-api-chat")
.description("Ollama-compatible chat endpoint")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -51,7 +64,12 @@ export function register_chat(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,60 +1,86 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_cli_tools(parent) {
const tag = parent.command("cli-tools").description("CLI Tools endpoints");
tag.command("get-api-cli-tools-backups")
tag
.command("get-api-cli-tools-backups")
.description("List CLI tool backups")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/backups";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-backups")
tag
.command("post-api-cli-tools-backups")
.description("Create CLI tool backup")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/backups";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-runtime-tool-id-")
tag
.command("get-api-cli-tools-runtime-tool-id-")
.description("Get runtime status for a CLI tool")
.requiredOption("--tool-id <toolId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/runtime/{toolId}";
url = url.replace("{toolId}", encodeURIComponent(opts.toolId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-guide-settings-tool-id-")
tag
.command("get-api-cli-tools-guide-settings-tool-id-")
.description("Get guide settings for a tool")
.requiredOption("--tool-id <toolId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/guide-settings/{toolId}";
url = url.replace("{toolId}", encodeURIComponent(opts.toolId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-antigravity-mitm")
tag
.command("get-api-cli-tools-antigravity-mitm")
.description("Get Antigravity MITM proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-antigravity-mitm")
tag
.command("post-api-cli-tools-antigravity-mitm")
.description("Update Antigravity MITM proxy settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -66,29 +92,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-antigravity-mitm")
tag
.command("delete-api-cli-tools-antigravity-mitm")
.description("Reset Antigravity MITM proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-antigravity-mitm-alias")
tag
.command("get-api-cli-tools-antigravity-mitm-alias")
.description("Get Antigravity MITM alias configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm/alias";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cli-tools-antigravity-mitm-alias")
tag
.command("put-api-cli-tools-antigravity-mitm-alias")
.description("Update Antigravity MITM alias configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -100,20 +142,31 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-claude-settings")
tag
.command("get-api-cli-tools-claude-settings")
.description("Get Claude CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/claude-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-claude-settings")
tag
.command("post-api-cli-tools-claude-settings")
.description("Apply Claude CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -125,29 +178,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-claude-settings")
tag
.command("delete-api-cli-tools-claude-settings")
.description("Reset Claude CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/claude-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-cline-settings")
tag
.command("get-api-cli-tools-cline-settings")
.description("Get Cline CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/cline-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-cline-settings")
tag
.command("post-api-cli-tools-cline-settings")
.description("Apply Cline CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -159,29 +228,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-cline-settings")
tag
.command("delete-api-cli-tools-cline-settings")
.description("Reset Cline CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/cline-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-codex-profiles")
tag
.command("get-api-cli-tools-codex-profiles")
.description("Get Codex profiles")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-codex-profiles")
tag
.command("post-api-cli-tools-codex-profiles")
.description("Create Codex profile")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -193,11 +278,17 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cli-tools-codex-profiles")
tag
.command("put-api-cli-tools-codex-profiles")
.description("Update Codex profile")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -209,29 +300,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-codex-profiles")
tag
.command("delete-api-cli-tools-codex-profiles")
.description("Delete Codex profile")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-codex-settings")
tag
.command("get-api-cli-tools-codex-settings")
.description("Get Codex CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-codex-settings")
tag
.command("post-api-cli-tools-codex-settings")
.description("Apply Codex CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -243,29 +350,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-codex-settings")
tag
.command("delete-api-cli-tools-codex-settings")
.description("Reset Codex CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-droid-settings")
tag
.command("get-api-cli-tools-droid-settings")
.description("Get Droid CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/droid-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-droid-settings")
tag
.command("post-api-cli-tools-droid-settings")
.description("Apply Droid CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -277,29 +400,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-droid-settings")
tag
.command("delete-api-cli-tools-droid-settings")
.description("Reset Droid CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/droid-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-kilo-settings")
tag
.command("get-api-cli-tools-kilo-settings")
.description("Get Kilo CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/kilo-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-kilo-settings")
tag
.command("post-api-cli-tools-kilo-settings")
.description("Apply Kilo CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -311,29 +450,45 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-kilo-settings")
tag
.command("delete-api-cli-tools-kilo-settings")
.description("Reset Kilo CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/kilo-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-openclaw-settings")
tag
.command("get-api-cli-tools-openclaw-settings")
.description("Get OpenClaw CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/openclaw-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-openclaw-settings")
tag
.command("post-api-cli-tools-openclaw-settings")
.description("Apply OpenClaw CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -345,16 +500,26 @@ export function register_cli_tools(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-openclaw-settings")
tag
.command("delete-api-cli-tools-openclaw-settings")
.description("Reset OpenClaw CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/openclaw-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_cloud(parent) {
const tag = parent.command("cloud").description("Cloud endpoints");
tag.command("post-api-cloud-auth")
tag
.command("post-api-cloud-auth")
.description("Authenticate with cloud worker")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_cloud(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cloud-credentials-update")
tag
.command("put-api-cloud-credentials-update")
.description("Update cloud worker credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -33,11 +40,17 @@ export function register_cloud(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cloud-model-resolve")
tag
.command("post-api-cloud-model-resolve")
.description("Resolve model via cloud")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -49,20 +62,31 @@ export function register_cloud(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cloud-models-alias")
tag
.command("get-api-cloud-models-alias")
.description("Get cloud model aliases")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/models/alias";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cloud-models-alias")
tag
.command("put-api-cloud-models-alias")
.description("Update cloud model alias")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -74,7 +98,12 @@ export function register_cloud(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_combos(parent) {
const tag = parent.command("combos").description("Combos endpoints");
tag.command("get-api-combos")
tag
.command("get-api-combos")
.description("List routing combos")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-combos")
tag
.command("post-api-combos")
.description("Create routing combo")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,43 +32,68 @@ export function register_combos(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-combos-id-")
tag
.command("patch-api-combos-id-")
.description("Update combo")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/{id}";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-combos-id-")
tag
.command("delete-api-combos-id-")
.description("Delete combo")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-combos-metrics")
tag
.command("get-api-combos-metrics")
.description("Get combo metrics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/metrics";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-combos-test")
tag
.command("post-api-combos-test")
.description("Test a combo configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_compression(parent) {
const tag = parent.command("compression").description("Compression endpoints");
tag.command("get-api-settings-compression")
tag
.command("get-api-settings-compression")
.description("Get global compression settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/compression";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-compression")
tag
.command("put-api-settings-compression")
.description("Update global compression settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,11 +32,17 @@ export function register_compression(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-compression-preview")
tag
.command("post-api-compression-preview")
.description("Preview compression for a message payload")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -42,38 +54,59 @@ export function register_compression(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compression-language-packs")
tag
.command("get-api-compression-language-packs")
.description("List Caveman compression language packs")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compression/language-packs";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compression-rules")
tag
.command("get-api-compression-rules")
.description("List Caveman compression rule metadata")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compression/rules";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-config")
tag
.command("get-api-context-rtk-config")
.description("Get RTK compression settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/config";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-context-rtk-config")
tag
.command("put-api-context-rtk-config")
.description("Update RTK compression settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -85,20 +118,31 @@ export function register_compression(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-filters")
tag
.command("get-api-context-rtk-filters")
.description("List RTK filters and load diagnostics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/filters";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-context-rtk-test")
tag
.command("post-api-context-rtk-test")
.description("Run RTK compression preview for text")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -110,18 +154,28 @@ export function register_compression(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-raw-output-id-")
tag
.command("get-api-context-rtk-raw-output-id-")
.description("Read retained redacted RTK raw output")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/raw-output/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,202 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_embedded_services(parent) {
const tag = parent.command("embedded-services").description("Embedded Services endpoints");
tag.command("post-api-services-9router-install")
.description("Install 9Router from npm")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/install";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-start")
.description("Start 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/start";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-stop")
.description("Stop 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/stop";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-restart")
.description("Restart 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-update")
.description("Update 9Router to a newer npm version")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/update";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-rotate-key")
.description("Rotate the 9Router API key")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/rotate-key";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-9router-status")
.description("Get 9Router status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-auto-start")
.description("Toggle 9Router auto-start")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/auto-start";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-install")
.description("Install CLIProxyAPI from npm")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/install";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-start")
.description("Start CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/start";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-stop")
.description("Stop CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/stop";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-restart")
.description("Restart CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-update")
.description("Update CLIProxyAPI to a newer npm version")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/update";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-cliproxy-status")
.description("Get CLIProxyAPI status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-auto-start")
.description("Toggle CLIProxyAPI auto-start")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/auto-start";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-name-logs")
.description("Stream service logs via SSE")
.requiredOption("--name <name>", "")
.option("--tail <tail>", "Number of historical lines to include in the initial snapshot")
.option("--filter <filter>", "Case-insensitive substring filter applied to log lines. No regex — ReDoS-safe by design.")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/{name}/logs";
url = url.replace("{name}", encodeURIComponent(opts.name ?? ""));
const qs = new URLSearchParams();
if (opts.tail != null) qs.set("tail", String(opts.tail));
if (opts.filter != null) qs.set("filter", String(opts.filter));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_embeddings(parent) {
const tag = parent.command("embeddings").description("Embeddings endpoints");
tag.command("post-api-v1-embeddings")
tag
.command("post-api-v1-embeddings")
.description("Create embeddings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_embeddings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-embeddings")
tag
.command("post-api-v1-providers-provider-embeddings")
.description("Create embeddings (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
@@ -35,7 +42,12 @@ export function register_embeddings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_fallback(parent) {
const tag = parent.command("fallback").description("Fallback endpoints");
tag.command("get-api-fallback-chains")
tag
.command("get-api-fallback-chains")
.description("List fallback chains")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/fallback/chains";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-fallback-chains")
tag
.command("post-api-fallback-chains")
.description("Create fallback chain")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,11 +32,17 @@ export function register_fallback(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-fallback-chains")
tag
.command("delete-api-fallback-chains")
.description("Delete fallback chain")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -42,7 +54,12 @@ export function register_fallback(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "DELETE", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_images(parent) {
const tag = parent.command("images").description("Images endpoints");
tag.command("post-api-v1-images-generations")
tag
.command("post-api-v1-images-generations")
.description("Generate images")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_images(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-images-generations")
tag
.command("post-api-v1-providers-provider-images-generations")
.description("Generate images (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
@@ -35,7 +42,12 @@ export function register_images(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,251 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_memory(parent) {
const tag = parent.command("memory").description("Memory endpoints");
tag.command("get-api-memory")
.description("List memory entries")
.option("--api-key-id <apiKeyId>", "")
.option("--type <type>", "")
.option("--session-id <sessionId>", "")
.option("--q <q>", "")
.option("--limit <limit>", "")
.option("--page <page>", "")
.option("--offset <offset>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory";
const qs = new URLSearchParams();
if (opts.apiKeyId != null) qs.set("apiKeyId", String(opts.apiKeyId));
if (opts.type != null) qs.set("type", String(opts.type));
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
if (opts.q != null) qs.set("q", String(opts.q));
if (opts.limit != null) qs.set("limit", String(opts.limit));
if (opts.page != null) qs.set("page", String(opts.page));
if (opts.offset != null) qs.set("offset", String(opts.offset));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory")
.description("Create a memory entry")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-id-")
.description("Get a single memory entry")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-memory-id-")
.description("Update a memory entry")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-memory-id-")
.description("Delete a memory entry")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-health")
.description("Memory store health check")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-retrieve-preview")
.description("Dry-run memory retrieval (Playground)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/retrieve-preview";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-embedding-providers")
.description("List embedding providers")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/embedding-providers";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-engine-status")
.description("Memory engine status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/engine-status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-summarize")
.description("Compact old memories")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/summarize";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-reindex")
.description("Trigger vector reindex")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/reindex";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-memory")
.description("Get memory settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/memory";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-memory")
.description("Update memory settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/memory";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant")
.description("Get Qdrant settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-qdrant")
.description("Update Qdrant settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant-health")
.description("Qdrant health probe")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-qdrant-search")
.description("Qdrant semantic search test")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/search";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-qdrant-cleanup")
.description("Clean up expired Qdrant points")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/cleanup";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant-embedding-models")
.description("List Qdrant embedding models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/embedding-models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_messages(parent) {
const tag = parent.command("messages").description("Messages endpoints");
tag.command("post-api-v1-messages")
tag
.command("post-api-v1-messages")
.description("Create message (Anthropic-compatible)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,11 +18,17 @@ export function register_messages(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-messages-count-tokens")
tag
.command("post-api-v1-messages-count-tokens")
.description("Count tokens for a message")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -33,7 +40,12 @@ export function register_messages(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,40 +1,59 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_models(parent) {
const tag = parent.command("models").description("Models endpoints");
tag.command("get-api-v1-models")
tag
.command("get-api-v1-models")
.description("List available models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-v1-providers-provider-models")
tag
.command("get-api-v1-providers-provider-models")
.description("List models for a specific provider")
.requiredOption("--provider <provider>", "Provider id or alias (for example `openai`, `claude`, `cc`).")
.requiredOption(
"--provider <provider>",
"Provider id or alias (for example `openai`, `claude`, `cc`)."
)
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/providers/{provider}/models";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-models")
tag
.command("get-api-models")
.description("List models (management)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-models-alias")
tag
.command("post-api-models-alias")
.description("Create or update a model alias")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -46,29 +65,45 @@ export function register_models(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-models-catalog")
tag
.command("get-api-models-catalog")
.description("Get full model catalog")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/models/catalog";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-v1beta-models")
tag
.command("get-api-v1beta-models")
.description("List models (Gemini format)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1beta/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1beta-models-path-")
tag
.command("post-api-v1beta-models-path-")
.description("Gemini generateContent")
.requiredOption("--path <path>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
@@ -82,7 +117,12 @@ export function register_models(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_moderations(parent) {
const tag = parent.command("moderations").description("Moderations endpoints");
tag.command("post-api-v1-moderations")
tag
.command("post-api-v1-moderations")
.description("Create moderation")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,7 +18,12 @@ export function register_moderations(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_oauth(parent) {
const tag = parent.command("oauth").description("OAuth endpoints");
tag.command("get-api-oauth-provider-action-")
tag
.command("get-api-oauth-provider-action-")
.description("OAuth flow handler")
.requiredOption("--provider <provider>", "")
.requiredOption("--action <action>", "")
@@ -14,29 +15,44 @@ export function register_oauth(parent) {
let url = "/api/oauth/{provider}/{action}";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
url = url.replace("{action}", encodeURIComponent(opts.action ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-cursor-auto-import")
tag
.command("get-api-oauth-cursor-auto-import")
.description("Auto-import Cursor OAuth credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/cursor/auto-import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-cursor-import")
tag
.command("get-api-oauth-cursor-import")
.description("Get Cursor import status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/cursor/import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-cursor-import")
tag
.command("post-api-oauth-cursor-import")
.description("Import Cursor OAuth credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -48,29 +64,45 @@ export function register_oauth(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-auto-import")
tag
.command("get-api-oauth-kiro-auto-import")
.description("Auto-import Kiro OAuth credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/auto-import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-import")
tag
.command("get-api-oauth-kiro-import")
.description("Get Kiro import status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-kiro-import")
tag
.command("post-api-oauth-kiro-import")
.description("Import Kiro OAuth credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -82,20 +114,31 @@ export function register_oauth(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-social-authorize")
tag
.command("get-api-oauth-kiro-social-authorize")
.description("Initiate Kiro social OAuth authorization")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/social-authorize";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-kiro-social-exchange")
tag
.command("post-api-oauth-kiro-social-exchange")
.description("Exchange Kiro social OAuth token")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -107,7 +150,12 @@ export function register_oauth(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,83 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_playground(parent) {
const tag = parent.command("playground").description("Playground endpoints");
tag.command("post-api-playground-improve-prompt")
.description("Improve prompt via LLM")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/improve-prompt";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-playground-presets")
.description("List playground presets")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-playground-presets")
.description("Create playground preset")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-playground-presets-id-")
.description("Get playground preset")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-playground-presets-id-")
.description("Update playground preset")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-playground-presets-id-")
.description("Delete playground preset")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,43 +1,63 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_pricing(parent) {
const tag = parent.command("pricing").description("Pricing endpoints");
tag.command("get-api-pricing")
tag
.command("get-api-pricing")
.description("Get model pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-pricing")
tag
.command("post-api-pricing")
.description("Set model pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-pricing-defaults")
tag
.command("get-api-pricing-defaults")
.description("Get default pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing/defaults";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-pricing-models")
tag
.command("get-api-pricing-models")
.description("Get pricing per model")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,61 +1,91 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_provider_nodes(parent) {
const tag = parent.command("provider-nodes").description("Provider Nodes endpoints");
tag.command("get-api-provider-nodes")
tag
.command("get-api-provider-nodes")
.description("List provider nodes")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-provider-nodes")
tag
.command("post-api-provider-nodes")
.description("Create provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-provider-nodes-id-")
tag
.command("patch-api-provider-nodes-id-")
.description("Update provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/{id}";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-provider-nodes-id-")
tag
.command("delete-api-provider-nodes-id-")
.description("Delete provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-provider-nodes-validate")
tag
.command("post-api-provider-nodes-validate")
.description("Validate a provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/validate";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-provider-models")
tag
.command("get-api-provider-models")
.description("List provider models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_providers(parent) {
const tag = parent.command("providers").description("Providers endpoints");
tag.command("get-api-providers")
tag
.command("get-api-providers")
.description("List provider connections")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers")
tag
.command("post-api-providers")
.description("Create provider connection")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,20 +32,31 @@ export function register_providers(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-id-")
tag
.command("get-api-providers-id-")
.description("Get provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-providers-id-")
tag
.command("patch-api-providers-id-")
.description("Update provider connection")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -51,97 +68,96 @@ export function register_providers(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-providers-id-")
tag
.command("delete-api-providers-id-")
.description("Delete provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-id-test")
tag
.command("post-api-providers-id-test")
.description("Test provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-id-models")
tag
.command("get-api-providers-id-models")
.description("List models for a provider")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-test-batch")
tag
.command("post-api-providers-test-batch")
.description("Test multiple providers at once")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/test-batch";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-validate")
tag
.command("post-api-providers-validate")
.description("Validate provider credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/validate";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-client")
tag
.command("get-api-providers-client")
.description("Get client-side provider info")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/client";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-import")
.description("Import an Antigravity CLI (agy) token file as an `agy` connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/import";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-import-bulk")
.description("Bulk-import multiple Antigravity CLI (agy) token files (up to 50)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/import-bulk";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-zip-extract")
.description("Extract `.json` token files from an uploaded ZIP for agy bulk import")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/zip-extract";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-apply-local")
.description("Auto-detect and import the local Antigravity CLI (agy) login from disk")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/apply-local";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,154 +0,0 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_quota(parent) {
const tag = parent.command("quota").description("Quota endpoints");
tag.command("get-api-quota-pools")
.description("List quota pools")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-quota-pools")
.description("Create quota pool")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-pools-id-")
.description("Get quota pool by ID")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-quota-pools-id-")
.description("Update quota pool (name or allocations)")
.requiredOption("--id <id>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-quota-pools-id-")
.description("Delete quota pool")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-pools-id-usage")
.description("Get pool usage snapshot (per-key consumption + burn rate)")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}/usage";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-plans")
.description("List resolved provider plans (catalog + manual overrides)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-plans-connection-id-")
.description("Get resolved plan for a connection")
.requiredOption("--connection-id <connectionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-quota-plans-connection-id-")
.description("Upsert manual plan override for a connection")
.requiredOption("--connection-id <connectionId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-quota-plans-connection-id-")
.description("Delete manual plan override (reverts to catalog/auto)")
.requiredOption("--connection-id <connectionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-preview")
.description("Dry-run quota enforcement check (preview only, no consumption recorded)")
.requiredOption("--api-key-id <apiKeyId>", "")
.requiredOption("--pool-id <poolId>", "")
.option("--estimated-tokens <estimatedTokens>", "")
.option("--estimated-usd <estimatedUsd>", "")
.option("--estimated-requests <estimatedRequests>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/preview";
const qs = new URLSearchParams();
if (opts.apiKeyId != null) qs.set("apiKeyId", String(opts.apiKeyId));
if (opts.poolId != null) qs.set("poolId", String(opts.poolId));
if (opts.estimatedTokens != null) qs.set("estimatedTokens", String(opts.estimatedTokens));
if (opts.estimatedUsd != null) qs.set("estimatedUsd", String(opts.estimatedUsd));
if (opts.estimatedRequests != null) qs.set("estimatedRequests", String(opts.estimatedRequests));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}

View File

@@ -1,6 +1,4 @@
// AUTO-GENERATED. Do not edit.
import { register_playground } from "./playground.mjs";
import { register_memory } from "./memory.mjs";
import { register_chat } from "./chat.mjs";
import { register_messages } from "./messages.mjs";
import { register_responses } from "./responses.mjs";
@@ -21,17 +19,37 @@ import { register_usage } from "./usage.mjs";
import { register_pricing } from "./pricing.mjs";
import { register_translator } from "./translator.mjs";
import { register_cli_tools } from "./cli-tools.mjs";
import { register_embedded_services } from "./embedded-services.mjs";
import { register_oauth } from "./oauth.mjs";
import { register_cloud } from "./cloud.mjs";
import { register_fallback } from "./fallback.mjs";
import { register_telemetry } from "./telemetry.mjs";
import { register_quota } from "./quota.mjs";
import { register_agentbridge } from "./agentbridge.mjs";
import { register_traffic_inspector } from "./traffic-inspector.mjs";
import { register_agent_skills } from "./agent-skills.mjs";
export const API_TAGS = ["playground","memory","chat","messages","responses","embeddings","images","audio","moderations","rerank","system","models","providers","provider-nodes","api-keys","combos","settings","compression","usage","pricing","translator","cli-tools","embedded-services","oauth","cloud","fallback","telemetry","quota","agentbridge","traffic-inspector","agent-skills"];
export const API_TAGS = [
"chat",
"messages",
"responses",
"embeddings",
"images",
"audio",
"moderations",
"rerank",
"system",
"models",
"providers",
"provider-nodes",
"api-keys",
"combos",
"settings",
"compression",
"usage",
"pricing",
"translator",
"cli-tools",
"oauth",
"cloud",
"fallback",
"telemetry",
];
export function registerApiCommands(program) {
const api = program
@@ -40,9 +58,9 @@ export function registerApiCommands(program) {
api
.command("tags")
.description("List available API tag groups")
.action(() => { API_TAGS.forEach((t) => console.log(t)); });
register_playground(api);
register_memory(api);
.action(() => {
API_TAGS.forEach((t) => console.log(t));
});
register_chat(api);
register_messages(api);
register_responses(api);
@@ -63,13 +81,8 @@ export function registerApiCommands(program) {
register_pricing(api);
register_translator(api);
register_cli_tools(api);
register_embedded_services(api);
register_oauth(api);
register_cloud(api);
register_fallback(api);
register_telemetry(api);
register_quota(api);
register_agentbridge(api);
register_traffic_inspector(api);
register_agent_skills(api);
}

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_rerank(parent) {
const tag = parent.command("rerank").description("Rerank endpoints");
tag.command("post-api-v1-rerank")
tag
.command("post-api-v1-rerank")
.description("Rerank documents")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,7 +18,12 @@ export function register_rerank(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,11 +1,12 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_responses(parent) {
const tag = parent.command("responses").description("Responses endpoints");
tag.command("post-api-v1-responses")
tag
.command("post-api-v1-responses")
.description("Create response (OpenAI Responses API)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -17,7 +18,12 @@ export function register_responses(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,20 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_settings(parent) {
const tag = parent.command("settings").description("Settings endpoints");
tag.command("get-api-settings")
tag
.command("get-api-settings")
.description("Get application settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-settings")
tag
.command("patch-api-settings")
.description("Update settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -26,20 +32,31 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-payload-rules")
tag
.command("get-api-settings-payload-rules")
.description("Get payload rules configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/payload-rules";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-payload-rules")
tag
.command("put-api-settings-payload-rules")
.description("Update payload rules configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -51,65 +68,101 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-combo-defaults")
tag
.command("get-api-settings-combo-defaults")
.description("Get combo default settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/combo-defaults";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-proxy")
tag
.command("get-api-settings-proxy")
.description("Get proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-settings-proxy")
tag
.command("patch-api-settings-proxy")
.description("Update proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-proxy-test")
tag
.command("post-api-settings-proxy-test")
.description("Test proxy connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-require-login")
tag
.command("post-api-settings-require-login")
.description("Toggle login requirement")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/require-login";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-ip-filter")
tag
.command("get-api-settings-ip-filter")
.description("Get IP filter configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/ip-filter";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-ip-filter")
tag
.command("put-api-settings-ip-filter")
.description("Update IP filter configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -121,20 +174,31 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-system-prompt")
tag
.command("get-api-settings-system-prompt")
.description("Get system prompt configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/system-prompt";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-system-prompt")
tag
.command("put-api-settings-system-prompt")
.description("Update system prompt configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -146,20 +210,31 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-thinking-budget")
tag
.command("get-api-settings-thinking-budget")
.description("Get thinking budget configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/thinking-budget";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-thinking-budget")
tag
.command("put-api-settings-thinking-budget")
.description("Update thinking budget configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -171,20 +246,31 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PUT",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-rate-limit")
tag
.command("get-api-rate-limit")
.description("Get rate limit configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/rate-limit";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-rate-limit")
tag
.command("post-api-rate-limit")
.description("Update rate limit configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -196,32 +282,12 @@ export function register_settings(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-quota-store")
.description("Get current quota store driver settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/quota-store";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-quota-store")
.description("Update quota store driver settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/quota-store";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,29 +1,40 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_system(parent) {
const tag = parent.command("system").description("System endpoints");
tag.command("get-api-v1")
tag
.command("get-api-v1")
.description("API v1 root endpoint")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tags")
tag
.command("get-api-tags")
.description("List Ollama-compatible model tags")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tags";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-auth-login")
tag
.command("post-api-auth-login")
.description("Authenticate user")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -35,101 +46,157 @@ export function register_system(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-auth-logout")
tag
.command("post-api-auth-logout")
.description("Log out")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/auth/logout";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-init")
tag
.command("get-api-init")
.description("Initialize application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/init";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-restart")
tag
.command("post-api-restart")
.description("Restart the application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-shutdown")
tag
.command("post-api-shutdown")
.description("Shutdown the application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/shutdown";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-db-backups")
tag
.command("get-api-db-backups")
.description("List database backups")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/db-backups";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-db-backups")
tag
.command("post-api-db-backups")
.description("Create database backup")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/db-backups";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-storage-health")
tag
.command("get-api-storage-health")
.description("Check storage health")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/storage/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-sync-cloud")
tag
.command("post-api-sync-cloud")
.description("Sync with cloud")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sync/cloud";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-sync-initialize")
tag
.command("post-api-sync-initialize")
.description("Initialize cloud sync")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sync/initialize";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-resilience")
tag
.command("get-api-resilience")
.description("Get resilience configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/resilience";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-resilience")
tag
.command("patch-api-resilience")
.description("Update resilience configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -141,92 +208,143 @@ export function register_system(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "PATCH",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-resilience-reset")
tag
.command("post-api-resilience-reset")
.description("Reset circuit breakers")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/resilience/reset";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-monitoring-health")
tag
.command("get-api-monitoring-health")
.description("System health check")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/monitoring/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-rate-limits")
tag
.command("get-api-rate-limits")
.description("Get per-account rate limit status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/rate-limits";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-sessions")
tag
.command("get-api-sessions")
.description("Get active sessions")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sessions";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cache")
tag
.command("get-api-cache")
.description("Get cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cache")
tag
.command("delete-api-cache")
.description("Clear all caches")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cache-stats")
tag
.command("get-api-cache-stats")
.description("Get detailed cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache/stats";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cache-stats")
tag
.command("delete-api-cache-stats")
.description("Clear cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache/stats";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-evals")
tag
.command("get-api-evals")
.description("List eval suites")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/evals";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-evals")
tag
.command("post-api-evals")
.description("Run evaluation")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -238,31 +356,47 @@ export function register_system(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-evals-suite-id-")
tag
.command("get-api-evals-suite-id-")
.description("Get eval suite details")
.requiredOption("--suite-id <suiteId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/evals/{suiteId}";
url = url.replace("{suiteId}", encodeURIComponent(opts.suiteId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-policies")
tag
.command("get-api-policies")
.description("List routing policies")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/policies";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-policies")
tag
.command("post-api-policies")
.description("Create routing policy")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
@@ -274,46 +408,58 @@ export function register_system(parent) {
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "POST",
body,
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-policies")
tag
.command("delete-api-policies")
.description("Delete routing policy")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/policies";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "DELETE",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compliance-audit-log")
tag
.command("get-api-compliance-audit-log")
.description("Get compliance audit log")
.option("--level <level>", "high = Activity feed events only; all = all audit events")
.option("--action <action>", "Filter by exact action string (e.g. \"provider.added\")")
.option("--actor <actor>", "Filter by actor identifier")
.option("--limit <limit>", "")
.option("--offset <offset>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compliance/audit-log";
const qs = new URLSearchParams();
if (opts.level != null) qs.set("level", String(opts.level));
if (opts.action != null) qs.set("action", String(opts.action));
if (opts.actor != null) qs.set("actor", String(opts.actor));
if (opts.limit != null) qs.set("limit", String(opts.limit));
if (opts.offset != null) qs.set("offset", String(opts.offset));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-openapi-spec")
tag
.command("get-api-openapi-spec")
.description("Get OpenAPI specification catalog")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/openapi/spec";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

View File

@@ -1,25 +1,35 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
// AUTO-GENERATED from docs/reference/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_telemetry(parent) {
const tag = parent.command("telemetry").description("Telemetry endpoints");
tag.command("get-api-telemetry-summary")
tag
.command("get-api-telemetry-summary")
.description("Get telemetry summary")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/telemetry/summary";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-token-health")
tag
.command("get-api-token-health")
.description("Get token health status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/token-health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const res = await apiFetch(url, {
method: "GET",
baseUrl: gOpts.baseUrl,
apiKey: gOpts.apiKey,
});
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});

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