mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Compare commits
136 Commits
release/v3
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c338477cd5 | ||
|
|
45b0672628 | ||
|
|
db6b4088a7 | ||
|
|
b444f518e1 | ||
|
|
971b54c452 | ||
|
|
0a68f51390 | ||
|
|
d4f253bbcd | ||
|
|
7219f2b38d | ||
|
|
6956edc418 | ||
|
|
c0a57d2c05 | ||
|
|
1aac066fb9 | ||
|
|
7f23fb50f2 | ||
|
|
abbbf90ebe | ||
|
|
7efd40729b | ||
|
|
f842ebaf24 | ||
|
|
e4f1324600 | ||
|
|
821a1f0b11 | ||
|
|
969addef6b | ||
|
|
27c5772cb8 | ||
|
|
f3cc266662 | ||
|
|
32b0f9303a | ||
|
|
f7d895e8ff | ||
|
|
3c8f347c88 | ||
|
|
0e07eb0518 | ||
|
|
06e85a90c0 | ||
|
|
d1088f0dc6 | ||
|
|
d8b6ae3688 | ||
|
|
da58330845 | ||
|
|
c5924a7639 | ||
|
|
37116fd4d6 | ||
|
|
01b1dab330 | ||
|
|
15ab183810 | ||
|
|
de12d1c109 | ||
|
|
33667fcf3a | ||
|
|
62a0f6a542 | ||
|
|
ae2a0e3e40 | ||
|
|
de60b4b171 | ||
|
|
b6c65efd28 | ||
|
|
9350a5d6c6 | ||
|
|
99397b4f41 | ||
|
|
a32e52eed6 | ||
|
|
88857237a2 | ||
|
|
c315a2394c | ||
|
|
4bfd9e2845 | ||
|
|
d6f008cdaf | ||
|
|
bf8b56b29f | ||
|
|
630680067c | ||
|
|
6a7a36c09e | ||
|
|
d65d8bb54f | ||
|
|
68e4d0c599 | ||
|
|
8169b97d84 | ||
|
|
50ce13bce6 | ||
|
|
e1a9c61179 | ||
|
|
2441a4f441 | ||
|
|
6ebc493770 | ||
|
|
259486afb5 | ||
|
|
500197846d | ||
|
|
ee0fdcb6c8 | ||
|
|
5c3545b045 | ||
|
|
1cc2313a4f | ||
|
|
49c11f0cea | ||
|
|
4f38167964 | ||
|
|
ff65652cdf | ||
|
|
cc850122e3 | ||
|
|
42887b65b2 | ||
|
|
1a98dfe8ed | ||
|
|
003e6a80b7 | ||
|
|
617a648088 | ||
|
|
1322411343 | ||
|
|
da273d37e2 | ||
|
|
dbd70ddd1f | ||
|
|
89a76d8c1c | ||
|
|
a00366602b | ||
|
|
96e5ec9269 | ||
|
|
858b6742e8 | ||
|
|
3c98e9f1ef | ||
|
|
2427df2f2c | ||
|
|
07a81c8a40 | ||
|
|
ea0c0d8499 | ||
|
|
23f31faf38 | ||
|
|
1e4185edac | ||
|
|
b3372e46c4 | ||
|
|
fc437ddecd | ||
|
|
70c6610fa8 | ||
|
|
d3ff0b3bde | ||
|
|
f01a0b0c6d | ||
|
|
de2420a35c | ||
|
|
eb8651780d | ||
|
|
c0dcdcc12f | ||
|
|
ea9d22beda | ||
|
|
60fc41f638 | ||
|
|
ac4fd7e078 | ||
|
|
85351bc63d | ||
|
|
ed3c188881 | ||
|
|
11bd96ec5c | ||
|
|
f112bc966f | ||
|
|
b60839b90c | ||
|
|
717f56bf93 | ||
|
|
3ea416350e | ||
|
|
1012603a1b | ||
|
|
b480e6c916 | ||
|
|
6ec4ca3f67 | ||
|
|
b145e41a42 | ||
|
|
e328e257d1 | ||
|
|
67d79f6c44 | ||
|
|
e0615a8194 | ||
|
|
f688d1150f | ||
|
|
fd6a2a7f95 | ||
|
|
ecdd5a36eb | ||
|
|
71f6e8d312 | ||
|
|
c9663d4f84 | ||
|
|
4c420b015d | ||
|
|
452e6cc937 | ||
|
|
48ed42c6c3 | ||
|
|
6df38155a4 | ||
|
|
5b72dc6250 | ||
|
|
4adc1d087f | ||
|
|
ee061d7a6d | ||
|
|
ed275bb54b | ||
|
|
8505e0f2b7 | ||
|
|
765964242c | ||
|
|
fc37c93a20 | ||
|
|
a471d70c3c | ||
|
|
b4437dcee4 | ||
|
|
a8522cc13a | ||
|
|
929caeb910 | ||
|
|
000d60b907 | ||
|
|
591084052a | ||
|
|
35a30609dd | ||
|
|
7db430a352 | ||
|
|
630baa6c18 | ||
|
|
df2379053e | ||
|
|
9535fa52a6 | ||
|
|
cd89ce3cfa | ||
|
|
a25d5f1ef6 | ||
|
|
78454eed5e |
@@ -39,10 +39,15 @@ playwright-report
|
||||
blob-report
|
||||
|
||||
# Documentation
|
||||
# Translations (~51 MB) are excluded — the Docs viewer reads English sources.
|
||||
# Screenshots (~1.7 MB) and SVGs (~250 KB) are needed at build time for MDX
|
||||
# image resolution (fumadocs-mdx bundles them).
|
||||
# Raster sources under docs/diagrams/ only (exported SVGs are required).
|
||||
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
|
||||
# runtime. The previous `docs/*` block hid every file except openapi.yaml,
|
||||
# so the in-product help screen failed with ENOENT for every page.
|
||||
# We now keep the English markdown tree plus the docs assets imported by MDX
|
||||
# during `next build`, while still dropping the bulky translated docs and
|
||||
# extra raster diagram sources that account for most of the docs footprint
|
||||
# of the ~50 MB docs directory. The Docs viewer reads the default-locale
|
||||
# (English) sources at runtime, so translations are not required in the
|
||||
# container image.
|
||||
docs/i18n/**
|
||||
docs/diagrams/**/*.png
|
||||
docs/diagrams/**/*.jpg
|
||||
|
||||
98
.env.example
98
.env.example
@@ -292,6 +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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -382,6 +388,13 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# ALL_PROXY=socks5://127.0.0.1:7890
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# 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.
|
||||
@@ -421,6 +434,12 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_QODER_BIN=qoder
|
||||
# CLI_QWEN_BIN=qwen
|
||||
|
||||
# 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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -476,6 +495,11 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
|
||||
# to opt out (restores fully concurrent fetches). Default: 1500
|
||||
PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
|
||||
# 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.
|
||||
@@ -490,6 +514,11 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# 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
|
||||
@@ -846,6 +875,15 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
# OMNIROUTE_GROK_TLS_TIMEOUT_MS=60000
|
||||
# OMNIROUTE_GROK_TLS_GRACE_MS=10000
|
||||
|
||||
# ── Browser-backed web-cookie chat (Playwright shared pool) ──
|
||||
# Used by: open-sse/services/browserPool.ts + browserBackedChat.ts. The shared
|
||||
# browser pool warms a headless context for web-cookie providers (e.g. claude-web)
|
||||
# that need a real browser to satisfy anti-bot challenges. Set OMNIROUTE_BROWSER_POOL=off
|
||||
# to fully disable the pool; set WEB_COOKIE_USE_BROWSER=1 to opt a web-cookie chat
|
||||
# request into the browser-backed path.
|
||||
# OMNIROUTE_BROWSER_POOL=on
|
||||
# WEB_COOKIE_USE_BROWSER=0
|
||||
|
||||
# ── Circuit breaker thresholds and reset windows ──
|
||||
# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts.
|
||||
# Defaults match historical PROVIDER_PROFILES values (post-scaling for
|
||||
@@ -1045,6 +1083,16 @@ APP_LOG_TO_FILE=true
|
||||
# Comma-separated data sources. Default: litellm
|
||||
# PRICING_SYNC_SOURCES=litellm
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 18b. ARENA ELO SYNC
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Enable auto-updating model intelligence from Arena AI leaderboard ELO scores.
|
||||
# Used by: src/lib/arenaEloSync.ts
|
||||
# ARENA_ELO_SYNC_ENABLED=false
|
||||
|
||||
# Sync interval in seconds. Default: 86400 (24 hours).
|
||||
# ARENA_ELO_SYNC_INTERVAL=86400
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 19. MODEL SYNC (Dev)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1063,6 +1111,14 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 86400000 (24 hours)
|
||||
# OPENROUTER_CATALOG_TTL_MS=86400000
|
||||
|
||||
# ── Model catalog response shape ──
|
||||
# Include display-friendly name fields in /v1/models responses.
|
||||
# Disable for clients that expect model IDs only.
|
||||
# Defined in: src/shared/constants/featureFlagDefinitions.ts
|
||||
# Used by: src/app/api/v1/models/catalog.ts
|
||||
# Default: true
|
||||
# MODEL_CATALOG_INCLUDE_NAMES=true
|
||||
|
||||
# ── NanoBanana (Image Generation) ──
|
||||
# Polling config for async image generation jobs.
|
||||
# Used by: open-sse/handlers/imageGeneration.ts
|
||||
@@ -1154,6 +1210,25 @@ 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
|
||||
|
||||
# Stagger interval (ms) between provider token healthchecks at startup.
|
||||
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
|
||||
# HEALTHCHECK_STAGGER_MS=3000
|
||||
@@ -1170,9 +1245,9 @@ APP_LOG_TO_FILE=true
|
||||
|
||||
# Enable verbose trace logging for OmniRoute internals.
|
||||
# Used by: open-sse/handlers/chatCore.ts.
|
||||
# OMNIRROUTE_TRACE=true
|
||||
# OMNIROUTE_TRACE=true
|
||||
|
||||
# Standard DEBUG flag (same effect as OMNIRROUTE_TRACE).
|
||||
# Standard DEBUG flag (same effect as OMNIROUTE_TRACE).
|
||||
# DEBUG=true
|
||||
# CURSOR_STREAM_DEBUG=1
|
||||
|
||||
@@ -1266,6 +1341,13 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: src/app/api/providers/command-code/auth/shared.ts.
|
||||
# COMMAND_CODE_CALLBACK_PORT=
|
||||
|
||||
# ── Command Code CLI version header ──
|
||||
# Value sent as the x-command-code-version header to the Command Code upstream.
|
||||
# Overrides the built-in default; bump if the upstream requires a newer CLI version.
|
||||
# Used by: open-sse/executors/commandCode.ts
|
||||
# Default: 0.33.2
|
||||
# COMMAND_CODE_VERSION=0.33.2
|
||||
|
||||
# ── MITM debug proxy (development only) ──
|
||||
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
|
||||
# MITM_LOCAL_PORT=443
|
||||
@@ -1459,3 +1541,15 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | 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
|
||||
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
|
||||
|
||||
# ─── OpenCode config regeneration (scripts/ad-hoc/regen-opencode-config.ts) ───
|
||||
# Base URL of the OmniRoute instance to query for /v1/models when regenerating
|
||||
# an opencode.json with accurate limit.context values. Used by:
|
||||
# scripts/ad-hoc/regen-opencode-config.ts. Default: http://localhost:20128
|
||||
# OMNIROUTE_URL=
|
||||
# API key to authenticate against the OmniRoute /v1/models endpoint. Falls back
|
||||
# to OPENCODE_API_KEY when unset. Used by: scripts/ad-hoc/regen-opencode-config.ts.
|
||||
# OMNIROUTE_KEY=
|
||||
# OpenCode-style API key (sk-...) for the regenerated opencode.json. Used by:
|
||||
# scripts/ad-hoc/regen-opencode-config.ts. Falls back to OMNIROUTE_KEY.
|
||||
# OPENCODE_API_KEY=
|
||||
|
||||
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# 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"]
|
||||
206
.github/workflows/ci.yml
vendored
206
.github/workflows/ci.yml
vendored
@@ -24,6 +24,12 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# tsx gates below (known-symbols, route-guard-membership) import modules that
|
||||
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
||||
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@v6
|
||||
- uses: actions/setup-node@v6
|
||||
@@ -37,6 +43,20 @@ jobs:
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:route-validation:t06
|
||||
- run: npm run check:any-budget:t11
|
||||
- run: npm run check:provider-consistency
|
||||
- run: npm run check:fetch-targets
|
||||
- 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:tracked-artifacts
|
||||
- run: npm run check:lockfile
|
||||
- run: npm run check:licenses
|
||||
- run: npm run check:docs-sync
|
||||
- run: npm run typecheck:core
|
||||
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
|
||||
@@ -45,6 +65,83 @@ jobs:
|
||||
- run: npm run typecheck:noimplicit:core
|
||||
continue-on-error: true
|
||||
|
||||
quality-gate:
|
||||
name: Quality Ratchet
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
if: ${{ always() && needs.test-coverage.result == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
- run: npm run quality:collect
|
||||
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
|
||||
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
|
||||
# coverage mergeado). Tamanho de arquivo e duplicação têm gates dedicados.
|
||||
- name: Ratchet check
|
||||
run: node scripts/quality/check-quality-ratchet.mjs --summary .artifacts/quality-ratchet.md
|
||||
# Catraca de duplicação (jscpd@4 sobre src+open-sse). Roda neste job (paralelo)
|
||||
# para não pesar no caminho crítico do lint.
|
||||
- name: Duplication ratchet
|
||||
run: npm run check:duplication
|
||||
- name: Complexity ratchet
|
||||
run: npm run check:complexity
|
||||
- name: Append summary
|
||||
if: always()
|
||||
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Upload ratchet report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: quality-ratchet
|
||||
path: .artifacts/quality-ratchet.md
|
||||
if-no-files-found: warn
|
||||
|
||||
# Phase 7 extended quality gates — ADVISORY (continue-on-error). The 5 npm-based
|
||||
# ratchets (dead-code/cognitive-complexity/type-coverage/circular-deps/bundle-size)
|
||||
# run for real. The external scans (vuln/secrets/workflows) skip gracefully until the
|
||||
# owner adds install steps for osv-scanner/gitleaks/actionlint/zizmor; CodeQL ratchet
|
||||
# works via the runner's gh token. SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
|
||||
quality-extended:
|
||||
name: Quality Gates (Extended, advisory)
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Dead-code (knip)
|
||||
run: npm run check:dead-code
|
||||
- name: Cognitive complexity (sonarjs)
|
||||
run: npm run check:cognitive-complexity
|
||||
- name: Type coverage
|
||||
run: npm run check:type-coverage
|
||||
- name: Circular deps (dpdm)
|
||||
run: npm run check:circular-deps
|
||||
- name: Bundle size
|
||||
run: npm run check:bundle-size
|
||||
- name: CodeQL alerts ratchet
|
||||
run: npm run check:codeql-ratchet
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Secret scan (gitleaks; skips if absent)
|
||||
run: npm run check:secrets
|
||||
- name: Vulnerability ratchet (osv-scanner; skips if absent)
|
||||
run: npm run check:vuln-ratchet
|
||||
- name: Workflow lint (actionlint+zizmor; skips if absent)
|
||||
run: npm run check:workflows
|
||||
|
||||
docs-sync-strict:
|
||||
name: Docs Sync (Strict)
|
||||
runs-on: ubuntu-latest
|
||||
@@ -56,6 +153,19 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:docs-all
|
||||
# Previously-orphaned contract gates (existed as files, never wired anywhere).
|
||||
# All exit 0 today: cli-i18n is a hard gate, openapi-coverage is a ratchet
|
||||
# (floor ~36), openapi-security-tiers is advisory (Hard Rules #15/#17).
|
||||
- name: CLI i18n consistency
|
||||
run: npm run check:cli-i18n
|
||||
- name: OpenAPI route coverage (ratchet)
|
||||
run: npm run check:openapi-coverage
|
||||
- name: OpenAPI security-tier consistency (advisory)
|
||||
run: npm run check:openapi-security-tiers
|
||||
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
|
||||
run: npm run check:openapi-routes
|
||||
- name: Doc /api refs resolve to real routes (anti-hallucination)
|
||||
run: npm run check:docs-symbols
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
|
||||
@@ -125,6 +235,14 @@ jobs:
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
|
||||
- name: Validate source changes include tests
|
||||
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
|
||||
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
|
||||
- name: Detect test-masking (weakened assertions)
|
||||
run: npm run check:test-masking
|
||||
# Evidence-in-PR-body (Hard Rule #18 mechanized): claims of "tests pass" must carry output.
|
||||
- name: Require evidence in PR body
|
||||
run: npm run check:pr-evidence
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
- name: Publish PR test policy summary
|
||||
if: always()
|
||||
run: |
|
||||
@@ -144,6 +262,21 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- name: Archive Next.js build for E2E shards
|
||||
# Use tar so the archive preserves paths relative to CWD (.build/next/...).
|
||||
# upload-artifact path-stripping is ambiguous when exclude patterns are used;
|
||||
# an explicit tar avoids the double-nesting issue (.build/next/next/...).
|
||||
run: |
|
||||
tar -czf /tmp/e2e-build.tar.gz \
|
||||
--exclude='.build/next/standalone/node_modules' \
|
||||
--exclude='.build/next/cache' \
|
||||
.build/next
|
||||
- name: Upload Next.js build for E2E shards
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e-next-build
|
||||
path: /tmp/e2e-build.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
package-artifact:
|
||||
name: Package Artifact
|
||||
@@ -215,7 +348,32 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --max-old-space-size=4096 --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts
|
||||
- run: node --max-old-space-size=4096 --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
|
||||
test-vitest:
|
||||
name: Vitest (MCP / autoCombo / UI components)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build
|
||||
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"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
|
||||
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
|
||||
- run: npm run test:vitest
|
||||
# vitest:ui is RED today (14 fails — UI component drift accumulated while the
|
||||
# suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+)
|
||||
# fixes the components/tests; then drop continue-on-error to make it blocking.
|
||||
- run: npm run test:vitest:ui
|
||||
continue-on-error: true
|
||||
|
||||
node-24-compat:
|
||||
name: Node 24 Compatibility (${{ matrix.shard }}/2)
|
||||
@@ -239,7 +397,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
|
||||
node-26-compat:
|
||||
name: Node 26 Compatibility (${{ matrix.shard }}/2)
|
||||
@@ -263,7 +421,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts
|
||||
- run: node --import tsx --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
|
||||
test-coverage-shard:
|
||||
name: Coverage Shard (${{ matrix.shard }}/8)
|
||||
@@ -302,7 +460,7 @@ jobs:
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
node --max-old-space-size=4096 --import tsx --test --test-force-exit --test-concurrency=4 \
|
||||
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts
|
||||
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
- name: Upload raw shard coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -345,10 +503,12 @@ jobs:
|
||||
find . -maxdepth 3 -type f | sort
|
||||
exit 1
|
||||
fi
|
||||
# Gate aligned to the project's local coverage bar (npm run test:coverage
|
||||
# uses 40/40/40/40). The previous 75/70 gate never ran on main (the
|
||||
# coverage shards always failed → this job was skipped), so it was never
|
||||
# actually enforced and is inconsistent with the repo's real standard.
|
||||
# Gate aligned to the project's local coverage bar: `npm run test:coverage`
|
||||
# gates at 60/60/60/60, so CI must match it (the previous CI floor of 40
|
||||
# silently undershot the local bar — a real drift). Real merged coverage is
|
||||
# ~79/79/82/75, so 60 is a conservative floor with headroom; the Fase-4
|
||||
# coverage ratchet (quality-baseline.json) layers "must not drop vs baseline"
|
||||
# on top of this floor.
|
||||
npx c8 report \
|
||||
--temp-directory coverage-shards \
|
||||
--reports-dir coverage \
|
||||
@@ -359,7 +519,7 @@ jobs:
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
--check-coverage \
|
||||
--statements 40 --lines 40 --functions 40 --branches 40
|
||||
--statements 60 --lines 60 --functions 60 --branches 60
|
||||
- name: Build coverage summary
|
||||
if: always()
|
||||
run: |
|
||||
@@ -498,14 +658,18 @@ jobs:
|
||||
}
|
||||
|
||||
test-e2e:
|
||||
name: E2E Tests (${{ matrix.shard }}/6)
|
||||
name: E2E Tests (${{ matrix.shard }}/9)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
# Build artifact from the `build` job is downloaded instead of rebuilding
|
||||
# (~5min saved per shard). 9 shards (up from 6) reduces tests per shard by
|
||||
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
|
||||
# Heavy shard target: ≤20min (was ~40min). Timeout 45min to cover slow runners.
|
||||
timeout-minutes: 45
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
@@ -519,9 +683,23 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:node-runtime
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v5
|
||||
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
|
||||
- run: npm run build
|
||||
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/6
|
||||
- name: Download Next.js build artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: e2e-next-build
|
||||
path: /tmp/
|
||||
- name: Extract Next.js build and restore standalone node_modules
|
||||
run: |
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
cp -r node_modules .build/next/standalone/node_modules
|
||||
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests (${{ matrix.shard }}/2)
|
||||
|
||||
26
.github/workflows/deploy-vps.yml
vendored
26
.github/workflows/deploy-vps.yml
vendored
@@ -17,7 +17,33 @@ jobs:
|
||||
name: Deploy OmniRoute to VPS
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check VPS SSH reachability from runner
|
||||
id: reach
|
||||
env:
|
||||
# Pass the host via env (never interpolate a secret straight into the
|
||||
# script body) so /dev/tcp gets a shell variable, not inlined text.
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
# A GitHub-hosted runner can only deploy when it can actually open a TCP
|
||||
# connection to the VPS SSH port. The Local VPS lives on a private LAN and
|
||||
# the Akamai host firewalls :22 to known IPs, so the runner is routinely
|
||||
# unable to reach it (`dial tcp ***:22: i/o timeout`). Treat "unreachable
|
||||
# from the runner" as a SKIP — the real deploys are run manually from an
|
||||
# allowed network via the deploy-vps-local / deploy-vps-akamai skills — so
|
||||
# an unreachable host no longer red-fails every release/push pipeline.
|
||||
# When the host IS reachable, the deploy step below still runs in full and
|
||||
# its health gate surfaces any genuine deploy failure.
|
||||
if timeout 15 bash -c 'exec 3<>"/dev/tcp/${VPS_HOST}/22"' 2>/dev/null; then
|
||||
echo "reachable=true" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ VPS_HOST:22 reachable from the runner — proceeding with deploy."
|
||||
else
|
||||
echo "reachable=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning title=Auto-deploy skipped::VPS_HOST:22 is not reachable from this GitHub runner (private LAN / firewalled). Deploy manually with the deploy-vps-local or deploy-vps-akamai skill."
|
||||
fi
|
||||
|
||||
- name: Deploy via SSH
|
||||
if: steps.reach.outputs.reachable == 'true'
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
|
||||
121
.github/workflows/docker-publish.yml
vendored
121
.github/workflows/docker-publish.yml
vendored
@@ -176,20 +176,46 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Export digest
|
||||
- name: Build and push WEB platform image by digest
|
||||
id: build-web
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: runner-web
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
${{ env.GHCR_IMAGE_NAME }}
|
||||
cache-from: type=gha,scope=docker-web-${{ matrix.arch }}
|
||||
cache-to: type=gha,scope=docker-web-${{ matrix.arch }},mode=max
|
||||
no-cache: false
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Export digests
|
||||
env:
|
||||
DIGEST_BASE: ${{ steps.build.outputs.digest }}
|
||||
DIGEST_WEB: ${{ steps.build-web.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/digests
|
||||
digest="${DIGEST#sha256:}"
|
||||
touch "/tmp/digests/${digest}"
|
||||
mkdir -p /tmp/digests/base /tmp/digests/web
|
||||
touch "/tmp/digests/base/${DIGEST_BASE#sha256:}"
|
||||
touch "/tmp/digests/web/${DIGEST_WEB#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
- name: Upload base digests
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
name: digests-base-${{ matrix.arch }}
|
||||
path: /tmp/digests/base/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload web digests
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-web-${{ matrix.arch }}
|
||||
path: /tmp/digests/web/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
@@ -228,54 +254,67 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download digests
|
||||
- name: Download base digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
pattern: digests-base-*
|
||||
path: /tmp/digests/base
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download web digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: digests-web-*
|
||||
path: /tmp/digests/web
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create Docker Hub manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tags=(-t "${IMAGE_NAME}:${VERSION}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${IMAGE_NAME}:latest")
|
||||
fi
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
fi
|
||||
local refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
done < <(find "$dir" -type f | sort)
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
}
|
||||
|
||||
refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${IMAGE_NAME}@sha256:$(basename "$digest_file")")
|
||||
done < <(find /tmp/digests -type f | sort)
|
||||
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
echo "No image digests were downloaded." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
|
||||
- name: Create GHCR manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
tags=(-t "${GHCR_IMAGE_NAME}:${VERSION}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${GHCR_IMAGE_NAME}:latest")
|
||||
fi
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
fi
|
||||
local refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
done < <(find "$dir" -type f | sort)
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
}
|
||||
|
||||
refs=()
|
||||
while IFS= read -r digest_file; do
|
||||
refs+=("${GHCR_IMAGE_NAME}@sha256:$(basename "$digest_file")")
|
||||
done < <(find /tmp/digests -type f | sort)
|
||||
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
echo "No image digests were downloaded." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
|
||||
- name: Inspect image
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
|
||||
49
.github/workflows/quality.yml
vendored
Normal file
49
.github/workflows/quality.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
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@v6
|
||||
- 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
|
||||
- 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:any-budget:t11
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -63,6 +63,9 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
.nvidia-api-key
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -195,4 +198,17 @@ pr_reviews*.json
|
||||
|
||||
#hidden local data directories (never commit)
|
||||
.local-data/
|
||||
.data-dev/
|
||||
.data-dev/
|
||||
/.junie/
|
||||
|
||||
# internal setup prompts with personal credentials — never commit
|
||||
CODEX-SETUP-PROMPT.md
|
||||
# Quality ratchet — métricas efêmeras (baseline é commitado, métricas não)
|
||||
quality-metrics.json
|
||||
-home-diegosouzapw-dev-automações-bots-yt-downloader-20260504 .txt
|
||||
-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
|
||||
|
||||
68
.gitleaks.toml
Normal file
68
.gitleaks.toml
Normal file
@@ -0,0 +1,68 @@
|
||||
# .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)
|
||||
|
||||
# Usar as regras padrão do gitleaks (não declaramos [rules] aqui para herdar tudo)
|
||||
# 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/''',
|
||||
# ]
|
||||
#
|
||||
@@ -1,31 +1,12 @@
|
||||
# #!/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
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
#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
|
||||
# .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)
|
||||
|
||||
#npm run test:unit
|
||||
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
|
||||
|
||||
85
.license-allowlist.json
Normal file
85
.license-allowlist.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"_comment": "SPDX license allowlist for OmniRoute (MIT project). Task 7.20 / PLANO-QUALITY-GATES-FASE7.md.",
|
||||
"_policy": "Production deps with a license outside 'allowed' and not in 'exceptions' fail the gate. devDeps get an advisory warning only.",
|
||||
|
||||
"allowed": [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"0BSD",
|
||||
"CC0-1.0",
|
||||
"Unlicense",
|
||||
"Python-2.0",
|
||||
"BlueOak-1.0.0",
|
||||
"Artistic-2.0",
|
||||
"Zlib",
|
||||
"X11",
|
||||
"WTFPL",
|
||||
"PSF-2.0",
|
||||
"CC-BY-3.0"
|
||||
],
|
||||
|
||||
"allowedExpressions": [
|
||||
"MIT*",
|
||||
"MIT AND ISC",
|
||||
"Apache-2.0 AND MIT",
|
||||
"(MIT OR Apache-2.0)",
|
||||
"(MIT OR CC0-1.0)",
|
||||
"(MIT OR WTFPL)",
|
||||
"(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"(MPL-2.0 OR Apache-2.0)"
|
||||
],
|
||||
|
||||
"exceptions": {
|
||||
"@img/sharp-libvips-linux-x64": {
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"justification": "Prebuilt native binary (libvips shared library) pulled in transitively by the 'sharp' package (optional dep of next/transformers). LGPL-3.0 on dynamically-linked native code allows use in non-GPL applications as long as the user can replace the library — satisfied here because this is a pre-built shared-object (.so) binary distributed by the sharp project, not source linked into OmniRoute code. OmniRoute does not modify or statically link libvips.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-x64": {
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"justification": "Same rationale as @img/sharp-libvips-linux-x64 — musl variant of the same prebuilt libvips shared library. Dynamically linked, not modified, not statically bundled.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"lightningcss": {
|
||||
"license": "MPL-2.0",
|
||||
"justification": "MPL-2.0 is a weak, file-level copyleft: only modifications to MPL-licensed files must be released under MPL. OmniRoute does not modify lightningcss source. It is pulled in transitively as a runtime dep of vite (via monaco-editor, a production dep). MPL-2.0 is explicitly compatible with Apache/MIT combinations per the Mozilla FAQ and is routinely used in Node.js production stacks. The OmniRoute codebase remains MIT.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"lightningcss-linux-x64-gnu": {
|
||||
"license": "MPL-2.0",
|
||||
"justification": "Platform-specific native binding for lightningcss. Same MPL-2.0 rationale as lightningcss itself: file-level copyleft, not modified by OmniRoute, compatible with MIT project.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"lightningcss-linux-x64-musl": {
|
||||
"license": "MPL-2.0",
|
||||
"justification": "Musl variant of lightningcss native binding. Same MPL-2.0 rationale.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"dompurify": {
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"justification": "dompurify is dual-licensed: MPL-2.0 OR Apache-2.0. The Apache-2.0 option is permissive and compatible with MIT. OmniRoute uses it indirectly via monaco-editor and mermaid (UI rendering). The gate expression matcher already accepts (MPL-2.0 OR Apache-2.0) as an allowed expression, but this exception is kept for explicitness and audit trail.",
|
||||
"risk": "none",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"license": "CC-BY-4.0",
|
||||
"justification": "CC-BY-4.0 applies to the caniuse browser-support data (a dataset, not code). The Creative Commons Attribution license requires attribution when distributing — OmniRoute does not distribute caniuse-lite data directly to end users; it is consumed by browserslist/PostCSS at build time to generate CSS compatibility info. This is a widely accepted pattern in the Node.js ecosystem (caniuse-lite is in millions of projects). Attribution is satisfied by keeping the package in node_modules with its original license file.",
|
||||
"risk": "low",
|
||||
"reviewAt": "v4.0.0"
|
||||
},
|
||||
"tls-client-node": {
|
||||
"license": "Custom: LICENSE (Apache-2.0 + Commons Clause)",
|
||||
"justification": "TODO: revisar — tls-client-node uses Apache-2.0 with a 'Commons Clause' addendum that restricts 'Selling' the software (i.e., offering it as a hosted/commercial service whose value derives substantially from tls-client-node). OmniRoute is an open-source proxy; however if deployed as a paid SaaS/hosting service, this restriction could apply. The package is used by grokTlsClient.ts for Grok TLS fingerprinting. RISK: medium — legal review recommended before commercial deployment. Alternatives: consider replacing with a native TLS fingerprinting approach or a truly permissive library.",
|
||||
"risk": "medium",
|
||||
"reviewAt": "v3.9.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
.npmignore
21
.npmignore
@@ -9,6 +9,16 @@ 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/
|
||||
@@ -18,6 +28,17 @@ 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
|
||||
|
||||
22
.size-limit.json
Normal file
22
.size-limit.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
51
.zizmor.yml
Normal file
51
.zizmor.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
# .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-finding ignores ──────────────────────────────────────────────────────
|
||||
# Format:
|
||||
# ignores:
|
||||
# - id: <zizmor-audit-id> # e.g. "unpinned-uses", "script-injection"
|
||||
# reason: "<justification>"
|
||||
# # Optional: scope to a specific workflow or step
|
||||
# # workflow: .github/workflows/ci.yml
|
||||
#
|
||||
# Example (do not uncomment without real justification):
|
||||
#
|
||||
# ignores:
|
||||
# - id: unpinned-uses
|
||||
# reason: >
|
||||
# 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.
|
||||
# workflow: .github/workflows/ci.yml
|
||||
|
||||
ignores: []
|
||||
@@ -1,26 +1,67 @@
|
||||
# @omniroute/opencode-plugin
|
||||
|
||||
First-class OpenCode plugin for the [OmniRoute AI Gateway](https://github.com/diegosouzapw/OmniRoute). Pulls a live model catalog from `/v1/models` (including `-low`/`-medium`/`-high`/`-thinking` variants as first-class IDs), aggregates combos via `/api/combos` using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
|
||||
> **Recommended way to use OmniRoute with OpenCode.** Pulls a live model catalog from `/v1/models` (including `-low`/`-medium`/`-high`/`-thinking` variants as first-class IDs), aggregates combos via `/api/combos` using a least-common-denominator capability/limit join, sanitizes Gemini tool schemas in flight, and supports multiple side-by-side OmniRoute instances out of the box.
|
||||
|
||||
## Why this and not `@omniroute/opencode-provider`?
|
||||
|
||||
`@omniroute/opencode-provider` is the legacy config-generator package — it writes a frozen `provider.omniroute` block into `opencode.json` with a **hardcoded list of 8 models** ([`OMNIROUTE_DEFAULT_OPENCODE_MODELS`](https://github.com/diegosouzapw/OmniRoute/blob/main/%40omniroute/opencode-provider/src/index.ts#L48-L56)). It works on the CLI but in the **OpenCode Desktop / Web** builds (Tauri / Electron) the runtime re-runs the model picker and the static block surfaces only a few of those — and they drift behind the live OmniRoute catalog.
|
||||
|
||||
This plugin solves that by:
|
||||
|
||||
- Fetching `/v1/models` and `/api/combos` **at OpenCode startup, in Node.js** — no CORS, no WebView restrictions
|
||||
- Emitting the provider block **dynamically** in the plugin's `config`/`provider` hook — so `opencode.json` only needs the plugin entry, not a static `provider.omniroute`
|
||||
- Re-fetching on a configurable TTL (default 5 min), so new models / combo changes in the OmniRoute UI appear without restarting OpenCode
|
||||
- Computing `limit.context` for combos as `min(member.context_length)` from the live catalog (no more `null` values that cause 4K-token truncation)
|
||||
- **Auto-pickup of `interleaved` capability** for thinking models (merged via PR #3138)
|
||||
|
||||
**If you only have the legacy `opencode-provider` block in your `opencode.json`, replace it with a single plugin entry.** No other config changes required — the same `auth.json` API key works.
|
||||
|
||||
## Install
|
||||
|
||||
Once published to npm:
|
||||
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:
|
||||
|
||||
```sh
|
||||
npm install @omniroute/opencode-plugin
|
||||
# 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
|
||||
```
|
||||
|
||||
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`:
|
||||
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:
|
||||
|
||||
```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)
|
||||
## Quick start (single instance, manual)
|
||||
|
||||
```jsonc
|
||||
// opencode.json
|
||||
@@ -28,7 +69,7 @@ Peer dep: `@opencode-ai/plugin` (managed by your OpenCode install).
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": [
|
||||
[
|
||||
"@omniroute/opencode-plugin",
|
||||
"./plugins/omniroute-opencode-plugin/dist/index.js",
|
||||
{
|
||||
"providerId": "omniroute",
|
||||
"baseURL": "https://or.example.com",
|
||||
|
||||
698
@omniroute/opencode-plugin/package-lock.json
generated
698
@omniroute/opencode-plugin/package-lock.json
generated
@@ -26,9 +26,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -43,9 +43,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -60,9 +60,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -77,9 +77,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -94,9 +94,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -111,9 +111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -128,9 +128,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -145,9 +145,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -162,9 +162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -179,9 +179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -196,9 +196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -213,9 +213,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -230,9 +230,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -247,9 +247,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -264,9 +264,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -281,9 +281,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -298,9 +298,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -315,9 +315,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -332,9 +332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -349,9 +349,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -366,9 +366,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -383,9 +383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -400,9 +400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -417,9 +417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -434,9 +434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -451,9 +451,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1165,9 +1165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1178,32 +1178,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
@@ -1843,490 +1843,6 @@
|
||||
"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",
|
||||
|
||||
@@ -26,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",
|
||||
"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",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -69,5 +69,8 @@
|
||||
"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
74
@omniroute/opencode-plugin/src/logger.ts
Normal file
74
@omniroute/opencode-plugin/src/logger.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Structured logger for the OmniRoute plugin.
|
||||
*
|
||||
* Levels: error < warn < info < debug
|
||||
* Default: warn (matches current console.warn behavior)
|
||||
* Set via features.logLevel in plugin options.
|
||||
*/
|
||||
|
||||
export type LogLevel = "error" | "warn" | "info" | "debug";
|
||||
|
||||
const LEVEL_ORDER: Record<LogLevel, number> = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
};
|
||||
|
||||
const TAG = "[omniroute-plugin]";
|
||||
|
||||
function shouldLog(current: LogLevel, target: LogLevel): boolean {
|
||||
return LEVEL_ORDER[current] >= LEVEL_ORDER[target];
|
||||
}
|
||||
|
||||
let _level: LogLevel = "warn";
|
||||
|
||||
export function setLogLevel(level: LogLevel): void {
|
||||
_level = level;
|
||||
}
|
||||
|
||||
export function getLogLevel(): LogLevel {
|
||||
return _level;
|
||||
}
|
||||
|
||||
function fmt(level: LogLevel, msg: string, tag?: string): string {
|
||||
const prefix = tag ? `${TAG}${tag}` : TAG;
|
||||
return `${prefix} [${level.toUpperCase()}] ${msg}`;
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
error(msg: string, ...args: unknown[]): void {
|
||||
if (shouldLog(_level, "error")) console.error(fmt("error", msg), ...args);
|
||||
},
|
||||
warn(msg: string, ...args: unknown[]): void {
|
||||
if (shouldLog(_level, "warn")) console.warn(fmt("warn", msg), ...args);
|
||||
},
|
||||
info(msg: string, ...args: unknown[]): void {
|
||||
if (shouldLog(_level, "info")) console.warn(fmt("info", msg), ...args);
|
||||
},
|
||||
debug(msg: string, ...args: unknown[]): void {
|
||||
if (shouldLog(_level, "debug")) console.warn(fmt("debug", msg), ...args);
|
||||
},
|
||||
/** Always emit regardless of level (for critical init breadcrumbs). */
|
||||
always(msg: string, ...args: unknown[]): void {
|
||||
console.warn(TAG, msg, ...args);
|
||||
},
|
||||
|
||||
// ── Tagged child loggers ──────────────────────────────────────────────
|
||||
child(tag: string) {
|
||||
return {
|
||||
error: (msg: string, ...args: unknown[]) =>
|
||||
shouldLog(_level, "error") &&
|
||||
console.error(fmt("error", msg, tag), ...args),
|
||||
warn: (msg: string, ...args: unknown[]) =>
|
||||
shouldLog(_level, "warn") &&
|
||||
console.warn(fmt("warn", msg, tag), ...args),
|
||||
info: (msg: string, ...args: unknown[]) =>
|
||||
shouldLog(_level, "info") &&
|
||||
console.warn(fmt("info", msg, tag), ...args),
|
||||
debug: (msg: string, ...args: unknown[]) =>
|
||||
shouldLog(_level, "debug") &&
|
||||
console.warn(fmt("debug", msg, tag), ...args),
|
||||
};
|
||||
},
|
||||
};
|
||||
301
@omniroute/opencode-plugin/src/naming.ts
Normal file
301
@omniroute/opencode-plugin/src/naming.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* Universal model naming template for the OmniRoute plugin.
|
||||
*
|
||||
* Naming pipeline:
|
||||
* [tag] <provider-label><separator><display-name><suffix>
|
||||
*
|
||||
* [Free] <provider> - <name> · <budget> ← free model
|
||||
* Auto: <variant> (<N>p) ← auto combo
|
||||
* Combo: <name> ← DB combo
|
||||
* <provider> - <name> ← regular model
|
||||
*/
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Separator between provider label and model display name. */
|
||||
export const PROVIDER_TAG_SEPARATOR = " - ";
|
||||
|
||||
/** Threshold beyond which providerDisplayName is abbreviated. */
|
||||
const PROVIDER_LABEL_MAX_CHARS = 12;
|
||||
|
||||
/** Aliases longer than this get title-case instead of UPPER. */
|
||||
const ALIAS_UPPER_MAX_CHARS = 5;
|
||||
|
||||
// ── Auto Combo Types ─────────────────────────────────────────────────────
|
||||
|
||||
export type AutoVariant =
|
||||
| "coding"
|
||||
| "fast"
|
||||
| "cheap"
|
||||
| "offline"
|
||||
| "smart"
|
||||
| "lkgp";
|
||||
|
||||
export const AUTO_VARIANTS: AutoVariant[] = [
|
||||
"coding",
|
||||
"fast",
|
||||
"cheap",
|
||||
"offline",
|
||||
"smart",
|
||||
"lkgp",
|
||||
];
|
||||
|
||||
export const AUTO_VARIANT_DESCRIPTIONS: Record<
|
||||
AutoVariant | "default",
|
||||
string
|
||||
> = {
|
||||
default: "Best provider via scoring",
|
||||
coding: "Quality-first for code tasks",
|
||||
fast: "Latency-optimized routing",
|
||||
cheap: "Cost-optimized routing",
|
||||
offline: "Offline-friendly providers",
|
||||
smart: "Quality-first with exploration",
|
||||
lkgp: "Last-Known-Good-Provider routing",
|
||||
};
|
||||
|
||||
// ── Free Model Types ─────────────────────────────────────────────────────
|
||||
|
||||
export type FreeModelFreeType =
|
||||
| "recurring-daily"
|
||||
| "recurring-monthly"
|
||||
| "recurring-credit"
|
||||
| "one-time-initial"
|
||||
| "keyless"
|
||||
| "discontinued";
|
||||
|
||||
// ── Provider Label ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Title-case a long, lowercase-looking alias.
|
||||
* `antigravity` → `Antigravity`
|
||||
*/
|
||||
function titleCaseAlias(alias: string): string {
|
||||
if (alias.length === 0) return alias;
|
||||
return alias.charAt(0).toUpperCase() + alias.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the short label for an upstream provider.
|
||||
*
|
||||
* Rules:
|
||||
* 1. Trim `providerDisplayName`. If ≤12 chars → use verbatim.
|
||||
* 2. Alias ≤5 chars → UPPER(alias). Alias >5 → titleCase.
|
||||
* 3. Neither → undefined.
|
||||
*/
|
||||
export function shortProviderLabel(
|
||||
enrichment:
|
||||
| { providerDisplayName?: string; providerAlias?: string }
|
||||
| undefined,
|
||||
): string | undefined {
|
||||
if (!enrichment) return undefined;
|
||||
const raw =
|
||||
typeof enrichment.providerDisplayName === "string"
|
||||
? enrichment.providerDisplayName.trim()
|
||||
: "";
|
||||
if (raw.length > 0 && raw.length <= PROVIDER_LABEL_MAX_CHARS) return raw;
|
||||
const alias =
|
||||
typeof enrichment.providerAlias === "string"
|
||||
? enrichment.providerAlias.trim()
|
||||
: "";
|
||||
if (alias.length > 0) {
|
||||
return alias.length <= ALIAS_UPPER_MAX_CHARS
|
||||
? alias.toUpperCase()
|
||||
: titleCaseAlias(alias);
|
||||
}
|
||||
// Long displayName with no alias to fall back on: keep the long label
|
||||
// rather than dropping the provider prefix entirely.
|
||||
return raw.length > 0 ? raw : undefined;
|
||||
}
|
||||
|
||||
// ── Free Label ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalise display name so free-tier models get a consistent `[Free] ` prefix.
|
||||
*
|
||||
* "GPT-4.1 (Free)" → "[Free] GPT-4.1"
|
||||
* "DeepSeek V4 Flash Free" → "[Free] DeepSeek V4 Flash"
|
||||
* "Claude Opus 4.7" → "Claude Opus 4.7" (unchanged)
|
||||
*/
|
||||
export function normaliseFreeLabel(name: string): string {
|
||||
// Bounded whitespace quantifiers ({0,8}/{1,8}) avoid the polynomial-ReDoS
|
||||
// backtracking that unbounded \s* before an anchored \s*$ would allow on
|
||||
// attacker-influenced display names. 8 covers any realistic label spacing.
|
||||
const cleaned = name
|
||||
.replace(/\s{0,8}\(free\)\s{0,8}$/i, "")
|
||||
.replace(/[\s-]{1,8}free\s{0,8}$/i, "")
|
||||
.trim();
|
||||
const wasFree = cleaned.length < name.trim().length;
|
||||
if (!wasFree) return name;
|
||||
return `[Free] ${cleaned}`;
|
||||
}
|
||||
|
||||
// ── Free Budget Formatting ────────────────────────────────────────────────
|
||||
|
||||
function fmtTokens(n: number): string {
|
||||
if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K";
|
||||
return String(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a free model budget into a short human-readable suffix.
|
||||
*
|
||||
* recurring-daily → "25M tokens/day"
|
||||
* recurring-monthly → "25M tokens/month"
|
||||
* recurring-credit → "10M credits"
|
||||
* one-time-initial → "1M credits (one-time)"
|
||||
* keyless → "(keyless)"
|
||||
* discontinued → "(discontinued)"
|
||||
*/
|
||||
export function formatFreeBudget(params: {
|
||||
freeType: FreeModelFreeType;
|
||||
monthlyTokens?: number;
|
||||
creditTokens?: number;
|
||||
}): string {
|
||||
const { freeType, monthlyTokens = 0, creditTokens = 0 } = params;
|
||||
|
||||
switch (freeType) {
|
||||
case "recurring-daily":
|
||||
return `${fmtTokens(monthlyTokens)} tokens/day`;
|
||||
case "recurring-monthly":
|
||||
return `${fmtTokens(monthlyTokens)} tokens/month`;
|
||||
case "recurring-credit":
|
||||
return `${fmtTokens(creditTokens)} credits`;
|
||||
case "one-time-initial":
|
||||
return `${fmtTokens(creditTokens)} credits (one-time)`;
|
||||
case "keyless":
|
||||
return "(keyless)";
|
||||
case "discontinued":
|
||||
return "(discontinued)";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto Combo Naming ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Format auto combo display name.
|
||||
*
|
||||
* "Auto: Coding (4p)"
|
||||
* "Auto: Default (6p)"
|
||||
* "Auto" (no candidate count when unknown)
|
||||
*/
|
||||
export function formatAutoComboName(
|
||||
variant: AutoVariant | undefined,
|
||||
candidateCount?: number,
|
||||
): string {
|
||||
const label = variant
|
||||
? variant.charAt(0).toUpperCase() + variant.slice(1)
|
||||
: "Default";
|
||||
const count =
|
||||
typeof candidateCount === "number" && candidateCount > 0
|
||||
? ` (${candidateCount}p)`
|
||||
: "";
|
||||
return `Auto: ${label}${count}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the model ID for an auto combo entry.
|
||||
* "auto/coding", "auto/fast", "auto" (default).
|
||||
*/
|
||||
export function autoComboModelId(variant: AutoVariant | undefined): string {
|
||||
return variant ? `auto/${variant}` : "auto";
|
||||
}
|
||||
|
||||
// ── Universal Display Name Builder ────────────────────────────────────────
|
||||
|
||||
export interface ModelDisplayNameParams {
|
||||
/** Raw model ID (e.g. "cc/claude-sonnet-4-6"). */
|
||||
rawId: string;
|
||||
/** Enrichment display name (e.g. "Claude Sonnet 4.6"). */
|
||||
enrichmentName?: string;
|
||||
/** Provider tag enrichment. */
|
||||
providerAlias?: string;
|
||||
/** Human-readable upstream provider label. */
|
||||
providerDisplayName?: string;
|
||||
/** Whether model is free tier. */
|
||||
isFree?: boolean;
|
||||
/** Free model budget info. */
|
||||
freeType?: FreeModelFreeType;
|
||||
/** Monthly token budget (for recurring free models). */
|
||||
monthlyTokens?: number;
|
||||
/** Credit token budget (for credit-based free models). */
|
||||
creditTokens?: number;
|
||||
/** Whether this is a combo entry (skip provider tag). */
|
||||
isCombo?: boolean;
|
||||
/** Whether this is an auto combo entry. */
|
||||
isAutoCombo?: boolean;
|
||||
/** Auto combo variant. */
|
||||
autoVariant?: AutoVariant;
|
||||
/** Auto combo candidate count. */
|
||||
autoCandidateCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the final display name following the universal template.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Auto combo → "Auto: <variant> (<N>p)"
|
||||
* 2. DB combo → "Combo: <name>"
|
||||
* 3. Free + enrichment + provider tag → "[Free] <label> - <name> · <budget>"
|
||||
* 4. Free + enrichment → "[Free] <name> · <budget>"
|
||||
* 5. Free + raw → "[Free] <rawId> · <budget>"
|
||||
* 6. Enrichment + provider tag → "<label> - <name>"
|
||||
* 7. Enrichment only → "<name>"
|
||||
* 8. Raw fallback → normaliseFreeLabel(rawId)
|
||||
*/
|
||||
export function buildModelDisplayName(params: ModelDisplayNameParams): string {
|
||||
// Auto combos
|
||||
if (params.isAutoCombo) {
|
||||
return formatAutoComboName(params.autoVariant, params.autoCandidateCount);
|
||||
}
|
||||
|
||||
// Determine base name — strip any existing free suffix first
|
||||
const rawBase =
|
||||
params.enrichmentName && params.enrichmentName.trim().length > 0
|
||||
? params.enrichmentName
|
||||
: params.rawId;
|
||||
const cleanedBase = rawBase
|
||||
.replace(/\s*\(free\)\s*$/i, "")
|
||||
.replace(/[\s-]+free\s*$/i, "")
|
||||
.trim();
|
||||
const wasFree = cleanedBase.length < rawBase.trim().length;
|
||||
const isFree = !!params.isFree || wasFree;
|
||||
|
||||
let baseName = cleanedBase;
|
||||
|
||||
// Provider tag (skip for combos)
|
||||
if (!params.isCombo) {
|
||||
const label = shortProviderLabel({
|
||||
providerDisplayName: params.providerDisplayName,
|
||||
providerAlias: params.providerAlias,
|
||||
});
|
||||
if (label) {
|
||||
const prefix = `${label}${PROVIDER_TAG_SEPARATOR}`;
|
||||
if (!baseName.startsWith(prefix)) {
|
||||
baseName = `${prefix}${baseName}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend [Free] if applicable (AFTER provider tag for correct ordering)
|
||||
if (isFree) {
|
||||
baseName = `[Free] ${baseName}`;
|
||||
}
|
||||
|
||||
// Free budget suffix
|
||||
if (isFree && params.freeType) {
|
||||
const budget = formatFreeBudget({
|
||||
freeType: params.freeType,
|
||||
monthlyTokens: params.monthlyTokens,
|
||||
creditTokens: params.creditTokens,
|
||||
});
|
||||
if (budget) {
|
||||
baseName = `${baseName} · ${budget}`;
|
||||
}
|
||||
}
|
||||
|
||||
return baseName;
|
||||
}
|
||||
74
@omniroute/opencode-plugin/tests/auto-combo-context.test.ts
Normal file
74
@omniroute/opencode-plugin/tests/auto-combo-context.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
291
@omniroute/opencode-plugin/tests/fork-features.test.ts
Normal file
291
@omniroute/opencode-plugin/tests/fork-features.test.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Tests for the 3 mrmm-fork features backported to @omniroute/opencode-plugin:
|
||||
*
|
||||
* 1. `normaliseFreeLabel` — free-tier model display names get a consistent
|
||||
* `[Free] ` prefix instead of trailing "(Free)" or ad-hoc "free" words.
|
||||
*
|
||||
* 2. `resolveApiBlock` — per-provider-prefix API format routing. Anthropic
|
||||
* prefixes (`cc/`, `claude/`, `anthropic/`, `kiro/`, `kr/`) get the
|
||||
* Anthropic SDK block; everything else gets OpenAI-compat.
|
||||
*
|
||||
* 3. `debugLog` — JSONL request/response capture, gated by
|
||||
* `features.debugLog` and togglable at runtime via
|
||||
* `debugLogEnabled/SetEnabled`.
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
normaliseFreeLabel,
|
||||
resolveApiBlock,
|
||||
DEFAULT_ANTHROPIC_PREFIXES,
|
||||
ensureV1Suffix,
|
||||
debugLogEnabled,
|
||||
debugLogSetEnabled,
|
||||
debugLogClear,
|
||||
debugLogRead,
|
||||
debugLogAppend,
|
||||
createDebugLoggingFetch,
|
||||
DebugLogEntry,
|
||||
} from "../src/index.js";
|
||||
|
||||
// ── 1. normaliseFreeLabel ────────────────────────────────────────────────────
|
||||
|
||||
test("normaliseFreeLabel: '(Free)' suffix becomes [Free] prefix", () => {
|
||||
assert.equal(normaliseFreeLabel("GPT-4.1 (Free)"), "[Free] GPT-4.1");
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: trailing ' Free' word becomes [Free] prefix", () => {
|
||||
assert.equal(
|
||||
normaliseFreeLabel("DeepSeek V4 Flash Free"),
|
||||
"[Free] DeepSeek V4 Flash"
|
||||
);
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: trailing '-free' (hyphen) becomes [Free] prefix", () => {
|
||||
assert.equal(normaliseFreeLabel("Llama 4 Scout-free"), "[Free] Llama 4 Scout");
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: case-insensitive (FREE, Free, free all match)", () => {
|
||||
assert.equal(normaliseFreeLabel("Model A FREE"), "[Free] Model A");
|
||||
assert.equal(normaliseFreeLabel("Model A free"), "[Free] Model A");
|
||||
assert.equal(normaliseFreeLabel("Model A Free"), "[Free] Model A");
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: names without 'free' pass through unchanged", () => {
|
||||
assert.equal(normaliseFreeLabel("Claude 4.7 Opus"), "Claude 4.7 Opus");
|
||||
assert.equal(normaliseFreeLabel("GPT-5"), "GPT-5");
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: 'free' in the middle of a name is NOT rewritten", () => {
|
||||
// Only trailing/standalone "free" markers count; embedded "freedom" stays
|
||||
assert.equal(
|
||||
normaliseFreeLabel("Freedom Model"),
|
||||
"Freedom Model"
|
||||
);
|
||||
});
|
||||
|
||||
test("normaliseFreeLabel: empty / whitespace-only inputs are handled", () => {
|
||||
// Empty input returns empty; pure whitespace input passes through (no Free marker)
|
||||
assert.equal(normaliseFreeLabel(""), "");
|
||||
assert.equal(normaliseFreeLabel(" "), " ");
|
||||
});
|
||||
|
||||
// ── 2. resolveApiBlock ───────────────────────────────────────────────────────
|
||||
|
||||
test("resolveApiBlock: cc/* models get the Anthropic SDK block (no /v1)", () => {
|
||||
const block = resolveApiBlock("cc/claude-opus-4-7", "https://api.example.com");
|
||||
assert.equal(block.id, "anthropic");
|
||||
assert.equal(block.npm, "@ai-sdk/anthropic");
|
||||
assert.equal(block.url, "https://api.example.com"); // NO /v1 suffix
|
||||
});
|
||||
|
||||
test("resolveApiBlock: claude/*, anthropic/*, kiro/*, kr/* all route to Anthropic", () => {
|
||||
for (const id of [
|
||||
"claude/claude-opus-4-7",
|
||||
"anthropic/claude-sonnet-4",
|
||||
"kiro/claude-sonnet-4-5",
|
||||
"kr/claude-opus-4-6",
|
||||
]) {
|
||||
const block = resolveApiBlock(id, "https://api.example.com");
|
||||
assert.equal(block.id, "anthropic", `${id} should route to Anthropic`);
|
||||
assert.equal(block.npm, "@ai-sdk/anthropic");
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveApiBlock: non-Anthropic models get OpenAI-compat with /v1", () => {
|
||||
const block = resolveApiBlock("gpt-4o", "https://api.example.com");
|
||||
assert.equal(block.id, "openai-compatible");
|
||||
assert.equal(block.npm, "@ai-sdk/openai-compatible");
|
||||
assert.equal(block.url, "https://api.example.com/v1");
|
||||
});
|
||||
|
||||
test("resolveApiBlock: user can override anthropicPrefixes to add custom prefixes", () => {
|
||||
const block = resolveApiBlock("myproxy/claude-opus", "https://api.example.com", {
|
||||
anthropicPrefixes: ["myproxy"],
|
||||
});
|
||||
assert.equal(block.id, "anthropic");
|
||||
assert.equal(block.npm, "@ai-sdk/anthropic");
|
||||
});
|
||||
|
||||
test("resolveApiBlock: empty anthropicPrefixes forces OpenAI-compat for everything", () => {
|
||||
const block = resolveApiBlock("cc/claude-opus", "https://api.example.com", {
|
||||
anthropicPrefixes: [],
|
||||
});
|
||||
assert.equal(block.id, "openai-compatible");
|
||||
});
|
||||
|
||||
test("resolveApiBlock: baseURL that already ends in /v1 is not double-suffixed (OpenAI path)", () => {
|
||||
const block = resolveApiBlock("gpt-4o", "https://api.example.com/v1");
|
||||
assert.equal(block.url, "https://api.example.com/v1"); // idempotent
|
||||
});
|
||||
|
||||
test("resolveApiBlock: model id without '/' uses the id as prefix", () => {
|
||||
const block = resolveApiBlock("claude-opus-4-7", "https://api.example.com");
|
||||
// The whole id is the prefix, which doesn't match "cc"/"claude" etc.
|
||||
// So it falls through to OpenAI-compat.
|
||||
assert.equal(block.id, "openai-compatible");
|
||||
});
|
||||
|
||||
test("DEFAULT_ANTHROPIC_PREFIXES: contains the canonical Anthropic aliases", () => {
|
||||
assert.deepEqual(DEFAULT_ANTHROPIC_PREFIXES, [
|
||||
"cc",
|
||||
"claude",
|
||||
"anthropic",
|
||||
"kiro",
|
||||
"kr",
|
||||
]);
|
||||
});
|
||||
|
||||
test("ensureV1Suffix: idempotent for URLs that already end in /v1", () => {
|
||||
assert.equal(ensureV1Suffix("https://api.example.com/v1"), "https://api.example.com/v1");
|
||||
assert.equal(
|
||||
ensureV1Suffix("https://api.example.com/v1/"),
|
||||
"https://api.example.com/v1" // trailing slash is stripped
|
||||
);
|
||||
});
|
||||
|
||||
test("ensureV1Suffix: appends /v1 when missing", () => {
|
||||
assert.equal(ensureV1Suffix("https://api.example.com"), "https://api.example.com/v1");
|
||||
assert.equal(ensureV1Suffix("https://api.example.com/"), "https://api.example.com/v1");
|
||||
});
|
||||
|
||||
// ── 3. debugLog ──────────────────────────────────────────────────────────────
|
||||
|
||||
test("debugLog: default state is disabled", () => {
|
||||
debugLogClear("test-provider-disabled-default");
|
||||
assert.equal(debugLogEnabled("test-provider-disabled-default"), false);
|
||||
});
|
||||
|
||||
test("debugLogSetEnabled + debugLogEnabled: roundtrip", () => {
|
||||
debugLogSetEnabled("test-provider-toggle", true);
|
||||
assert.equal(debugLogEnabled("test-provider-toggle"), true);
|
||||
debugLogSetEnabled("test-provider-toggle", false);
|
||||
assert.equal(debugLogEnabled("test-provider-toggle"), false);
|
||||
});
|
||||
|
||||
test("debugLogAppend + debugLogRead: roundtrip preserves entry shape", () => {
|
||||
const providerId = "test-provider-readroundtrip";
|
||||
debugLogClear(providerId);
|
||||
const entry: DebugLogEntry = {
|
||||
reqId: "req-1",
|
||||
providerId,
|
||||
ts: 1700000000000,
|
||||
url: "https://api.example.com/v1/chat",
|
||||
method: "POST",
|
||||
reqHeaders: { "content-type": "application/json" },
|
||||
reqBody: { model: "gpt-4o", messages: [] },
|
||||
resStatus: 200,
|
||||
resHeaders: { "content-type": "application/json" },
|
||||
resBody: { choices: [] },
|
||||
durationMs: 42,
|
||||
};
|
||||
debugLogAppend(entry);
|
||||
const read = debugLogRead(providerId, 10);
|
||||
assert.equal(read.length, 1);
|
||||
assert.deepEqual(read[0], entry);
|
||||
});
|
||||
|
||||
test("createDebugLoggingFetch: passes through when disabled", async () => {
|
||||
const providerId = "test-provider-passthrough";
|
||||
debugLogClear(providerId);
|
||||
debugLogSetEnabled(providerId, false);
|
||||
const calls: unknown[] = [];
|
||||
const inner: typeof fetch = async (input) => {
|
||||
calls.push(input);
|
||||
return new Response("ok", { status: 200 });
|
||||
};
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, false);
|
||||
const res = await wrapped("https://api.example.com/v1/chat");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(calls.length, 1);
|
||||
// No log entry should be written when disabled
|
||||
assert.equal(debugLogRead(providerId).length, 0);
|
||||
});
|
||||
|
||||
test("createDebugLoggingFetch: captures request/response when enabled", async () => {
|
||||
const providerId = "test-provider-captures";
|
||||
debugLogClear(providerId);
|
||||
const inner: typeof fetch = async () =>
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
||||
const res = await wrapped("https://api.example.com/v1/chat", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ model: "gpt-4o" }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
const entries = debugLogRead(providerId);
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].method, "POST");
|
||||
assert.equal(entries[0].resStatus, 200);
|
||||
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
||||
assert.deepEqual(entries[0].reqBody, { model: "gpt-4o" });
|
||||
});
|
||||
|
||||
test("createDebugLoggingFetch: records error without crashing the wrapped fetch", async () => {
|
||||
const providerId = "test-provider-error";
|
||||
debugLogClear(providerId);
|
||||
const inner: typeof fetch = async () => {
|
||||
throw new Error("network down");
|
||||
};
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
||||
await assert.rejects(wrapped("https://api.example.com/v1/chat"), /network down/);
|
||||
const entries = debugLogRead(providerId);
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].resStatus, null);
|
||||
assert.equal(entries[0].error, "network down");
|
||||
});
|
||||
|
||||
// ── Regression tests for the 3 HIGH-priority bot review fixes ───────────────
|
||||
|
||||
test("createDebugLoggingFetch: URL instance input is captured (not 'undefined')", async () => {
|
||||
const providerId = "test-provider-url-input";
|
||||
debugLogClear(providerId);
|
||||
const inner: typeof fetch = async () =>
|
||||
new Response("ok", { status: 200 });
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
||||
await wrapped(new URL("https://api.example.com/v1/chat"));
|
||||
const entries = debugLogRead(providerId);
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
||||
assert.notEqual(entries[0].url, undefined);
|
||||
});
|
||||
|
||||
test("createDebugLoggingFetch: Request object input captures URL and headers", async () => {
|
||||
const providerId = "test-provider-request-input";
|
||||
debugLogClear(providerId);
|
||||
const inner: typeof fetch = async () =>
|
||||
new Response("ok", { status: 200 });
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
||||
const req = new Request("https://api.example.com/v1/chat", {
|
||||
method: "POST",
|
||||
headers: { "x-test": "yes" },
|
||||
});
|
||||
await wrapped(req);
|
||||
const entries = debugLogRead(providerId);
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
||||
assert.equal(entries[0].reqHeaders["x-test"], "yes");
|
||||
});
|
||||
|
||||
test("createDebugLoggingFetch: SSE response is NOT buffered (resBody is the stream marker)", async () => {
|
||||
const providerId = "test-provider-sse";
|
||||
debugLogClear(providerId);
|
||||
const inner: typeof fetch = async () =>
|
||||
new Response("data: hello\n\n", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
||||
const res = await wrapped("https://api.example.com/v1/stream");
|
||||
// The response body must remain readable downstream
|
||||
const txt = await res.text();
|
||||
assert.equal(txt, "data: hello\n\n");
|
||||
const entries = debugLogRead(providerId);
|
||||
assert.equal(entries.length, 1);
|
||||
assert.equal(entries[0].resBody, "[stream]", "SSE responses must not be buffered");
|
||||
});
|
||||
@@ -1,5 +1,23 @@
|
||||
# @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.
|
||||
|
||||
698
@omniroute/opencode-provider/package-lock.json
generated
698
@omniroute/opencode-provider/package-lock.json
generated
@@ -19,9 +19,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -36,9 +36,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -53,9 +53,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -70,9 +70,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -87,9 +87,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -104,9 +104,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -121,9 +121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -138,9 +138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -155,9 +155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -172,9 +172,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -189,9 +189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -206,9 +206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -223,9 +223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -240,9 +240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -257,9 +257,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -274,9 +274,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -291,9 +291,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -308,9 +308,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -325,9 +325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -342,9 +342,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -359,9 +359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -376,9 +376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -393,9 +393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"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==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -410,9 +410,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -427,9 +427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -444,9 +444,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -974,9 +974,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -987,32 +987,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
@@ -1482,490 +1482,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@omniroute/opencode-provider",
|
||||
"version": "0.1.0",
|
||||
"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.",
|
||||
"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.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
@@ -56,5 +56,8 @@
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.28.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,8 @@ export interface OmniRouteProviderOptions {
|
||||
apiKey: string;
|
||||
/** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */
|
||||
displayName?: string;
|
||||
/** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */
|
||||
models?: readonly string[];
|
||||
/** Override the model catalog. Accepts model ids (strings) or live model entries from `fetchLiveModels`. When entries carry a `contextLength`, it is used directly — no hardcoded map needed. */
|
||||
models?: readonly (string | { id: string; contextLength?: number })[];
|
||||
/** Optional human-readable labels keyed by model id. Overridden by `modelCapabilities[id].label`. */
|
||||
modelLabels?: Record<string, string>;
|
||||
/**
|
||||
@@ -139,6 +139,12 @@ export interface OmniRouteProviderOptions {
|
||||
* for custom ids the override is used verbatim.
|
||||
*/
|
||||
modelCapabilities?: Record<string, ModelCapabilities>;
|
||||
/**
|
||||
* Optional per-model context-length overrides (tokens). Takes precedence
|
||||
* over the static `OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS` map but is
|
||||
* superseded by `contextLength` on live model entries passed via `models`.
|
||||
*/
|
||||
modelContextLengths?: Record<string, string | number>;
|
||||
/**
|
||||
* Primary model for OpenCode (top-level `model` key).
|
||||
* Emitted as `"omniroute/<id>"`. When omitted the key is not written.
|
||||
@@ -248,7 +254,12 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open
|
||||
const models: Record<string, OpenCodeModelEntry> = {};
|
||||
const seen = new Set<string>();
|
||||
for (const raw of modelList) {
|
||||
const id = typeof raw === "string" ? raw.trim() : "";
|
||||
const id =
|
||||
typeof raw === "object" && raw !== null && "id" in raw && typeof (raw as any).id === "string"
|
||||
? (raw as { id: string }).id.trim()
|
||||
: typeof raw === "string"
|
||||
? raw.trim()
|
||||
: "";
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
const defaults = OMNIROUTE_DEFAULT_MODEL_CAPABILITIES[id] ?? {};
|
||||
@@ -266,10 +277,18 @@ export function createOmniRouteProvider(options: OmniRouteProviderOptions): Open
|
||||
if (typeof merged.temperature === "boolean") entry.temperature = merged.temperature;
|
||||
if (typeof merged.tool_call === "boolean") entry.tool_call = merged.tool_call;
|
||||
|
||||
// Include context window limit when known — OpenCode reads this to
|
||||
// determine usable context length for compaction & overflow detection.
|
||||
const contextLength = OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
|
||||
if (typeof contextLength === "number" && contextLength > 0) {
|
||||
// Context window: live model entry (from API catalog) > modelContextLengths > static defaults
|
||||
const liveContext =
|
||||
typeof raw === "object" && raw !== null
|
||||
? (raw as { contextLength?: number }).contextLength
|
||||
: undefined;
|
||||
const rawContextLength =
|
||||
liveContext ??
|
||||
options.modelContextLengths?.[id] ??
|
||||
OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS[id];
|
||||
const contextLength =
|
||||
typeof rawContextLength === "string" ? parseInt(rawContextLength, 10) : rawContextLength;
|
||||
if (typeof contextLength === "number" && !isNaN(contextLength) && contextLength > 0) {
|
||||
entry.limit = { context: contextLength };
|
||||
}
|
||||
|
||||
@@ -508,7 +527,7 @@ export interface OmniRouteLiveModel {
|
||||
* const config = buildOmniRouteOpenCodeConfig({
|
||||
* baseURL: "http://localhost:20128",
|
||||
* apiKey: "sk_omniroute",
|
||||
* models: models.map((m) => m.id),
|
||||
* models, // OmniRouteLiveModel[] — contextLength auto-extracted
|
||||
* modelLabels: Object.fromEntries(models.map((m) => [m.id, m.name])),
|
||||
* });
|
||||
* ```
|
||||
|
||||
@@ -2,6 +2,9 @@ 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,
|
||||
@@ -430,6 +433,34 @@ test("createOmniRouteProvider omits limit.context for unknown model ids", () =>
|
||||
assert.equal(entry.limit, undefined);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider reads contextLength from a live model entry for ids absent from the static map", () => {
|
||||
// #3298 regression guard: the static OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS
|
||||
// map only covers the legacy 8 Claude/Gemini ids. Before this change, any
|
||||
// other model got `undefined` context (see the test above, string form) and
|
||||
// OpenCode silently fell back to its 128K internal default. A live model
|
||||
// entry carrying `contextLength` must now surface as `limit.context`.
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: [{ id: "completely-unknown-model", contextLength: 262_144 }],
|
||||
});
|
||||
const entry = provider.models["completely-unknown-model"];
|
||||
assert.ok(entry.limit, "a live contextLength should produce a limit field even for ids absent from the static map");
|
||||
assert.equal(entry.limit!.context, 262_144);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider: a live model contextLength wins over the static default map", () => {
|
||||
// `cc/claude-opus-4-8` has a static default (1_000_000). A live entry carrying
|
||||
// a different contextLength must take precedence (live > modelContextLengths >
|
||||
// static defaults).
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
apiKey: "sk_omniroute",
|
||||
models: [{ id: "cc/claude-opus-4-8", contextLength: 524_288 }],
|
||||
});
|
||||
assert.equal(provider.models["cc/claude-opus-4-8"].limit!.context, 524_288);
|
||||
});
|
||||
|
||||
test("createOmniRouteProvider serialises limit.context to JSON", () => {
|
||||
const provider = createOmniRouteProvider({
|
||||
baseURL: "http://localhost:20128",
|
||||
@@ -639,3 +670,17 @@ 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/);
|
||||
});
|
||||
|
||||
189
AGENTS.md
189
AGENTS.md
@@ -3,19 +3,53 @@
|
||||
## Project
|
||||
|
||||
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
|
||||
with **212 providers** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
with **232 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** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
with **MCP Server** (87 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
|
||||
> **Live counts (v3.8.24)**: providers 232 · MCP tools 87 · MCP scopes 30 · A2A skills 6 ·
|
||||
> open-sse services 115 · routing strategies 15 · auto-combo scoring factors 12 ·
|
||||
> DB modules 83 · DB migrations 97 · base tables 17 · search providers 11 ·
|
||||
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
|
||||
|
||||
## Doc Accuracy Discipline (read before writing any doc)
|
||||
|
||||
> **If `grep -rn "name" src/ open-sse/ bin/` returns nothing, the name does not exist. Do not document it.**
|
||||
|
||||
The recurring failure mode in AI-generated docs is _plausible-but-unverified specifics_.
|
||||
Every claim in a `.md` file under `docs/` should be verifiable against the source.
|
||||
|
||||
**Rules (enforced by `npm run check:fabricated-docs`):**
|
||||
|
||||
1. **Never state an API name, endpoint, path, CLI command, or env var without grepping for it first.**
|
||||
```bash
|
||||
grep -rn "theName" src/ open-sse/ bin/
|
||||
# 0 hits → do not document
|
||||
```
|
||||
2. **Never write a line count, file size, migration count, provider count, or strategy count from memory.**
|
||||
```bash
|
||||
wc -l <file> # exact line count
|
||||
ls <dir>/*.ts | wc -l # file count
|
||||
```
|
||||
3. **Every code example should be copy-pasted from real usage or actually run** — not synthesized.
|
||||
Link to a real call site (`path:line`) instead of inventing a signature.
|
||||
4. **Prefer citing real source (`file.ts:line`) over paraphrasing behavior** — verifiable and self-correcting.
|
||||
5. **A shorter doc that is 100% accurate beats a comprehensive one with fabrications.**
|
||||
Wrong docs cost more than missing docs, because people trust and act on them.
|
||||
|
||||
The script `scripts/check/check-fabricated-docs.mjs` extracts every route path, env var, hook
|
||||
name, function name, and file reference from `docs/**/*.md` and verifies each one against the
|
||||
codebase. Run it locally before pushing docs; it runs in CI via `npm run check:docs-all`.
|
||||
|
||||
## Stack
|
||||
|
||||
- **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/`)
|
||||
- **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/`)
|
||||
- **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/`
|
||||
- **Streaming**: SSE via `open-sse` internal workspace package
|
||||
- **Styling**: Tailwind CSS v4
|
||||
- **i18n**: next-intl with 40+ languages
|
||||
- **i18n**: next-intl with 42 locales (`src/i18n/messages/`) — refresh with `ls src/i18n/messages/*.json | wc -l`
|
||||
- **Desktop**: Electron (cross-platform: Windows, macOS, Linux)
|
||||
- **Schemas**: Zod v4 for all API / MCP input validation
|
||||
|
||||
@@ -23,28 +57,28 @@ with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop ap
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
|
||||
| Command | Description |
|
||||
| ----------------------------------- | ---------------------------------------------------------------- |
|
||||
| `npm run dev` | Start Next.js dev server |
|
||||
| Command | Description |
|
||||
| ----------------------------------- | ------------------------------------------------------------------ |
|
||||
| `npm run dev` | Start Next.js dev server |
|
||||
| `npm run build` | Production build: `next build` → `.build/next/` + assemble `dist/` |
|
||||
| `npm run build:release` | Clean rebuild + HEAD sentinel (`dist/BUILD_SHA`) — use for deploy |
|
||||
| `npm run start` | Run production build |
|
||||
| `npm run build:cli` | Build CLI package |
|
||||
| `npm run lint` | ESLint on all source files |
|
||||
| `npm run typecheck:core` | TypeScript core type checking |
|
||||
| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) |
|
||||
| `npm run check` | Run lint + test |
|
||||
| `npm run check:cycles` | Check for circular dependencies |
|
||||
| `npm run electron:dev` | Run Electron app in dev mode |
|
||||
| `npm run electron:build` | Build Electron app for current OS |
|
||||
| `npm run build:release` | Clean rebuild + HEAD sentinel (`dist/BUILD_SHA`) — use for deploy |
|
||||
| `npm run start` | Run production build |
|
||||
| `npm run build:cli` | Build CLI package |
|
||||
| `npm run lint` | ESLint on all source files |
|
||||
| `npm run typecheck:core` | TypeScript core type checking |
|
||||
| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) |
|
||||
| `npm run check` | Run lint + test |
|
||||
| `npm run check:cycles` | Check for circular dependencies |
|
||||
| `npm run electron:dev` | Run Electron app in dev mode |
|
||||
| `npm run electron:build` | Build Electron app for current OS |
|
||||
|
||||
**Build output layout:**
|
||||
|
||||
| Directory | Purpose | Gitignored |
|
||||
| --------- | --------------------------------------------------- | ---------- |
|
||||
| `src/` | Application source (TypeScript / TSX) | No |
|
||||
| `.build/` | Build intermediates (`distDir = .build/next`) | Yes |
|
||||
| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes |
|
||||
| Directory | Purpose | Gitignored |
|
||||
| --------- | -------------------------------------------------- | ---------- |
|
||||
| `src/` | Application source (TypeScript / TSX) | No |
|
||||
| `.build/` | Build intermediates (`distDir = .build/next`) | Yes |
|
||||
| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes |
|
||||
|
||||
The pipeline is a single `next build` pass — intermediates land in `.build/next/`, the
|
||||
assembled bundle in `dist/`. VPS deploys rsync `dist/` into the remote
|
||||
@@ -144,7 +178,7 @@ Always run `prettier --write` on changed files.
|
||||
|
||||
### Data Layer (`src/lib/db/`)
|
||||
|
||||
All persistence uses SQLite through **45+ domain-specific modules** in `src/lib/db/`. Top modules:
|
||||
All persistence uses SQLite through **83 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`
|
||||
@@ -154,17 +188,17 @@ All persistence uses SQLite through **45+ domain-specific modules** in `src/lib/
|
||||
- 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 45).
|
||||
Schema migrations live in `db/migrations/` (55 files) and run via `migrationRunner.ts`.
|
||||
Live count: `ls src/lib/db/*.ts | wc -l` (currently 83). Drift detection: `npm run check:docs-counts`.
|
||||
Schema migrations live in `db/migrations/` (**97 files** as of v3.8.24) and run via `migrationRunner.ts`.
|
||||
`src/lib/localDb.ts` is a **re-export layer only** — never add logic there.
|
||||
|
||||
#### DB Internals
|
||||
|
||||
- **`core.ts`**: `getDbInstance()` returns a singleton `better-sqlite3` instance with WAL
|
||||
journaling. `SCHEMA_SQL` defines 15 base tables. Helpers: `rowToCamel`, `encryptConnectionFields`.
|
||||
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**: 55 files (`001_initial_schema.sql` → `055_command_code_auth_sessions.sql`).
|
||||
- **Migrations**: 97 files (`001_initial_schema.sql` → `099_*.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`,
|
||||
@@ -180,19 +214,19 @@ Route → CORS preflight → Body validation (Zod) → Optional auth (extractApi
|
||||
→ API key policy enforcement (enforceApiKeyPolicy) → Handler delegation (open-sse)
|
||||
```
|
||||
|
||||
| Route | Handler | Notes |
|
||||
| ------------------------------- | ------------------------- | ----------------------------------------- |
|
||||
| `chat/completions/route.ts` | `handleChat()` | + prompt injection guard (clones request) |
|
||||
| `responses/route.ts` | `handleChat()` (unified) | Responses API format |
|
||||
| `embeddings/route.ts` | `handleEmbedding()` | Model listing + creation |
|
||||
| `images/generations/route.ts` | `handleImageGeneration()` | Model listing + creation |
|
||||
| `audio/transcriptions/route.ts` | audio handler | Multipart form data |
|
||||
| `audio/speech/route.ts` | TTS handler | Binary audio response |
|
||||
| `videos/generations/route.ts` | video handler | ComfyUI/SD WebUI |
|
||||
| `music/generations/route.ts` | music handler | ComfyUI workflows |
|
||||
| `moderations/route.ts` | moderation handler | Content safety |
|
||||
| `rerank/route.ts` | rerank handler | Document relevance |
|
||||
| `search/route.ts` | search handler | Web search (5 providers) |
|
||||
| Route | Handler | Notes |
|
||||
| ------------------------------- | ------------------------- | ------------------------------------------------------------- |
|
||||
| `chat/completions/route.ts` | `handleChat()` | + prompt injection guard (clones request) |
|
||||
| `responses/route.ts` | `handleChat()` (unified) | Responses API format |
|
||||
| `embeddings/route.ts` | `handleEmbedding()` | Model listing + creation |
|
||||
| `images/generations/route.ts` | `handleImageGeneration()` | Model listing + creation |
|
||||
| `audio/transcriptions/route.ts` | audio handler | Multipart form data |
|
||||
| `audio/speech/route.ts` | TTS handler | Binary audio response |
|
||||
| `videos/generations/route.ts` | video handler | ComfyUI/SD WebUI |
|
||||
| `music/generations/route.ts` | music handler | ComfyUI workflows |
|
||||
| `moderations/route.ts` | moderation handler | Content safety |
|
||||
| `rerank/route.ts` | rerank handler | Document relevance |
|
||||
| `search/route.ts` | search handler | Web search (12 providers per `open-sse/handlers/search.ts:6`) |
|
||||
|
||||
**No global Next.js middleware file** — interception is route-specific. Auth is optional
|
||||
(controlled by `REQUIRE_API_KEY` env). Prompt injection guard is unique to chat completions.
|
||||
@@ -302,7 +336,8 @@ Includes request/response translators with helpers for image handling.
|
||||
|
||||
### Services (`open-sse/services/`)
|
||||
|
||||
36+ service modules including: `combo.ts` (routing engine), `usage.ts`, `tokenRefresh.ts`,
|
||||
115 service modules in `open-sse/services/` (top-level only; 184 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`,
|
||||
`contextManager.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`,
|
||||
@@ -343,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** (14): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
|
||||
cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay.
|
||||
- **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.
|
||||
|
||||
@@ -356,7 +391,7 @@ Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
|
||||
|
||||
### MCP Server (`open-sse/mcp-server/`)
|
||||
|
||||
37 tools (30 base + 3 memory + 4 skills), 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (~13 scopes), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
|
||||
**87 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 33-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), 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).
|
||||
|
||||
**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,
|
||||
@@ -374,6 +409,14 @@ 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,
|
||||
@@ -382,7 +425,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** (10): Control which tool categories an API key can access. Enforcement happens
|
||||
- **Scopes** (30): 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.
|
||||
@@ -391,7 +434,7 @@ handler: async (args) => {...} }`. Zod validates inputs before the handler fires
|
||||
|
||||
JSON-RPC 2.0, SSE streaming, Task Manager with TTL cleanup.
|
||||
Agent Card at `/.well-known/agent.json`.
|
||||
Skills (5): `smartRouting.ts`, `quotaManagement.ts`, `providerDiscovery.ts`, `costAnalysis.ts`, `healthReport.ts`.
|
||||
Skills (6): `smartRouting.ts`, `quotaManagement.ts`, `providerDiscovery.ts`, `costAnalysis.ts`, `healthReport.ts`, `listCapabilities.ts`.
|
||||
|
||||
#### A2A Internals
|
||||
|
||||
@@ -484,7 +527,6 @@ 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
|
||||
|
||||
@@ -492,31 +534,32 @@ 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 (9-factor, 14 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) |
|
||||
| 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) |
|
||||
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
752
CHANGELOG.md
752
CHANGELOG.md
@@ -4,6 +4,579 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.24] — 2026-06-13
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(plugins): custom plugin marketplace support** — the plugin registry now fetches from a custom URL set in system settings (`pluginMarketplaceUrl`), falling back to the local seed registry when none is configured. Adds a `GET /api/plugins/marketplace` endpoint and a revamped Marketplace UI. ([#3656](https://github.com/diegosouzapw/OmniRoute/pull/3656) — thanks @oyi77)
|
||||
- **feat(api-keys): strict-mode controls for the Claude Code default routing path** — `Claude Code default` is now an explicit `cc/*` model permission, so an API key can allow the default path while blocking specific model families (e.g. Fable) in strict mode. Previously the default path received dynamic/unprefixed models (`sonnet`, `opus`, `claude-opus-4-8[1m]`, …) that no single permission represented, so it broke under strict permissions. ([#3776](https://github.com/diegosouzapw/OmniRoute/pull/3776) — thanks @Witroch4)
|
||||
- **feat(flags): expose the emergency budget fallback in the Feature Flags page** — `OMNIROUTE_EMERGENCY_FALLBACK` is now a runtime boolean (enabled by default, applied without restart) resolved through the feature-flag stack, so a DB override can toggle it while still honoring the raw env fallback. Follow-up to [#3741](https://github.com/diegosouzapw/OmniRoute/pull/3741) by @zoispag. ([#3752](https://github.com/diegosouzapw/OmniRoute/pull/3752) — thanks @rdself)
|
||||
- **feat(reasoning): preserve `xhigh` reasoning effort by default** — `xhigh` now passes through unless a model explicitly sets `supportsXHighEffort: false`, with the existing `max` normalization kept separate. ([#3756](https://github.com/diegosouzapw/OmniRoute/pull/3756) — thanks @rdself)
|
||||
- **feat(codex): inject OmniRoute memory into Codex Responses WebSocket requests** — retrieved memory is injected into the Responses WebSocket prepare request via the `instructions` field, with the retrieval query derived from the latest user input (skipping tool/reasoning payloads) and duplicate-safe injection. ([#3749](https://github.com/diegosouzapw/OmniRoute/pull/3749) — thanks @kkkayye)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **security(proxy): IPv6-only egress enforcement + closing IP-leak paths (L1/L2/L3)** — de-brackets IPv6 literals at the SOCKS host and the proxy-health tcpCheck (so `socks5://[2001:db8::1]` and any v6-literal proxy connect instead of dying with `ENOTFOUND`), adds a per-proxy `family` policy (`auto`/`ipv4`/`ipv6`), and enforces it end-to-end across SOCKS5/HTTP/HTTPS × global/provider/key × literal/hostname. 16 commits, TDD+BDD, 73 tests. ([#3777](https://github.com/diegosouzapw/OmniRoute/pull/3777))
|
||||
- **security(marketplace): harden the custom-URL SSRF guard** against three bypasses found by automated security review — IPv6/AAAA records (only `dns.resolve4` was checked, so a private AAAA record or an IPv6 literal slipped through), redirect-following (a public URL could 30x to an internal one), and DNS-rebinding TOCTOU. The guard now resolves A+AAAA via the canonical `isPrivateHost`, routes the fetch through `safeOutboundFetch` (public-only, blocks redirects to private hosts), and re-validates on fetch. Reachable only with management auth + a custom `pluginMarketplaceUrl`. Follow-up to [#3656](https://github.com/diegosouzapw/OmniRoute/pull/3656). ([#3774](https://github.com/diegosouzapw/OmniRoute/pull/3774))
|
||||
- **security: resolve all open CodeQL + Dependabot alerts** — CodeQL `js/insufficient-password-hash` (the semantic-cache `apiKeyId` is now a plaintext key prefix, `${apiKeyId}.${digest}`, instead of being folded into the SHA-256 digest, clearing the false positive while preserving per-key cache isolation) and a URL-substring check tightened to an exact host match; Dependabot `esbuild < 0.28.1` pinned via override in both workspaces. ([#3778](https://github.com/diegosouzapw/OmniRoute/pull/3778))
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(combo): deep audit of the combo + quota-shared routing system** — repairs 5 dead/broken rules (streaming-USD cost recording, quota-pool usage provider resolution, provider-diversity wiring, `maxComboDepth` threading, and scoring clamp/NaN-safety incl. `connectionDensity`) and revives the dead `tierAffinity`/`specificityMatch` scoring factors — root cause was a `require()` that throws under ESM, so both factors silently collapsed to `0.5`; now a static import. Validates every auto-router strategy (cost / latency / sla-aware / lkgp / `selectWithStrategy` + aliases) and the predictive-TTFT decision, adds E2E coverage (3-hop priority failover, per-target timeout failover, real `strategy:auto` dispatch), and introduces opt-in complexity-aware routing (2026) layered over the existing specificity detector. Per-target credential+proxy isolation verified clean (`AsyncLocalStorage`). 4 TDD waves, 10 new/updated test files. ([#3779](https://github.com/diegosouzapw/OmniRoute/pull/3779) — thanks @diegosouzapw)
|
||||
- fix(anthropic): normalize sampling params under extended thinking — Claude models with extended thinking (e.g. Opus 4.8 via the Claude Code provider) returned **HTTP 400** when a request carried non-default `temperature`/`top_p` (`temperature may only be set to 1 …`, `top_p must be ≥ 0.95 or unset …`). Tools like VS Code Copilot's "Ollama" BYOK send `temperature: 0.7` + `top_p: 0.9`, so every thinking-enabled Claude request failed; the proxy now drops/normalizes these params at the chokepoint so the request succeeds. ([#3780](https://github.com/diegosouzapw/OmniRoute/pull/3780) — thanks @zhiru)
|
||||
- fix(sse): pass Claude passthrough `thinking` blocks through unchanged — the Anthropic-native Claude OAuth passthrough rewrote every assistant `thinking` block to `redacted_thinking`, which the Messages API rejects (submitted thinking blocks are validated against the original response), so every multi-turn request with extended thinking failed with `400 … thinking blocks … cannot be modified` (very visible on long Claude Code tool-loops). The blocks are now passed through verbatim; the signature is validated server-side and stays valid on replay (including across an OAuth token switch), so the redaction was unnecessary. ([#3775](https://github.com/diegosouzapw/OmniRoute/pull/3775) — thanks @havockdev)
|
||||
- fix(mcp): resolve the bundled MCP server entry from `dist/` instead of the legacy `app/` path — `omniroute --mcp` crashed on npm installs with `ERR_MODULE_NOT_FOUND: Cannot find package @/lib` because `bin/mcp-server.mjs` looked for the compiled entry under `app/` (a VPS-deploy path that never exists in the npm package) and fell back to the un-bundled `.ts`. ([#3765](https://github.com/diegosouzapw/OmniRoute/pull/3765) — thanks @megamen32)
|
||||
- fix(sse): preserve streamed tool-call arguments end-to-end — incremental tool-call argument deltas could be truncated/duplicated through SSE parsing, transformation and response translation, corrupting tool calls in CLI tool-use output. Dedup now only collapses unambiguous snapshots. ([#3762](https://github.com/diegosouzapw/OmniRoute/pull/3762), closes [#3701](https://github.com/diegosouzapw/OmniRoute/issues/3701) — thanks @Mffff4)
|
||||
- fix(dashboard): repair the Logs page light-mode controls — the "Clean history" button keeps readable contrast (while preserving the red destructive affordance), request-row hover uses a cool blue tint so it no longer reads like a failed-request row, and the custom auto-refresh interval persists in `localStorage` (clamped to 1–300s). Also refreshes the Feature Flags light-mode treatment. ([#3760](https://github.com/diegosouzapw/OmniRoute/pull/3760) — thanks @rdself)
|
||||
- fix(dashboard): make the Request Logs "Clean history" action perform a full request-history purge — clears `call_logs`, legacy `request_detail_logs`, and local JSON artifacts under `DATA_DIR/call_logs` (including orphaned artifact files) via a dedicated maintenance API route, instead of a retention-only cleanup. ([#3751](https://github.com/diegosouzapw/OmniRoute/pull/3751) — thanks @rdself)
|
||||
- fix(cli): detect CLI tools installed outside the GUI PATH on macOS. macOS GUI/Electron apps don't inherit the user's login-shell PATH, so Homebrew (`/opt/homebrew/bin`), nvm and volta-installed CLIs (Cline, Codex, OpenCode, Continue, Hermes, …) were reported "not installed" and the Cline runtime couldn't be spawned. CLI detection (`omniroute doctor`) and the provider-runtime lookup now enrich the lookup PATH with the login shell's PATH (`$SHELL -ilc`, darwin-only, cached, fail-safe). ([#3321](https://github.com/diegosouzapw/OmniRoute/issues/3321) — thanks @mikmaneggahommie)
|
||||
- fix(dashboard): repair the Playground model selector for custom OpenAI/Anthropic-compatible providers. Two bugs left it unusable: (1) when the provider's catalog prefix didn't resolve, the list was filtered by the raw connection id (which matches nothing) so the selector showed nothing; and (2) selecting a provider reset the model to empty and nothing ever picked a default, so the chat failed with "Set a model in the config pane". The selector now falls back to the full catalog instead of emptying, and auto-selects the first available model. ([#3731](https://github.com/diegosouzapw/OmniRoute/issues/3731), [#3009](https://github.com/diegosouzapw/OmniRoute/issues/3009) — thanks @tjengbudi)
|
||||
|
||||
- fix(cursor): send a `ModelDetails` envelope (`model_id` + `display_model_id` + `display_name`) in the Cursor agent request, alongside the existing `RequestedModel`. Pinned Cursor Claude/GPT _thinking_ variants (e.g. `cursor/claude-opus-4-7-thinking-xhigh`) were returning an empty turn → `502 Provider returned empty content`, because Cursor needs the `ModelDetails` envelope (which `cursor-agent`'s real wire format sends) to resolve them; `RequestedModel` alone only resolves server-routed ids (`auto`/`composer-*`). The `-fast` parameter path on `RequestedModel` is preserved. ([#3714](https://github.com/diegosouzapw/OmniRoute/issues/3714))
|
||||
|
||||
- fix(docs): correct the OAuth redirect URI in the Fly.io deployment guide. It told users to register `<NEXT_PUBLIC_BASE_URL>/api/oauth/<provider>/callback`, but OmniRoute's browser OAuth flow uses a single `<NEXT_PUBLIC_BASE_URL>/callback` handler (there is no per-provider callback route). The mismatch caused GitLab Duo (and any OAuth provider) to reject the flow with "The redirect URI included is not valid". Added a regression guard test. ([#3732](https://github.com/diegosouzapw/OmniRoute/issues/3732))
|
||||
|
||||
- fix(providers): give Ollama Cloud's `kimi-k2.7-code` its real capabilities (262K context, 262K max output, vision + thinking + tools) instead of the degraded `128000 / 8192` defaults. The model had no spec/registry entry, so importing it via "Import from /models" (whose `/v1/models` upstream returns no per-model metadata) left it as a bare custom model with fallback capabilities. Added a global `kimi-k2.7-code` model spec (parity with `kimi-k2.6`) plus a registry entry on `ollama-cloud`. ([#3761](https://github.com/diegosouzapw/OmniRoute/issues/3761) — thanks @SultanKs4)
|
||||
|
||||
- fix(providers): repair qwen-web (chat.qwen.ai) connection validation, which failed with a misleading `provider.validation.ssrf_blocked` error. qwen-web had no specialty validator, so the generic OpenAI-compatible path probed a non-existent `/api/v2/models` URL that answers with a 307 redirect — the outbound guard blocked the redirect and the route mislabeled it as an SSRF security block. Added a `qwen-web` specialty validator that probes the real session endpoint (`GET /api/v2/user`, mirroring the executor's anti-bot headers + cookie-jar replay). Also hardened `toValidationErrorResult` so a blocked redirect is only flagged `securityBlocked` when its target is a private/internal host — a benign 3xx to a public host is no longer mislabeled as an SSRF attempt (this affected every web-cookie provider, not just qwen). ([#3288](https://github.com/diegosouzapw/OmniRoute/issues/3288), [#3758](https://github.com/diegosouzapw/OmniRoute/issues/3758))
|
||||
|
||||
- fix(oauth): stop nulling the stored `refresh_token` of non-rotating providers when a proactive health-check refresh fails with `invalid_grant`. The destructive `refreshToken: null` write in `tokenHealthCheck` was only meant for rotating one-time-use tokens (Codex/OpenAI), but it also fired for Google-family providers (gemini-cli / antigravity / gemini) whose refresh tokens are non-rotating. Once nulled, the connection reported "No valid refresh token available" and could never recover even after re-activation. The token is now preserved (gated on `isRotatingProvider`) so it stays as the recovery artifact. ([#3679](https://github.com/diegosouzapw/OmniRoute/issues/3679) — thanks @3xa228148)
|
||||
|
||||
- fix(dashboard): self-host the Material Symbols icon font instead of loading it from the Google Fonts CDN. On networks where `fonts.googleapis.com` is unreachable (e.g. mainland China), the icon ligature font never loaded, so every icon rendered as its literal text name (`smart_toy`, `visibility`, …) and the layout broke — especially after importing many models. The font is now bundled locally via the `material-symbols` package (`@import "material-symbols/outlined.css"` in `globals.css`), removing the runtime CDN dependency. ([#3695](https://github.com/diegosouzapw/OmniRoute/issues/3695) — thanks @lqyiwwx)
|
||||
|
||||
- fix(antigravity): skip Google One AI credits retry on `full_quota_exhausted` verdict — antigravity executor now calls `decide429()` before attempting the credits retry so that a quota-exhausted account (24h cooldown) bypasses the extra upstream HTTP call instead of hanging for up to ~41s. Also persists the cooldown in the DB via `setConnectionRateLimitUntil` so post-restart routing skips exhausted connections without re-learning the hard way. Bonus: `antigravity429Engine` now recognises the real Antigravity "Individual quota reached. Contact your administrator to enable overages." error message as `quota_exhausted`. ([#3707](https://github.com/diegosouzapw/OmniRoute/issues/3707) — thanks @andrea-kingautomation)
|
||||
|
||||
- fix(cli): `ServerSupervisor.handleExit` now coerces the exit code to a number before calling `process.exit()` — Node.js v24 throws `TypeError [ERR_INVALID_ARG_TYPE]` when `process.exit()` receives a string (e.g. `'ENOENT'` from a spawn `error` event's `err.code`). The `error` callback also now passes `-1` instead of the raw `err.code`, which is an OS error string rather than a meaningful exit code. ([#3748](https://github.com/diegosouzapw/OmniRoute/issues/3748))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(quality): Quality Gates → 100%** — completes Fase 6A (systemic hardening: a `stale-allowlist` helper applied across ~10 gates, `docs/architecture/QUALITY_GATES.md`, and ratchet engine v2 with `--require-tighten` + per-metric `eps`) and the entire Fase 7 (20 security/dead-code/mutation/tooling gates), with the Fase 8 plan documented. ([#3757](https://github.com/diegosouzapw/OmniRoute/pull/3757))
|
||||
- **docs: close the remaining documentation gaps** for proxy operations, skills internals, the memory engine, RTK customization, and compression extensibility (post-#3438 audit, 5 areas in one PR). ([#3453](https://github.com/diegosouzapw/OmniRoute/pull/3453) — thanks @oyi77)
|
||||
- **chore(quality): re-baseline `providerRegistry.ts` file-size** (4692→4703) after #3768's Ollama Cloud `kimi-k2.7-code` capability fix grew the file past the frozen baseline, turning the release's own Fast Quality Gates red. No source file is touched. ([#3770](https://github.com/diegosouzapw/OmniRoute/pull/3770))
|
||||
- **fix(publish): clean the `@omniroute/opencode-plugin` `node_modules` after the tsup build** — the hard links npm creates on Linux ended up in the published tarball as LINK entries, which the npm registry rejects with `E415 "Hard link is not allowed"`. The dependencies are only needed for the build, never shipped. (thanks @diegosouzapw)
|
||||
- **chore(docs): prune internal planning/spec artifacts and sync the i18n CHANGELOG to 3.8.24.** (thanks @diegosouzapw)
|
||||
- **test: align two unit tests left stale by this cycle's behavior changes** — `executor-codex` now asserts that GPT 5.4 Mini's `xhigh` reasoning effort passes through unchanged (the intended #3756 default; the model ships an `xhigh` catalog variant and carries no `supportsXHighEffort:false` flag) instead of the old downgrade-to-`high`, and `plugins-route-error-sanitization` now covers the new `/api/plugins/marketplace` route from #3656 (verified compliant with Hard Rule #12: imports + uses `buildErrorBody`). No production behavior change. (thanks @diegosouzapw)
|
||||
- **docs: refresh root + `docs/` documentation to the current architecture** — a full codebase audit corrected stale counts/facts (MCP 43→87 tools / ~13→30 scopes, DB 45+→83 modules / 55→97 migrations, routing 14→15 strategies, A2A 5→6 skills, Node `>=22.0.0 <23 || >=24.0.0 <27`, TypeScript 6.0) across `CLAUDE.md`, `AGENTS.md`, `README.md`, and the `docs/` index, and documented the plugin marketplace, Notion/Obsidian, and embedded-services subsystems. (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.23] — 2026-06-12
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Emergency budget fallback: opt-out env switch `OMNIROUTE_EMERGENCY_FALLBACK`** ([#3741](https://github.com/diegosouzapw/OmniRoute/pull/3741) — thanks @zoispag): adds an `OMNIROUTE_EMERGENCY_FALLBACK` environment variable that disables the budget-exhaustion emergency reroute to `nvidia/openai/gpt-oss-120b` entirely when set to `false` or `0`. Default behavior (enabled) is unchanged.
|
||||
|
||||
- **Auto-Combo: live model intelligence scoring via Arena ELO + models.dev** ([#3660](https://github.com/diegosouzapw/OmniRoute/pull/3660) — thanks @pizzav-xyz): replaces the static fitness lookup with a 5-layer resolution chain (user override → Arena ELO → models.dev tiers → hardcoded map → neutral fallback). A sync pipeline auto-fetches Arena AI leaderboard ELO scores and derives intelligence tiers from models.dev capabilities; combo picks now update as leaderboard rankings change without any manual configuration.
|
||||
|
||||
- **Vertex AI: dynamic model discovery** ([#3712](https://github.com/diegosouzapw/OmniRoute/pull/3712) — thanks @artickc): the `vertex` provider now queries the Generative Language models API at runtime to surface the full account catalog — including image-generation models (Imagen, `gemini-*-image`), embeddings, and partner models — instead of returning only the small hardcoded registry list.
|
||||
|
||||
- **Vertex AI: self-tracked USD spend on the Limits page** ([#3724](https://github.com/diegosouzapw/OmniRoute/pull/3724) — thanks @artickc): since the Google Cloud Billing API is inaccessible via the proxy credential, Vertex connections now track their own cumulative USD spend locally (based on token-cost accounting) and display it on the Limits page as "$ used since account added."
|
||||
|
||||
- **Gemini: rate-limit metadata for known per-model RPM/RPD caps** ([#3686](https://github.com/diegosouzapw/OmniRoute/pull/3686) — thanks @hartmark): injects known rate-limit headers (RPM/RPD) for Gemini models that carry per-model limits (e.g. Gemma 4's 15 RPM / generous RPD), so the cooldown engine applies them correctly instead of locking out the whole account on daily-limit hits.
|
||||
|
||||
- **Model Lockout: full settings UI with success-decay recovery** ([#3629](https://github.com/diegosouzapw/OmniRoute/pull/3629) — thanks @Chewji9875): end-to-end wiring of the per-model lockout feature — settings UI (enable/disable, configure thresholds), backend integration, structured error classification, and a success-decay mechanism that gradually recovers a locked model's fitness as successful calls accumulate. Lockout now applies to all providers when enabled, not just per-model-quota providers.
|
||||
|
||||
- **Provider display modes — All / Configured / Compact** ([#3743](https://github.com/diegosouzapw/OmniRoute/pull/3743) — thanks @rdself): adds a three-state display mode control to the Providers page. "All" shows every registered provider; "Configured" shows only providers with at least one connection; "Compact" shows configured providers in a condensed card layout for denser views.
|
||||
|
||||
- **API key cost drilldown + quota % used** ([#3742](https://github.com/diegosouzapw/OmniRoute/pull/3742) — thanks @Witroch4): the API Keys page now shows a per-key cost breakdown and the percentage of quota consumed for each key.
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **`@omniroute/opencode-plugin` bundled in the npm tarball + `omniroute setup opencode` CLI command** ([#3726](https://github.com/diegosouzapw/OmniRoute/pull/3726) — thanks @herjarsa): the plugin was never compiled as part of the publish pipeline, requiring manual extraction. Now ships pre-built inside the `omniroute` package and installed via `omniroute setup opencode` (copies plugin into `~/.config/opencode/plugins/omniroute/`, updates `opencode.json` idempotently). Also fixes `provider.models` baseURL resolution — checks `_provider.options.baseURL` as a third fallback so partner/tiered providers no longer return zero models. ([#3711](https://github.com/diegosouzapw/OmniRoute/issues/3711))
|
||||
|
||||
- **MiMoCode 403 "Illegal access" fixed** ([#3728](https://github.com/diegosouzapw/OmniRoute/pull/3728) — thanks @felipesartori): the Xiaomi free endpoint gates requests on a recognized MiMoCode system-prompt signature; OmniRoute forwarded raw requests without the marker, causing 403 on every call. The executor now injects the required anti-abuse signature.
|
||||
|
||||
- **"Test all models" flow: i18n crash, status icons, auto-hide** ([#3729](https://github.com/diegosouzapw/OmniRoute/pull/3729) — thanks @felipesartori): three bugs in the provider-detail test-all-models flow — `providerText()` crash because the `testAllResults` template requires `{ok, total}` but callers passed `{ok, error}`; missing `online`/`offline` status icons on model rows; results panel not auto-hiding after run completes.
|
||||
|
||||
- **OAuth token-refresh invalidation loop fixed** ([#3692](https://github.com/diegosouzapw/OmniRoute/pull/3692) — thanks @diegosouzapw): `refreshClaudeOAuthToken` returned `null` instead of the error sentinel on non-canonical 400 bodies, causing the caller to retry every 60 seconds — observed as 1,352 consecutive refresh attempts on one Claude account. Fixed alongside hardening of `safeResolveProxy` (proxy resolution errors now warn instead of silently falling back to DIRECT) and adding egress-IP visibility to `safeLogEvents`.
|
||||
|
||||
- **`safeLogEvents` async hotfix** (thanks @diegosouzapw): PR #3692 introduced a `lazy await import(proxyEgress)` inside a sync `safeLogEvents` — an ES syntax error that broke every consumer loading `chatHelpers` via `tsx` and caused 14 tests to fail at module load. Made `safeLogEvents` async; `void`-ed the single `chat.ts` call site.
|
||||
|
||||
- **Kiro: quota tracking for IAM Identity Center accounts** ([#3722](https://github.com/diegosouzapw/OmniRoute/pull/3722) — thanks @artickc): `getKiroUsage` returned "0 used" for IAM Identity Center accounts (and `kiro-cli` imports) because those connections frequently lack a persisted `profileArn`. Now falls back to a name-based profile lookup so quota displays correctly.
|
||||
|
||||
- **Empty Claude SSE stream now surfaces a real error** ([#3689](https://github.com/diegosouzapw/OmniRoute/pull/3689) — thanks @TechNickAI): when a Claude stream completed with lifecycle events but no content block, the proxy returned a synthetic `"[Proxy Error] The upstream API returned an empty response"` as a _successful_ assistant message. Now emits a proper SSE error event; the missing-finalizer synthetic path is preserved for streams that already produced content.
|
||||
|
||||
- **Vertex AI Express-mode API keys** ([#3690](https://github.com/diegosouzapw/OmniRoute/pull/3690) — thanks @artickc): the Vertex executor rejected every non-JSON credential with "Vertex AI requires a valid Service Account JSON." Now accepts Express-mode API key strings (`AIza*`) alongside Service Account JSON, routing them through the correct token endpoint.
|
||||
|
||||
- **Anthropic: strip `top_p` when `temperature` is set** ([#3691](https://github.com/diegosouzapw/OmniRoute/pull/3691) — thanks @zhiru): Anthropic API rejects requests containing both `temperature` and `top_p`; VS Code's Claude extension sends both in every request, causing 400s on all routed calls. The OpenAI→Claude translator now drops `top_p` when `temperature` is present.
|
||||
|
||||
- **Combo reasoning token buffer: conservative application + feature flag** ([#3700](https://github.com/diegosouzapw/OmniRoute/pull/3700) — thanks @rdself): tightens the #3588 buffer (only applies when the model is explicitly thinking-capable, has a non-default known output cap, and the full buffered value fits inside that cap) and adds a `reasoningTokenBufferEnabled` feature flag in combo defaults so users can fully disable it from Settings.
|
||||
|
||||
- **Emergency budget fallback: cross-provider credential leak fixed** ([#3699](https://github.com/diegosouzapw/OmniRoute/pull/3699) — thanks @diegosouzapw): the executor-level emergency hop re-sent the failing provider's API key to the emergency provider's endpoint (e.g. the OpenAI `Authorization` header going to `integrate.api.nvidia.com`). Now orchestrated exclusively by the routing layer, which resolves credentials for the emergency provider via account selection and no longer fires inside combo targets.
|
||||
|
||||
- **`/v1/messages/count_tokens` now honors the connection's proxy assignment** ([#3699](https://github.com/diegosouzapw/OmniRoute/pull/3699) — thanks @diegosouzapw): token count calls went DIRECT regardless of configured proxies, leaking the host IP for proxy-isolated setups. Now wraps execution in `runWithProxyContext`, exactly like chat execution.
|
||||
|
||||
- **Gemini: context-mode fallback for signatureless tool calls** ([#3688](https://github.com/diegosouzapw/OmniRoute/pull/3688) — thanks @diegosouzapw): fixes HTTP 400 on multi-turn thinking-model tool calls when `thought_signature` is unavailable — standard Gemini provider now falls back to context mode instead of sending the unsigned call.
|
||||
|
||||
- **Antigravity: preserve `gemini-3.1-pro` High/Low budget tiers** ([#3696](https://github.com/diegosouzapw/OmniRoute/pull/3696) — thanks @diegosouzapw): upstream accepts the suffixed ids; stop collapsing to bare `gemini-3.1-pro`.
|
||||
|
||||
- **Stream combo: fail over on empty/content-filtered response** ([#3685](https://github.com/diegosouzapw/OmniRoute/pull/3685) — thanks @diegosouzapw): streaming combos now route to the next target instead of surfacing a blank reply.
|
||||
|
||||
- **Qwen Web: migrated to v2 chat API** ([#3723](https://github.com/diegosouzapw/OmniRoute/pull/3723) — thanks @diegosouzapw): the legacy `/api/chat/completions` endpoint was retired upstream returning `504` HTML from Alibaba's gateway for all requests. The executor now uses the two-step v2 flow (`/api/v2/chats/new` → `/api/v2/chat/completions?chat_id=`), replays the full browser cookie jar (cna + ssxmod_itna/itna2 + token) required by Alibaba's WAF instead of only a Bearer token, parses phase-based SSE (think→reasoning, answer→content), and refreshes the model catalog to current ids (`qwen3.7-max`, `qwen3.7-plus`, `qwen3.6-plus`; legacy ids kept as aliases). 17 unit tests. (Closes [#3288](https://github.com/diegosouzapw/OmniRoute/issues/3288))
|
||||
|
||||
- **Responses API: `stream` defaults to `false` when omitted (spec compliance)** ([#3708](https://github.com/diegosouzapw/OmniRoute/pull/3708) — thanks @diegosouzapw): `/v1/responses` requests that omit `stream` no longer 502 (`STREAM_EARLY_EOF`) when the upstream returns a valid JSON response. `resolveStreamFlag` now applies the OpenAI Responses API spec default (stream=false) in addition to the existing Anthropic Messages API default — previously only `sourceFormat=claude` triggered this path, leaving `sourceFormat=openai-responses` to fall through to the wildcard-Accept heuristic (`Accept: */*` → streaming intent), which caused spec-compliant upstreams that return JSON to appear as a dead stream. Codex CLI (always sends `stream: true`) and explicit SSE clients (`Accept: text/event-stream`) are unaffected.
|
||||
|
||||
- **Semantic cache: scope to requesting API key** ([#3740](https://github.com/diegosouzapw/OmniRoute/pull/3740) — thanks @diegosouzapw): two callers with different API keys sending the same prompt and model no longer receive each other's cached responses. `generateSignature` now includes the `api_key_id` dimension in the SHA-256 hash; unauthenticated requests (no API key) remain isolated from keyed requests. Existing cache entries (generated without the key dimension) are cleared by migration `098`.
|
||||
|
||||
- **Model-family fallback: dot-notation model IDs now resolve correctly** (thanks @diegosouzapw): `getNextFamilyFallback` normalizes dots to hyphens for the initial lookup but also falls back to the bare model name, supporting IDs like `gemini-3.1-pro-high` whose dots are part of the literal name. Previously, `gemini-3.1-pro-high` silently returned null and bypassed the entire family.
|
||||
|
||||
### ♻️ Code Quality
|
||||
|
||||
- **Dashboard god-component (#3501): Phases 1g → 1t complete — ≤800 LOC target reached** ([#3717](https://github.com/diegosouzapw/OmniRoute/pull/3717), [#3721](https://github.com/diegosouzapw/OmniRoute/pull/3721), [#3725](https://github.com/diegosouzapw/OmniRoute/pull/3725), [#3727](https://github.com/diegosouzapw/OmniRoute/pull/3727) — thanks @diegosouzapw): four extraction phases bring `ProviderDetailPageClient.tsx` from 4,062 to 781 LOC — the ≤800 target set at the start of the refactor. Extracted OAuth flow helpers, quota display, traffic-inspector panel, logs viewer, combo-target editor, and remaining inline UI into standalone components under `providers/[id]/components/`.
|
||||
|
||||
### 🌍 Internationalization
|
||||
|
||||
- **zh-CN: comprehensive Simplified Chinese translation improvements** ([#3736](https://github.com/diegosouzapw/OmniRoute/pull/3736) — thanks @sdfsdfw2): broad pass on Simplified Chinese UI strings for accuracy and consistency.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **CI: bump GitHub Actions artifacts/cache actions to latest** (thanks @diegosouzapw): `actions/download-artifact` 4→8 ([#3733](https://github.com/diegosouzapw/OmniRoute/pull/3733)), `actions/cache` 4→5 ([#3734](https://github.com/diegosouzapw/OmniRoute/pull/3734)), `actions/upload-artifact` 4→7 ([#3735](https://github.com/diegosouzapw/OmniRoute/pull/3735)).
|
||||
|
||||
- **File-size ratchet baseline reconciled** ([#3705](https://github.com/diegosouzapw/OmniRoute/pull/3705) — thanks @diegosouzapw): freezes 27 inherited/previously-grown files at their current LOC and registers `providerLimits.ts` in the gate; ongoing shrink tracked via #3501.
|
||||
|
||||
- **docs: add FUNDING.yml and README Support section** ([#3698](https://github.com/diegosouzapw/OmniRoute/pull/3698) — thanks @diegosouzapw)
|
||||
|
||||
- **docs(changelog): restore `#3590` bullet lost on the v3.8.20 release branch** (thanks @diegosouzapw): the fix reached `main` pre-tag via cherry-pick `#3591`, but its changelog bullet only existed on `release/v3.8.20` after the squash-merge; restored per the 2026-06-12 release-branch leftover audit.
|
||||
|
||||
### ✅ Tests
|
||||
|
||||
- **Combo strategy fallback coverage** (`tests/unit/combo-strategy-fallbacks.test.ts`, 11 tests): fill-first / p2c / random / cost-optimized / strict-random fallback paths (previously happy-path only), price-tie stability, stale strict-random deck degradation, unknown-strategy normalization to priority, and circuit-breaker HALF_OPEN recovery inside the combo loop + `preScreenTargets` (lazy-recovery contract).
|
||||
- **`#1731` fast-skip suite restored** (`tests/integration/combo-provider-exhaustion.test.ts`): the five skipped tests were rewritten against the current routing policy (quota-exhausted 429 marks the provider for the request; transient 429 retries other connections; connection errors skip per-connection; nothing persists across requests) and re-enabled — 8/8 green.
|
||||
- **Proxy context passthrough** (`tests/integration/proxy-context-passthrough.test.ts`): combo targets each execute under their own connection's proxy; `count_tokens` runs inside the connection's proxy context.
|
||||
|
||||
---
|
||||
|
||||
## [3.8.22] — 2026-06-11
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **MiMoCode free-tier provider** ([#3659] — thanks @pizzav-xyz): new no-auth provider `mimocode` (alias `mcode`) exposing Xiaomi's `mimo-auto` model (1M context) via device-fingerprint bootstrap-JWT auth (`/api/free-ai/bootstrap` → Bearer JWT → `/api/free-ai/openai/chat`). Supports multiple accounts (N fingerprints → round-robin with exponential cooldown), re-bootstrap on 401/403, and cooldown on 429. Reuses a new generic `NoAuthAccountCard` dashboard component (also wired for `opencode`). 22 unit tests; upstream validated live during review. (Maintainer follow-up: added the required `authHeader: "none"` field to the registry entry.) Co-authored with @pizzav-xyz.
|
||||
- **Prefer Claude Code for unprefixed `claude-*` model IDs** ([#3540] — thanks @Witroch4): opt-in setting (default off) that routes bare `claude-*` model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Configurable via the `OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS` env flag or a dashboard toggle on the Claude provider page; explicit provider prefixes still win. Full layer coverage (resolver + DB setting + zod schemas + types + UI) with 6 tests. Co-authored with @Witroch4.
|
||||
- **Codex Responses-WebSocket call history** ([#3616] — thanks @kkkayye): Codex `/v1/responses` WebSocket calls are now persisted to request history — success completions plus prepare-failures, upstream WS errors and premature closes — with `sanitizeErrorMessage` applied to the stored error. Two proxy-side integration tests cover the success and failure paths.
|
||||
- **Obsidian/WebDAV**: add the `/api/v1/webdav` file server (PROPFIND/GET/PUT/DELETE/MKCOL/MOVE, Basic-Auth, path-traversal hardened) so Obsidian mobile can sync the vault (#3485, part 2). Implemented in the custom server layer (`scripts/dev/webdav-handler.mjs`) — intercepted before Next.js to support non-standard HTTP methods (`PROPFIND`, `MKCOL`, `MOVE`, `LOCK`). Reads vault path and credentials (with enc:v1: AES-256-GCM decryption) directly from the SQLite `key_value` table; credentials configured via PR1's `/api/settings/obsidian/webdav` endpoint. 36 TDD unit tests covering traversal guard, constant-time auth, decrypt round-trip, XML generation, and full CRUD cycle.
|
||||
- **Quota overview**: deactivate/activate an account directly from the quota card header (toggle button) so users can park a near-zero-quota account without navigating to the provider detail page. ([#3675](https://github.com/diegosouzapw/OmniRoute/pull/3675) — thanks @leninejunior)
|
||||
|
||||
### ♻️ Code Quality
|
||||
|
||||
- **providers/[id]**: extract `useProviderConnections`, `useProviderSettings`, `useProviderModels` hooks from the god-component — #3501 Phase 1f. `ProviderDetailPageClient.tsx`: 4,948 → 4,063 LOC (−885 lines). New hooks in `hooks/`: `useProviderConnections.ts` (954 LOC — all connection management, batch ops, proxy/CLIProxyAPI state, batch-test runner with MAX_BULK_IDS chunking), `useProviderSettings.ts` (264 LOC — Codex global service mode + Claude routing preference), `useProviderModels.ts` (155 LOC — model metadata, aliases). Frozen baselines updated. 10 Phase-1f smoke tests; typecheck/cycles/lint green. Co-authored with @oyi77.
|
||||
- **providers/[id]**: extract `useModelCompatState` hook + model sections (`ModelRow`, `PassthroughModelRow`, `PassthroughModelsSection`, `CustomModelsSection`, `CompatibleModelsSection`) from the god-component — #3501 Phase 1e. `ProviderDetailPageClient.tsx`: 6,838 → 4,922 LOC (−1,916 lines). New leaf `hooks/useModelCompatState.ts` (101 LOC); compat helpers moved to `providerPageHelpers.ts`. Frozen baselines: `providerPageHelpers.ts: 822`. 12 Phase-1e smoke tests; typecheck/cycles/lint green; #3610 auto-hide fix preserved.
|
||||
- **providers/[id]**: extract `ConnectionRow` (+ `CooldownTimer`/`inferErrorType`/`getStatusPresentation`), `ModelCompatPopover` (+ `recordToHeaderRows`), and `SiliconFlowEndpointModal` from the god-component into `components/` — #3501 Phase 1d. `ProviderDetailPageClient.tsx`: 8,092 → 6,838 LOC (−1,254 lines). Frozen baselines: `ConnectionRow.tsx: 941`. 7 new Phase-1d smoke tests; typecheck/cycles/lint green.
|
||||
- **providers/[id]: extract AddApiKeyModal + EditConnectionModal (+ WebSessionCredentialGuide) from the god-component into components/** ([#3501] Phase 1c): extracted the two heaviest inline modals — `AddApiKeyModal` (~787-LOC body) and `EditConnectionModal` (~1091-LOC body) — plus shared `WebSessionCredentialGuide` (~103 LOC) into standalone files under `providers/[id]/components/modals/` and `providers/[id]/components/` respectively. Added `ERROR_TYPE_LABELS` and `formatTimeAgo` to `providerPageHelpers.ts` (leaf) so `EditConnectionModal` and `ConnectionRow` share them without cycles. Pruned 14 now-unused imports from the god-component. `ProviderDetailPageClient.tsx`: 9,981 → 8,092 LOC (−1,889 lines). Frozen baselines: `AddApiKeyModal.tsx: 842`, `EditConnectionModal.tsx: 1170`. 6 new Phase-1c smoke tests; all 21 vitest modal tests pass; typecheck/cycles/lint green.
|
||||
- **refactor: small db/utils cleanup** ([#3523] — thanks @androw): table-driven `compression_analytics` column migration (replaces 17 repeated `ALTER TABLE` calls), a single merged `serializeJsonField` helper in `db/providers.ts` (folded two byte-identical serializers), and removal of the dead no-op `syncProviderDataToCloud`/`getProvidersNeedingRefresh` stubs from `shared/utils/machine.ts` (no remaining callers). Pure refactor; behavior unchanged.
|
||||
- **Provider-detail god-component decomposition — Phase 2b (remaining shared helpers→leaf)** ([#3501]): extended `providers/[id]/providerPageHelpers.ts` with all remaining pure helpers needed by the heavy modals (`AddApiKeyModal`/`EditConnectionModal`) before they can be extracted. Moved 22 symbols: web-session credential label/hint/check/title helpers; upstream-headers helpers (`upstreamHeadersRecordsEqual`, `headerRowsToRecord`, `effectiveUpstreamHeadersForProtocol`, `anyUpstreamHeadersBadge`, `getProtoSlice`) plus their `HeaderDraftRow`/`CompatModelRow`/`CompatModelMap`/`CompatByProtocolMap` types; Codex consts and helpers (`CODEX_REASONING_STRENGTH_OPTIONS`, `CODEX_ACCOUNT_SERVICE_TIER_VALUES`, `CODEX_GLOBAL_SERVICE_MODE_VALUES`, `getCodexServiceTierLabel`, `normalizeCodexLimitPolicy`, `getCodexRequestDefaults`, `getClaudeCodeCompatibleRequestDefaults`); misc helpers (`compatProtocolLabelKey`, `extractCommandCodeCredentialInput`, `normalizeAndValidateHttpBaseUrl`, `SILICONFLOW_ENDPOINTS`, `CommandCodeAuthFlowState`). New transitive imports wired into the leaf: `MODEL_COMPAT_PROTOCOL_KEYS` (`@/shared/constants/modelCompat`), `CodexServiceTier`/`getCodexRequestDefaults`/`getClaudeCodeCompatibleRequestDefaults` (`@/lib/providers/requestDefaults`), `CodexGlobalServiceMode` (`@/lib/providers/codexFastTier`), `WebSessionCredentialRequirement` (`./webSessionCredentials`). `ProviderDetailPageClient.tsx`: 10,288 → 9,980 LOC. Leaf module: 589 LOC (acyclic). 25-assertion unit test suite passes; smoke test 3/3; no import cycles. Co-authored with @oyi77.
|
||||
- **Provider-detail god-component decomposition — Phase 2 (helpers→lib)** ([#3501]): extracted the pure shared helpers — `ProviderMessageTranslator`/`LocalProviderMetadata` types, `providerText`/`providerCountText`/`readBooleanToggle`, and the provider base-URL + routing-tag/excluded-model parse/format block — into a new leaf `providers/[id]/providerPageHelpers.ts` (imports only `@/shared`, so the client and modals share them with no import cycle). `ProviderDetailPageClient.tsx`: 10,435 → 10,288 LOC. Unblocks extracting the heavier `AddApiKeyModal`/`EditConnectionModal` (which depend on these helpers) without cycling. The Phase 0 smoke test caught a missing transitive import (`isSelfHostedChatProvider`) at mount — now wired + locked by a new helpers unit test (12 assertions). Co-authored with @oyi77.
|
||||
|
||||
- **#3500 fully resolved** — Hard Rule #5 (no raw SQL in route handlers): all 13 internal offenders migrated to `src/lib/db/` modules across slices (call*logs, usage_history/daily_usage_summary, community_servers, usage_logs, semantic_cache, proxy_logs, skills UPDATE, db-backups). The gate's `KNOWN_RAW_SQL` set is renamed to `EXTERNAL_DB_ALLOWED` (with a back-compat alias) and now holds only the **2 external-DB reads** (`oauth/cursor/auto-import`, `oauth/kiro/auto-import`) — these open \_another app's* SQLite to import credentials, so by design they cannot live in OmniRoute's `db/` domain. The gate still blocks any NEW raw SQL against OmniRoute's DB.
|
||||
- **#3500 fully resolved** — Hard Rule #5 (no raw SQL in route handlers): all 13 internal offenders migrated to `src/lib/db/` modules across slices (call*logs, usage_history/daily_usage_summary, community_servers, usage_logs, semantic_cache, proxy_logs, skills UPDATE, db-backups). The gate's `KNOWN_RAW_SQL` set is renamed to `EXTERNAL_DB_ALLOWED` (with a back-compat alias) and now holds only the **2 external-DB reads** (`oauth/cursor/auto-import`, `oauth/kiro/auto-import`) — these open \_another app's* SQLite to import credentials, so by design they cannot live in OmniRoute's `db/` domain. The gate still blocks any NEW raw SQL against OmniRoute's DB.
|
||||
- **chore(db-gate):** reclassify `KNOWN_UNEXPORTED` → `INTENTIONALLY_INTERNAL` in `scripts/check/check-db-rules.mjs` ([#3499]): a full audit of all 25 db modules confirmed each is consumed via direct/dynamic import per Hard Rule #2 ("Never barrel-import from localDb.ts"). The old framing labelled them as "debt", which was misleading — they are the correct pattern. The gate's blocking behaviour is unchanged (a NEW unexported module still fails); only the name, comments, and per-module justifications were updated to reflect audited truth. Four modules flagged `DEAD?` (`compressionScheduler`, `discovery`, `pluginMetrics`, `prompts`) have zero production importers and are documented as schema-reserved. A new regression-guard test (`tests/unit/check-db-rules-classification.test.ts`) asserts every non-dead module in the set has ≥1 real importer, so a future consumer removal surfaces as a test failure requiring explicit reclassification.
|
||||
- **refactor(db): move `call_logs` aggregations into `callLogStats` db module** ([#3500]): extracted raw SQL from three route handlers (`/api/provider-metrics`, `/api/search/stats`, `/api/v1/search/analytics`) into a new `src/lib/db/callLogStats.ts` domain module (`getProviderMetrics`, `getSearchProviderStats`, `getRecentSearchLogs`, `getSearchAggregateStats`, `getSearchProviderCounts`). First slice of #3500 (call_logs cluster). Behavior unchanged; the three routes are removed from `KNOWN_RAW_SQL` in the gate. Validated with TDD unit tests (6 assertions seeding an in-memory SQLite fixture).
|
||||
- **refactor(db): move `usage_history`/`daily_usage_summary` SQL into `usageAnalytics` db module** ([#3500]): extracted all inline `db.prepare(...)` calls from two route handlers (`/api/usage/analytics`, `/api/settings/export-json`) into a new `src/lib/db/usageAnalytics.ts` module and extended `src/lib/db/callLogStats.ts` with `getFallbackStats`. New exports: `buildUnifiedSource`, `buildPresetUnifiedSource` (UNION CTE builders), plus 12 typed query functions covering summary, daily, daily-cost, heatmap, model, provider, account, api-key, service-tier, weekly-pattern, and preset-cost aggregations, plus `getAllUsageHistory`/`getAllDomainCostHistory`/`getAllDomainBudgets` for backup export. Second slice of #3500. `KNOWN_RAW_SQL` drops from 12 → 10. Validated with 21 TDD unit tests (`tests/unit/db-usage-analytics-3500.test.ts`) seeding a temp SQLite fixture.
|
||||
|
||||
- **refactor(db): move `community_servers` auth look-up into `gamification` db module** ([#3500]): extracted raw SQL from two federation route handlers (`/api/gamification/federation/leaderboard`, `/api/gamification/federation/score`) into a new `getConnectedServerByKeyHash(apiKeyHash)` function in `src/lib/db/gamification.ts`. Third slice of #3500 (gamification federation cluster). Behavior unchanged; the two routes are removed from `KNOWN_RAW_SQL` in the gate. Validated with TDD unit tests (3 assertions seeding a temp SQLite fixture).
|
||||
|
||||
- **refactor(db): move `skills UPDATE` + `db-backups` SQL into db modules** ([#3500]): fifth slice of #3500. Extracted the dynamic `UPDATE skills SET …` from `src/app/api/skills/[id]/route.ts` into a new `src/lib/db/skills.ts` module (`updateSkill(id, patch)`). The dynamic SET clause is injection-safe: column names are validated against a hard-coded allowlist of known writable columns before being interpolated; unknown keys are silently ignored. Extended `src/lib/db/backup.ts` with three new functions: `exportAllSummaryRows()` (multi-table SELECT for key_value / combos / provider_connections / api_keys, used by exportAll), `getTableNamesFromAdapter()` (sqlite_master introspection via an adapter arg, used by import validation), and `countImportedRows()` (post-import COUNT(\*) per table). The backup domain module is the correct home for sqlite_master introspection — it is not "raw SQL in a route" once moved there. `KNOWN_RAW_SQL` drops by 3 (from 8 → 5). Validated with 11 TDD unit tests (`tests/unit/db-backups-skills-3500.test.ts`).
|
||||
- **refactor(db): move `skills UPDATE` + `db-backups` SQL into db modules** ([#3500]): fifth slice of #3500. Extracted the dynamic `UPDATE skills SET …` from `src/app/api/skills/[id]/route.ts` into a new `src/lib/db/skills.ts` module (`updateSkill(id, patch)`). The dynamic SET clause is injection-safe: column names are validated against a hard-coded allowlist of known writable columns before being interpolated; unknown keys are silently ignored. Extended `src/lib/db/backup.ts` with three new functions: `exportAllSummaryRows()` (multi-table SELECT for key_value / combos / provider_connections / api_keys, used by exportAll), `getTableNamesFromAdapter()` (sqlite_master introspection via an adapter arg, used by import validation), and `countImportedRows()` (post-import COUNT(\*) per table). The backup domain module is the correct home for sqlite_master introspection — it is not "raw SQL in a route" once moved there. `KNOWN_RAW_SQL` drops by 3 (from 8 → 5). Validated with 11 TDD unit tests (`tests/unit/db-backups-skills-3500.test.ts`).
|
||||
- **refactor(db): move `usage_logs`/`semantic_cache`/`proxy_logs` SQL into db modules** ([#3500]): extracted raw `db.prepare(...)` SQL from three route handlers (`/api/analytics/auto-routing` → `usageLogs.ts`; `/api/cache/entries` → `semanticCache.ts`; `/api/logs/export` → `proxyLogs.ts`) into new `src/lib/db/` domain modules. New exports: `getAutoRoutingTotalCount`, `getAutoRoutingVariantBreakdown`, `getAutoRoutingTopProviders` (usage_logs), `listSemanticCacheEntries`, `deleteSemanticCacheBySignature`, `deleteSemanticCacheByModel` (semantic_cache), and `exportProxyLogsSince` (proxy_logs). Fourth slice of #3500. `KNOWN_RAW_SQL` drops from 8 → 5. Validated with 13 TDD unit tests (`tests/unit/db-logs-cache-3500.test.ts`) seeding temp SQLite fixtures.
|
||||
|
||||
- **Provider-detail god-component decomposition — Phase 0** ([#3501]): introduced `ProviderDetailPageClient.tsx` and reduced `providers/[id]/page.tsx` to a thin 9-line route wrapper (was 12,882 LOC), following the repo's `*PageClient` convention. Added the first-ever smoke render test for the page (Hard Rule #8) as the safety net every later extraction phase is diffed against. Behavior unchanged; the `check-file-size` ratchet now tracks the extracted client. Foundation for Phases 1–6 (strangler-fig). Thanks @oyi77 for the parallel modularization effort in #3627.
|
||||
- **Provider-detail god-component decomposition — Phase 1a** ([#3501]): extracted the three self-contained auth-import modal clusters (Codex/Claude/Gemini `Import*AuthModal` + `Apply*AuthModal` + their co-located helpers, ~2,160 LOC) into `providers/[id]/components/modals/`. `ProviderDetailPageClient.tsx` drops 12,882 → 10,719 LOC. Behavior unchanged (smoke test green; clusters had clean `{ onClose, onSuccess }` / inline-prop interfaces). Co-authored with @oyi77.
|
||||
- **Provider-detail god-component decomposition — Phase 1b** ([#3501]): extracted `EditCompatibleNodeModal` (+ its node/props types) into `providers/[id]/components/modals/`, and moved the shared `CC_COMPATIBLE_DEFAULT_CHAT_PATH` constant into a leaf `providerDetailConstants.ts` so the page client and the modal can both import it without a circular dependency. Also removed two dangling section comments left by Phase 1a. `ProviderDetailPageClient.tsx`: 10,719 → 10,435 LOC. Behavior unchanged (smoke test + a new standalone modal render test green; `check:cycles` clean). Co-authored with @oyi77.
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **Combos / Auto-Combo: premature context compaction ("agent keeps forgetting things")** ([#3680]): two related context-window bugs fixed. (1) `GET /api/combos/auto` now advertises `context_length` / `max_output_tokens` (MAX across the candidate pool — safe because the auto-combo context pre-filter routes oversized requests to large-window candidates), and the opencode plugin consumes them instead of hardcoding `limit: { context: 0 }` — a zero context silently disables opencode's smart auto-compaction, letting sessions grow until the gateway's destructive history purge kicks in. (2) chatCore's proactive compression for DB combos (incl. quota-shared pools) no longer compresses at `min(...allTargets)`: it now uses the EXECUTING target's own window (`resolveComboContextLimit`), keeping min-of-targets only as a defensive fallback when the current provider/model resolves no specific limit. TDD: 8 server tests (`tests/unit/auto-combo-context-advertising.test.ts`) + 3 plugin tests (`tests/auto-combo-context.test.ts`).
|
||||
- **Obsidian/WebDAV**: add the `/api/settings/obsidian/webdav` config route (enable/disable vault sync), encrypt WebDAV credentials at rest, and remove the duplicate UI block (#3485, part 1).
|
||||
|
||||
- **OpenCode Free / passthrough**: "Test all models" now respects "Auto-hide failed models" and switches the list to the visible filter so hidden models actually disappear (#3610). Three related bugs fixed: `autoHideFailed` is now threaded from the outer component into `PassthroughModelsSection` via a prop (single shared checkbox); the `/api/models/test-all` request body now includes `autoHideFailed: true` so the server persists the hide; and after the loop, `visibilityFilter` is switched to `"visible"` when ≥1 model was hidden. Two pure-function helpers (`buildPassthroughTestBody`, `shouldSwitchToVisibleFilter`) extracted to `providerPageHelpers.ts` with 7 unit tests.
|
||||
- **Resilience**: clear stale transient connection cooldowns on startup so a prior unclean crash no longer makes every request time out at 120s after restart (#3625)
|
||||
|
||||
- **fix(home topology): restore live in-flight request pulse** ([#3507]): the animated "pulse" edges in the home Provider Topology panel went dead after PR #3401 unified request visibility, because `activeRequests` was hardcoded to `[]`. Re-wired to `useLiveRequests()` (the existing WebSocket hook on port 20129) so that every pending/running request drives the animation in real time. A pure `selectActiveRequests` mapping helper was extracted to `home/topologyUtils.ts` with 5 unit tests.
|
||||
- **Electron desktop**: launch the peer-stamping `server-ws.mjs` entrypoint so local-only routes (AgentBridge, MCP, services) no longer return 403 LOCAL_ONLY (#3386)
|
||||
- **Provider Topology**: stop flagging healthy providers as errored based on stale historical failures; use current request status (#3619)
|
||||
- **OpenCode Free**: fetch the live model catalog from the provider's `modelsUrl` for the no-auth model picker instead of serving a stale hardcoded list (#3611)
|
||||
- **Hermes Agent**: honour the `HERMES_HOME` env var when writing/reading the agent config instead of always using `~/.hermes` (#3628). Introduced `getHermesHome()` / `getHermesConfigPath()` helpers (read at call-time) and routed all four hardcoded callsites through them so OmniRoute's config lands in the same directory that the Hermes PowerShell installer configures on Windows.
|
||||
- **MITM/cert**: remove the duplicated "Command failed:" prefix in system-command error messages ([#3641](https://github.com/diegosouzapw/OmniRoute/issues/3641)): `execFileText` was prepending its own `"Command failed: "` prefix on top of Node's `execFile` error message, which already begins with `"Command failed: <cmd>"` for non-zero exits. The error message now surfaces Node's message directly (no double prefix), with stderr appended only when non-empty.
|
||||
|
||||
- **fix(reasoning): replay `reasoning_content` on plain DeepSeek turns** ([#3632] — thanks @adivekar-utexas): the reasoning-replay gate previously only fired when an assistant message already carried `reasoning_content`. Plain (non-tool-call) turns whose `reasoning_content` was stripped by the client (e.g. Cursor) were forwarded without it, so DeepSeek V4+ rejected the request with 400 "the reasoning_content in the thinking mode must be passed back". The gate now also covers missing/empty `reasoning_content` on DeepSeek replay targets, injecting the cached reasoning (or the non-Anthropic placeholder) so multi-turn text conversations no longer 400. Fixes #1682. 2 regression tests.
|
||||
- **fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint** ([#3631] — thanks @artickc): Kiro/CodeWhisperer access tokens and Q Developer profile ARNs are region-bound, so enterprise IAM Identity Center accounts outside `us-east-1` (e.g. `eu-central-1`) were rejected by the default host. Adds `resolveKiroRegion` (stored region → profileArn region → `us-east-1`) and `kiroRuntimeHost` (regional `q.{region}.amazonaws.com`, legacy `codewhisperer.us-east-1` for the default), routes chat + usage to the regional endpoint, and discovers the region-matched `profileArn` via `ListAvailableProfiles` in a best-effort `postExchange` hook. 9 tests.
|
||||
- **fix(combo): skip same-provider/connection targets on connection-level errors** ([#3637] — thanks @herjarsa): on connection-level upstream errors (408/500/502/503/504/524), remaining same-`provider:connection` targets in a combo request are now skipped to avoid hammering a known-bad connection, in both the priority and round-robin paths. Adjusted in review to **exclude OmniRoute circuit-breaker-open responses** (503 + `X-OmniRoute-Provider-Breaker` / `provider_circuit_open`) from this skip, preserving the invariant that a breaker-open is an ordinary target failure (the next same-provider target is still tried). Co-authored with @herjarsa.
|
||||
- **/v1/responses**: detect stream readiness for tool-call-only and `object`-less chunks so Codex-shaped (reasoning + tools) requests no longer fail with "Stream ended before producing useful content" (#3612)
|
||||
- **RTL locales (ar/fa/he/ur)**: use logical CSS direction utilities for the sidebar and key overlays so the layout mirrors correctly under `dir=rtl` (#3541, partial — core layout)
|
||||
- **Kiro/AWS auto-import**: set a descriptive account name and dedupe by `profileArn` so imports no longer create nameless duplicate "OAuth Account" rows (#3615)
|
||||
- **fix(guardrails):** the `/api/guardrails/test` route now validates its body through `validateBody()` (Zod) instead of parsing raw JSON directly, aligning it with the repo-wide input-validation pattern (Hard Rule #7). ([#3621](https://github.com/diegosouzapw/OmniRoute/pull/3621) — thanks @diegosouzapw)
|
||||
- **fix(dashboard): bulk provider connection actions** — close audit, API, and UX gaps in the batch activate/deactivate flow: register the `provider.credentials.batch_updated` event in `HIGH_LEVEL_ACTIONS` and `ACTIVITY_ICONS` (was silently dropped from the Activity feed); fix `/api/providers` PATCH to return `warn` status when `notFound` is non-empty instead of always `success`; `/api/providers/test-batch` empty-result early-return now includes a `summary` so stale-ID mode reports to the user; bulk activate/deactivate chunks selection by 100 to avoid the Zod 400 cap on large provider accounts. ([#3673](https://github.com/diegosouzapw/OmniRoute/pull/3673) — thanks @leninejunior)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **fix(ci):** increase the `execFileSync` `maxBuffer` in `validate-pack-artifact` so the npm-pack inventory no longer overflows on large tarballs during release validation — follow-up to the v3.8.21 pack-artifact hotfix. ([#3622](https://github.com/diegosouzapw/OmniRoute/pull/3622) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.21] — 2026-06-11
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **feat(cli):** `omniroute autostart` now accepts the shorthand the headless / `omniroute serve` path was missing — `omniroute autostart on` / `... true` (aliases of `enable`), `... off` / `... false` (aliases of `disable`), a new `... toggle`, and a default `... status` (bare `omniroute autostart` is a safe read-only). Previously autostart could only be toggled from the tray (`serve --tray`) or the Electron Appearance tab, so a plain `omniroute serve` user had no way to enable it. (The cross-platform launchd/systemd/registry logic is unchanged — this only wires the ergonomic CLI surface.) ([#3331](https://github.com/diegosouzapw/OmniRoute/issues/3331) — thanks @uniQta)
|
||||
|
||||
### ♻️ Code Quality
|
||||
|
||||
- **refactor(chatCore):** extract the chatCore request phases — idempotency check, semantic cache check, common request sanitization, and memory/skills injection — into dedicated `open-sse/handlers/chatCore/` modules (`idempotency.ts`, `semanticCache.ts`, `sanitization.ts`, `memorySkillsInjection.ts`), slimming the monolithic handler with no behavior change. (Maintainer follow-up: re-derive `idempotencyKey` at the Phase 9.2 save site after the check moved into the module, fixing a `ReferenceError` on successful non-cached responses.) ([#3598](https://github.com/diegosouzapw/OmniRoute/pull/3598) — thanks @oyi77)
|
||||
- **docs(opencode-provider):** soft-deprecate `@omniroute/opencode-provider` in favour of `@omniroute/opencode-plugin`. The provider package writes a **static** model list to `opencode.json` that drifts behind the live OmniRoute catalog, whereas the plugin fetches `/v1/models` at OpenCode startup. The package keeps working (no code/behavior change), but its npm description and README now carry a deprecation banner with the one-line migration, and a guard test pins the notice. ([#3419](https://github.com/diegosouzapw/OmniRoute/issues/3419) — thanks @herjarsa)
|
||||
- **chore(review):** pre-release hardening from a multi-reviewer `/review-reviews` battery over the v3.8.21 diff (7 Opus reviewers; zero blocker/high). Resolved findings: npm tarball no longer ships co-located test files (`files[]` negations + reconciled `.npmignore`; the #3578 closure gate now asserts the real `npm pack` output in both directions); `getSanitizedCachedProviderLimitsMap` scopes its connection scan to antigravity/agy instead of decrypting every active connection on each dashboard poll; the Antigravity quota-tier remap (`toClientAntigravityQuotaModelId`) is centralized in `antigravityModelAliases.ts` (was an inline if-ladder in `usage.ts`); the chatCore idempotency check returns its resolved key so the save site reuses a single derivation; and new tests pin the chatCore extracted modules, the Antigravity `usage_history` fallback contract, the reasoning-wrapper prefix-preservation heuristic, the Antigravity SSE `markdown` branch, and the upstream-ca/test no-persist guarantee. (Live-verified that agy consumer tokens are accepted by the non-daily `cloudcode-pa` host used by `retrieveUserQuota`, so #3604 is not agy-host-limited.)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(routing):** reasoning models (deepseek-v4-flash, nemotron, etc.) no longer return empty content in combo routing when they spend all of `max_tokens` on reasoning — `validateResponseQuality` now rejects an empty-content-but-`reasoning_content` response when reasoning consumed ≥90% of completion tokens (so the combo loop retries/falls back), and reasoning models receive a `max_tokens` buffer (+50%, +1000 floor) so reasoning and content both fit. (Maintainer follow-up: the round-robin buffer is applied to a per-attempt copy so it does not compound across models/retries — `4096 → 6144 → 9216 → …`.) ([#3588](https://github.com/diegosouzapw/OmniRoute/pull/3588) — thanks @herjarsa)
|
||||
- **fix(routing):** a valid `max_tokens`-truncated upstream response is no longer misclassified as empty content and rewritten into a fake 502 — `isEmptyContentResponse()` flagged any Claude `content:[]` / OpenAI empty-choice payload regardless of `stop_reason`/`finish_reason`, so a Claude Code `max_tokens: 1` connectivity ping (HTTP 200, `stop_reason:"max_tokens"`, empty content) became a synthetic `502 "Provider returned empty content"` and triggered a needless family fallback. The guard now treats a terminal truncation/tool signal (Claude `stop_reason` `max_tokens`/`tool_use`, OpenAI `finish_reason` `length`/`tool_calls`) as a legitimate completion; genuinely empty responses (no terminal reason, or `stop`/`end_turn` with empty content) are still caught. ([#3572](https://github.com/diegosouzapw/OmniRoute/issues/3572))
|
||||
- **fix(api):** `/v1/completions` now returns the legacy OpenAI Completions shape (`object:"text_completion"`, `choices[].text`) instead of chat payloads (`choices[].message|delta.content`) — the endpoint routes internally through the chat pipeline, so legacy Completion clients like TabbyML's `openai/completion` backend crashed with `missing field "text"`. The response (both non-streaming JSON and the SSE stream) is now translated back to the text-completion shape; `[DONE]` and error bodies pass through unchanged. ([#3571](https://github.com/diegosouzapw/OmniRoute/issues/3571))
|
||||
- **fix(usage):** the z.ai/GLM coding-plan quota card no longer shows "Monthly 0%" — coding plans have no monthly cap (only 5-hour windows), so the quota API reports the `TIME_LIMIT` ("Monthly") entry with `total=0`, and the `total>0 ? … : 0` fallback rendered a misleading 0% remaining (which can skew downstream model-choice). With no absolute cap the remaining percentage now falls back to the percentage-derived value (full/100% when 0% used). ([#3580](https://github.com/diegosouzapw/OmniRoute/issues/3580))
|
||||
- **docs(discovery):** mark `DISCOVERY_TOOL_DESIGN.md`'s API Endpoints table with an explicit "⚠️ Not yet implemented — Phase 2" banner — the discovery routes are a design proposal (Phase-1 stub only), and the banner makes clear the `KNOWN_STALE_DOC_REFS` gate suppression is intentional, not stale drift. ([#3498](https://github.com/diegosouzapw/OmniRoute/issues/3498))
|
||||
- **fix(agent-bridge):** add the missing `POST /api/tools/agent-bridge/upstream-ca/test` route — the UpstreamCaField "Test" button POSTed to it but it didn't exist (404). The new validate-only route checks the CA file exists and is a parseable PEM certificate (returns the subject/expiry) **without** persisting the path or activating it; it inherits the `/api/tools/agent-bridge/` LOCAL_ONLY classification. ([#3488](https://github.com/diegosouzapw/OmniRoute/issues/3488))
|
||||
- **fix(gamification):** the dashboard Profile page no longer hits three 404s — added the missing `GET /api/gamification/{level,badges,badges/earned}` routes (management-scoped). The page is operator-wide (no `apiKeyId`), so `level`/`badges/earned` aggregate across all keys (with an optional `?apiKeyId` for a single key), and `badges` seeds the built-in catalog first (idempotent) so the grid is populated even on installs that never seeded it (see #3472). ([#3484](https://github.com/diegosouzapw/OmniRoute/issues/3484))
|
||||
- **security(oauth):** migrate the five public OAuth client_ids (Claude, Codex, Qwen, Kimi, GitHub Copilot — 9 server-side call-sites in `providerRegistry.ts` + `oauth.ts`) from string literals to `resolvePublicCred()` (Hard Rule #11), matching the existing Gemini/Antigravity pattern. The values decode byte-for-byte to the same public client_ids (env overrides still win), so OAuth flows are unchanged; the `check-public-creds` allowlist is now empty. The browser-bundled `codexDeviceFlow.ts` copy stays a literal by necessity (it cannot import `open-sse`). ([#3493](https://github.com/diegosouzapw/OmniRoute/issues/3493))
|
||||
- **fix(mcp):** `omniroute --mcp` no longer crashes on npm installs with `ERR_MODULE_NOT_FOUND` (e.g. `src/lib/combos/steps.ts`) — the MCP server runs from raw TypeScript and imports across `src/` + `open-sse/`, but the published `files` allowlist only shipped a handful of cherry-picked paths, so the transitive closure (~400 files) was absent from the tarball. `files` now ships the backend source the MCP server needs (`open-sse/` + `src/{domain,lib,mitm,server,shared,sse,types}/`, excluding the `src/app` UI), and a new regression test computes the MCP import closure and fails if any reachable source file is not covered by `files`. ([#3578](https://github.com/diegosouzapw/OmniRoute/issues/3578))
|
||||
- **fix(api):** `API_REFERENCE.md` no longer documents a non-existent `/api/guardrails*` / `/api/shadow*` surface (doc-fiction flagged by `check-docs-symbols`, frozen in `KNOWN_STALE_DOC_REFS`). The guardrail pipeline is real (`src/lib/guardrails`), so the two routes that map to actual behavior are now implemented — `GET /api/guardrails` (list the registered guardrails + status) and `POST /api/guardrails/test` (dry-run the pre-call pipeline over a sample input), both management-scoped — while the fictional `enable`/`disable`/`logs` rows and the entire `/api/shadow*` table (shadow A-B comparison is combo-config + `/api/combos/metrics`) were removed from the doc and dropped from the allowlist. ([#3496](https://github.com/diegosouzapw/OmniRoute/issues/3496))
|
||||
- **fix(agent-bridge):** the MITM "Start" button no longer reports a misleading "port 443 may be in use" for every failure cause — `startMitm()` only matched the EADDRINUSE stderr line and always threw the port-443 message, so a missing `ROUTER_API_KEY` or an `EACCES` permission error sent users debugging the wrong thing. The startup watcher now buffers the MITM child's stderr and `interpretMitmStartupError()` maps the real `server.cjs` `❌` cause (port-in-use / permission-denied / missing API key / any other diagnostic line) into the surfaced error; with no captured output it stays generic instead of guessing port 443. ([#3606](https://github.com/diegosouzapw/OmniRoute/issues/3606))
|
||||
- **fix(oauth):** Kiro "Import Token" no longer reports a bare `Internal server error` that hides the real cause — the import validates/refreshes the pasted refresh token against AWS, and the catch returned a generic 500 string, so an `invalid_grant`, an expired token, or a region mismatch all surfaced identically in the dashboard. The import error now carries the sanitized upstream cause via `sanitizeErrorMessage()` (Hard Rule #12 — no stack, no secrets), keeping the same `{ error: <string> }` response shape, and still falls back to the generic message when there is nothing to report. ([#3589](https://github.com/diegosouzapw/OmniRoute/issues/3589))
|
||||
- **fix(antigravity):** the Antigravity/agy Gemini 3.5 Flash catalog now exposes clean public tier IDs (`gemini-3.5-flash-low`/`-medium`/`-high`, matching Antigravity 2.0.4's Low/Medium/High selector) and maps them to the live upstream IDs at the executor boundary, instead of the old confusing `-preview`/`-agent` names. Antigravity model-id normalization moved out of the global model resolver into the executor so client-visible IDs are no longer rewritten before account/credential routing and logging. (Maintainer follow-up: kept `gemini-3.5-flash-preview` as a hidden backward-compat alias routing to the High tier so saved combos/configs keep working; live-validated the tier set via the `agy` CLI catalog.) ([#3603](https://github.com/diegosouzapw/OmniRoute/pull/3603) — thanks @dhaern)
|
||||
- **fix(usage):** Antigravity/agy Provider Limits now report accurate consumption — `retrieveUserQuota` (live usage) is preferred over the `fetchAvailableModels` catalog view (which keeps reporting full buckets after real usage), with a local `usage_history` fallback for buckets that are only catalog-visible; cached entries are sanitized so retired upstream IDs are not re-exposed, and a deduplicated post-usage refresh keeps the dashboard fresh after each request. (Maintainer follow-up: the post-usage refresh is decoupled through a lightweight `usageEvents` bus so `usageHistory` no longer imports `providerLimits`/the executors graph, keeping the `typecheck:core` surface stable.) ([#3604](https://github.com/diegosouzapw/OmniRoute/pull/3604) — thanks @dhaern)
|
||||
- **fix(gemini):** textual reasoning wrappers emitted as assistant prose (`<think>`/`<thinking>`/`<thought>`/`<internal_thought>`, including malformed/open tags like `<thought\n…` before a tool call) are now routed to `reasoning_content` instead of leaking into visible `content`, in both the non-streaming sanitizer and the Gemini streaming translator (with split-chunk buffering so a tag fragmented across SSE chunks stays hidden). Structured tool calls and the existing textual tool-call conversion are preserved. ([#3605](https://github.com/diegosouzapw/OmniRoute/pull/3605) — thanks @dhaern)
|
||||
- **fix(gemini):** a signed native `functionCall` arriving while a textual reasoning wrapper opened in an earlier streaming chunk is still buffered now flushes that buffered reasoning to `reasoning_content` before the tool call, instead of silently discarding it. (Pre-release `/review-reviews` finding.)
|
||||
- **fix(api):** `/v1/completions` now drops a stale upstream `content-length` on the SSE branch too (the JSON branch already did) — re-serialization changes the byte length, so a buffered SSE body could otherwise advertise the pre-rewrite length and truncate/hang the client. (Pre-release `/review-reviews` finding.)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.20] — 2026-06-10
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add Claude Fable 5 (`claude-fable-5`) — wires the new flagship model across the full pipeline: `cc` and `kiro` provider registries (1M context, 128k output), pricing at $15/$75 per 1M tokens, model spec (adaptive thinking, vision, tool use), fast mode, 1M-context beta header, fallback chain (`claude-fable-5 → claude-opus-4-8 → claude-opus-4-7 → claude-sonnet-4-6`), and cost data. ([#3524](https://github.com/diegosouzapw/OmniRoute/pull/3524) — thanks @ggiak)
|
||||
- **feat(resilience):** add global provider cooldown tracking to prevent combo re-walking — after a provider fails in a combo request, subsequent requests skip it for a configurable exponential backoff (default 5s min, 5min max, doubling per failure), reducing wasted time on known-failing providers. Configurable and opt-out via Settings → Resilience. ([#3556](https://github.com/diegosouzapw/OmniRoute/pull/3556) — thanks @pizzav-xyz)
|
||||
- **feat(resilience):** expose provider breaker degradation threshold setting — the consecutive-failure count before a provider enters the DEGRADED state is now configurable in Settings → Resilience alongside the existing open/half-open thresholds. ([#3535](https://github.com/diegosouzapw/OmniRoute/pull/3535) — thanks @rdself)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(translator):** scope the Gemini `thoughtSignature` bypass to the Antigravity/CLI path and unwrap array-shaped Gemini error bodies — signature-less historical tool calls on Antigravity/CLI are emitted as native parts carrying the `skip_thought_signature_validator` sentinel (preventing upstream 400s), while the standard Gemini direct path keeps its existing text/context representation untouched. ([#3560](https://github.com/diegosouzapw/OmniRoute/issues/3560) — thanks @oyi77 and @Six7Day via [#3414](https://github.com/diegosouzapw/OmniRoute/pull/3414))
|
||||
|
||||
- **fix(routing):** combo model substitution no longer forwards a client `thinking:{type:"disabled"}` to a target model that rejects it — when a combo/route swaps the upstream model (e.g. `claude-opus-4-8` → `claude-fable-5`), OmniRoute now strips the now-invalid `thinking.type:"disabled"` for models flagged `rejectsThinkingDisabled` (Fable 5 defaults to adaptive and rejects it), preventing the upstream 400 that silently broke Claude Code's internal title/name-generation calls. Models that accept `disabled` (opus/sonnet) are untouched. ([#3554](https://github.com/diegosouzapw/OmniRoute/issues/3554))
|
||||
- **fix(usage):** the budget dashboard can now save a budget with some limit fields left empty and clear all limits — `setBudgetSchema` used `.positive()` (rejecting the `0` the form sends for blank fields) plus a superRefine requiring at least one limit `> 0`, so saving with one field filled 400'd and clearing all limits was impossible. Limits now accept `0` (= "no limit for this period"; enforcement only kicks in above 0) and the cross-field minimum was removed; negatives are still rejected. ([#3537](https://github.com/diegosouzapw/OmniRoute/issues/3537))
|
||||
- **fix(gamification):** badge-unlock events no longer re-fire on every request — the "already unlocked?" guard used `getBadges()`, which INNER-JOINs `badge_definitions` (empty until seeded), so it always reported "not earned" and re-emitted `events.badge_unlocked` per request. Added a `hasBadge()` helper that reads `user_badges` directly, so dedup is correct regardless of whether definitions are seeded. ([#3472](https://github.com/diegosouzapw/OmniRoute/issues/3472))
|
||||
- **fix(routing):** the `auto` model keyword now works on the Codex `/v1/responses` path — `resolveResponsesApiModel` rewrote the bare `auto` keyword to `codex/auto`, which ChatGPT rejects (`The 'auto' model is not supported when using Codex with a ChatGPT account`). `auto` (OmniRoute's zero-config auto-routing keyword) now passes through untouched so combo routing handles it. ([#3509](https://github.com/diegosouzapw/OmniRoute/issues/3509))
|
||||
- **fix(cli-tools):** saving the OpenCode/CLI tool config no longer 400s in cloud mode — every CLI tool card posts `apiKey: null` (the real key is resolved server-side from `keyId`), but `guideSettingsSaveSchema` used `z.string().optional()`, which rejects `null`. The schema now normalizes `null` → `undefined`, so the save succeeds and the `keyId`/default path is used. ([#3552](https://github.com/diegosouzapw/OmniRoute/issues/3552))
|
||||
- **fix(catalog):** PublicAI is no longer miscatalogued as keyless/free — it requires an API key (registry `authType:"apikey"`; signup grants a one-time credit, then it bills). The three PublicAI models moved from `freeType:"keyless"` (which could pick them into the no-auth pool and dispatch with no `Authorization` header) to `"one-time-initial"`, and the provider's `hasFree` flag is now `false` — matching `freeTierCatalog.ts`, which already excluded publicai. ([#3558](https://github.com/diegosouzapw/OmniRoute/issues/3558))
|
||||
- **fix(gemini-web):** a missing Playwright Chromium browser no longer loops and trips the provider breaker — when the browser binary is not installed, `chromium.launch()` threw an error surfaced as a retryable **500**, so accountFallback marked the account unavailable and retry-looped. It is now classified as a host/config problem and returns **503** with an actionable message (`npx playwright install chromium`) and the `X-Omni-Fallback-Hint: connection_cooldown` header, which skips the provider circuit breaker and applies a short non-exponential cooldown. ([#3516](https://github.com/diegosouzapw/OmniRoute/issues/3516))
|
||||
- **fix(proxy):** the SOCKS5 proxy option now follows the runtime `ENABLE_SOCKS5_PROXY` env instead of the build-time `NEXT_PUBLIC_ENABLE_SOCKS5_PROXY` — Next.js inlines `NEXT_PUBLIC_*` at build time, so a prebuilt Docker image ignored a runtime setting and the SOCKS5 type stayed hidden. The proxy modal now reads `socks5Enabled` from `GET /api/settings/proxies` (server-side `ENABLE_SOCKS5_PROXY`), with the build-time value kept only as a static-deploy fallback. ([#3508](https://github.com/diegosouzapw/OmniRoute/issues/3508))
|
||||
- **fix(playground):** the playground model selector now lists models from custom-endpoint (OpenAI/Anthropic-compatible) providers — it filtered `/v1/models` by the provider's connection id, but the catalog emits compatible-provider models under the node's custom prefix (`prefix/model`), so the list came up empty ("None"/"-"). The selector now filters by the node prefix (exposed additively as `modelPrefix` on provider options; the connection id is unchanged, so translator send/translate and connection lookups are unaffected). ([#3505](https://github.com/diegosouzapw/OmniRoute/issues/3505))
|
||||
- **fix(usage):** the Kiro quota card no longer renders a blank when the account returns no usage breakdown — `getKiroUsage` returned `quotas:{}` for a successful GetUsageLimits response without a `usageBreakdownList` (observed with some AWS IAM / Builder ID accounts), which the dashboard showed as an unexplained empty card. It now returns an informative message (surfaced via the card's connection-message path). ([#3506](https://github.com/diegosouzapw/OmniRoute/issues/3506))
|
||||
- **fix(security):** route raw `err.message` through `sanitizeErrorMessage()` in five web executors (`adapta-web`, `deepseek-web`, `perplexity-web`, `qoder`, `veoaifree-web`) and the embeddings + search handlers (Hard Rule #12) — these built error response bodies from the raw upstream/exception message, which could leak internal detail. ([#3494](https://github.com/diegosouzapw/OmniRoute/issues/3494), [#3495](https://github.com/diegosouzapw/OmniRoute/issues/3495))
|
||||
- **fix(dashboard):** correct two dashboard fetches that hit non-existent routes (404) — `CustomHostsManager` called `/api/tools/traffic-inspector/custom-hosts` (the real route is `/hosts`), and `FeatureFlagsGrid`'s post-restart liveness probe called `/api/health` (the real lightweight endpoint is `/api/health/ping`). ([#3486](https://github.com/diegosouzapw/OmniRoute/issues/3486), [#3487](https://github.com/diegosouzapw/OmniRoute/issues/3487))
|
||||
- **chore(providers):** remove the dead `krutrim` registry entry — it was half-registered (present in `providerRegistry.ts` with a baseUrl + one model, but absent from `providers.ts`, with no executor/translator/OAuth), so it was never selectable. Dropped its `ProviderIcon` entry and the `KNOWN_REGISTRY_ONLY` exception. ([#3483](https://github.com/diegosouzapw/OmniRoute/issues/3483))
|
||||
- **docs(api):** fix the agent-bridge per-agent state route in `openapi.yaml` and `AGENTBRIDGE.md` — both documented `/api/tools/agent-bridge/agents/{id}/state`, which has no route; corrected to the real per-agent `/api/tools/agent-bridge/agents/{id}` (global state remains `/api/tools/agent-bridge/state`). ([#3489](https://github.com/diegosouzapw/OmniRoute/issues/3489))
|
||||
- **docs(api):** correct `API_REFERENCE.md` endpoints that documented non-existent routes — skills (`PUT /api/skills/[id]`, `POST`/`GET /api/skills/executions`), plugins (`[id]`→`[name]`, `activate`/`deactivate`), ACP (`DELETE`/`POST /api/acp/agents` via `?id`/`{action:"refresh"}`), cache (`DELETE /api/cache/reasoning`, `/api/cache/entries`), and removed the fabricated `/api/admin/circuit-breaker`, `/api/admin/rate-limits`, and `/api/system-info` (admin only exposes `/concurrency`). ([#3497](https://github.com/diegosouzapw/OmniRoute/issues/3497))
|
||||
- **fix(executor):** strip provider prefix from versioned built-in tool model field — Anthropic rejects `tools[N].model: "cc/claude-opus-4-8"` from Claude Code's `advisor_20260301` and similar versioned built-in tools; the native Claude OAuth execute path now strips any provider prefix from `model` on tools whose name matches `name_YYYYMMDD`. ([#3532](https://github.com/diegosouzapw/OmniRoute/pull/3532) — thanks @ggiak)
|
||||
- **fix(dashboard):** handle DEGRADED and unknown provider breaker states on the Runtime page — an unrecognised breaker state (e.g. DEGRADED) caused a crash because the styling map had no entry for it; now falls back to a neutral style so the page never throws on unknown states. ([#3533](https://github.com/diegosouzapw/OmniRoute/pull/3533) — thanks @rdself)
|
||||
- **fix(usage):** make opencode-go quota fetcher fail-open instead of throwing 500 — the quota API rejects chat API keys with a JSON-401 body even though the same key works for chat; previously this threw and crashed the dashboard with a red error banner. It now returns an informative message and keeps rendering like other connection-message cases. ([#3522](https://github.com/diegosouzapw/OmniRoute/pull/3522) — thanks @wilsonicdev)
|
||||
- **fix(translator):** map the Codex `local_shell` tool type — `local_shell` was absent from the translator's tool-type map, causing it to fall through as an unknown type; it is now forwarded correctly to the upstream. ([#3534](https://github.com/diegosouzapw/OmniRoute/pull/3534) — thanks @kamaka)
|
||||
- **fix(images):** prefer bare combo names over built-in image model aliases — a user combo named `gpt-image-2` can now shadow the native OpenAI alias so image requests route through the combo; provider-qualified IDs like `openai/gpt-image-2` still resolve via the built-in path. ([#3527](https://github.com/diegosouzapw/OmniRoute/pull/3527) — thanks @AveryanAlex)
|
||||
- **fix(translator):** fix OpenAI→Gemini translation of historical tool calls — tool results from earlier turns were being converted to text, causing Gemini to pattern-match the response as prose rather than structured content; they now use the native Gemini `functionResponse` part format. ([#3569](https://github.com/diegosouzapw/OmniRoute/pull/3569) — thanks @hartmark)
|
||||
- **fix(plugins):** forward plugin lifecycle hooks (`onInstall`, `onActivate`, `onDeactivate`, `onUninstall`) via IPC and wrap `onDeactivate`/`onUninstall` in try/catch so a buggy plugin handler can no longer brick teardown; also removes redundant `RegExp()` wrappers in `accountFallback.ts` and fixes indentation in `requestLogger.ts`. ([#3562](https://github.com/diegosouzapw/OmniRoute/pull/3562) — thanks @oyi77)
|
||||
- **fix(auto-update):** use a stable PROJECT_ROOT walker instead of frozen `process.cwd()` — `resolveProjectRoot` now walks up from `__dirname` to find the nearest directory containing `package.json` or `.git` (bounded at 16 levels), preventing ENOENT errors when the working directory is not the project root. ([#3561](https://github.com/diegosouzapw/OmniRoute/pull/3561) — thanks @oyi77 / @ViFigueiredo via [#3423](https://github.com/diegosouzapw/OmniRoute/pull/3423))
|
||||
- **fix(resilience):** expose `providerCooldown` in `GET /api/resilience` and accept it in `PATCH` — PR #3556 added the global provider cooldown tracker to the settings model and `ResilienceTab` UI, but the API route never returned the field (causing a crash when the tab loaded) and `updateResilienceSchema` (`.strict()`) rejected PATCH bodies containing it with 400. `providerCooldownSettingsSchema` is now wired into the Zod schema, returned in the GET response, and merged in the PATCH handler. Caught during v3.8.20 VPS homologation (Hard Rule #18 — 5 TDD tests); shipped to main via [#3591](https://github.com/diegosouzapw/OmniRoute/pull/3591). ([#3590](https://github.com/diegosouzapw/OmniRoute/pull/3590) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.19] — 2026-06-09
|
||||
|
||||
> Focused quality-infrastructure release: the complete **quality-gate ratchet + anti-hallucination guardrail system** (Phases 0–6 + fast-tracked 6A.1/6A.2). No external PRs were taken this cycle by design — community PRs carry over to the next cycle.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(quality):** quality-gate ratchet + anti-hallucination/rule-enforcement guardrails (Phases 0–6) — generic multi-metric ratchet engine (`quality-baseline.json` + collector + comparator, regression-only) and ~18 deterministic gates wired into CI: provider-consistency, dashboard `fetch()`→route and OpenAPI/docs→route resolution (anti-hallucination), dependency allowlist (anti-slopsquatting), file-size/duplication/complexity ratchets (frozen debt only shrinks), anti test-masking (assert-removal/tautology detection on PR diffs), error-helper (Hard Rule #12), public-creds (Rule #11), route-guard membership (Rules #15/#17), db-rules (Rules #2/#5), known-symbols (executors/strategies/translators), migration numbering. Re-enabled the cheap pre-commit hook, tiered `npm audit`, reconciled the CI coverage gate (40→60) and wired 3 orphaned contract gates. ([#3471](https://github.com/diegosouzapw/OmniRoute/pull/3471) — thanks @diegosouzapw)
|
||||
- **feat(quality):** test-discovery gate + 135 orphan tests re-wired + vitest in CI (fast-tracked Phase 6A.1/6A.2) — new `check:test-discovery` proves every `*.test.ts|tsx` is collected by a runner that actually executes (15 collectors with textual drift-check; orphans frozen in a shrink-only baseline). Found **195 orphan test files** (incl. `authz/routeGuard.test.ts` guarding Rules #15/#17 — already rotten); 135 re-wired into the node runner via explicit-braces recursive globs across all scripts + 4 CI call sites; the remaining 60 are categorized debt. New `test-vitest` CI job: `test:vitest` blocking (146/146), `test:vitest:ui` informational (14 pre-existing UI-drift fails, triage 2026-06-16). ([#3536](https://github.com/diegosouzapw/OmniRoute/pull/3536) — thanks @diegosouzapw)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(authz):** restored the missing `BYPASS_PREFIX_NOT_ALLOWED` schema guard (Hard Rules #15/#17) — the zod refine documented as layer-1 in `routeGuard.ts` was absent from the live `settingsSchemas.ts`, so `PATCH /api/settings` accepted spawn-capable prefixes (e.g. `/api/cli-tools/runtime/`) into the manage-scope bypass list (the layer-2 runtime predicate still refused to honour them). Surfaced by re-wired orphan tests AC-8/AC-10c, which now stand as the permanent regression guard. ([#3536](https://github.com/diegosouzapw/OmniRoute/pull/3536) — thanks @diegosouzapw)
|
||||
- **fix(db):** `closeDbInstance()`/`resetDbInstance()` now fire the `stateReset.ts` module-state resetters (previously only backup-restore did) — `apiKeys.ts` kept a process-level schema memo across a recreated DB, so the stale re-prepare exploded with `no such column: is_active` and clients received **503 instead of 403** for an invalid bearer; the same path hit production when restoring an older backup snapshot. Includes a dedicated regression test; a test that had accommodated the buggy 503 now asserts the deterministic 403. ([#3536](https://github.com/diegosouzapw/OmniRoute/pull/3536) — thanks @diegosouzapw)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **fix(security):** block the cloud-metadata SSRF pivot in the cli-tools catalog fetch (CodeQL `js/request-forgery`, **critical**) — `fetchOmniRouteCatalog()` built its `/v1/models` URL from a user-controlled `baseUrl` and fetched it. Since the legitimate target is the user's own OmniRoute (loopback), the public-only guard can't apply; `assertSafeCatalogUrl()` now blocks the cloud-metadata/link-local pivot (`169.254.169.254`, `metadata.google.internal`, …) unconditionally, plus non-http(s) protocols and embedded credentials, and the request fetches the re-parsed (taint-severed) URL. Loopback and public OmniRoute Cloud targets stay allowed. ([#3544](https://github.com/diegosouzapw/OmniRoute/pull/3544) — thanks @diegosouzapw)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **docs(quality):** Phase 6A critical-audit plan + Phase 7 community-tooling additions, both stored with an activation gate of **2026-06-16** — 6A: stale-allowlist enforcement, ratchet `--require-tighten`, gate scope expansions, remaining orphan/UI-suite triage; Phase 7 additions: gitleaks (Betterleaks noted), actionlint + zizmor, SPDX license compliance. ([#3530](https://github.com/diegosouzapw/OmniRoute/pull/3530) — thanks @diegosouzapw)
|
||||
- **chore(quality):** conscious, documented re-baselines so the quality-gate debuts holding the REAL published line — file-size frozen at current sizes for 9 files that grew in the v3.8.18 era (RequestLoggerV2 +281, stream +101, combo +73, chatCore +45, …) and `eslintWarnings` 3482→3501 (the published v3.8.18 tag already measured 3501; this cycle is neutral). Driving both down is Phase 6A work. ([#3538](https://github.com/diegosouzapw/OmniRoute/pull/3538) — thanks @diegosouzapw)
|
||||
- **chore(release):** open the v3.8.19 development cycle (version bump + electron lockfile sync) and ignore generated yt-downloader artifacts. (thanks @diegosouzapw)
|
||||
- **test:** release-gate stabilization — the re-wired suites + the debuting CI gates surfaced and fixed 6 latent test defects: 2 suites depended on the dev machine's configured password (now hermetic), the breaker reset-timeout test ran on a 5ms margin, the bypass-prefix schema test consecrated the pre-#3536 bug, the chatcore upstream-timeout test had a structurally-broken pending-detail predicate (tested `.providerRequest` on an array — never passed isolated, even at the published v3.8.18 tag), and internal planning docs were excluded from the docs-symbols gate. Coverage floors re-baselined to the honest post-re-wire denominator (78.4% measured: previously-never-imported modules now count). (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.18] — 2026-06-09
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(ui):** unified Active + Finished requests into a single view — the dashboard now shows in-flight and completed requests in one list with deep-linking, live streaming detail, and a dedicated `/api/logs/[id]` detail route; pending requests are tracked per connection and finalized as they complete. ([#3401](https://github.com/diegosouzapw/OmniRoute/pull/3401) — thanks @hartmark / @diegosouzapw)
|
||||
- **feat(plugins):** plugin lifecycle hooks + theme-manager example — adds `onInstall`/`onActivate`/`onDeactivate`/`onUninstall` lifecycle events dispatched by the plugin manager, thins `index.ts` to a backward-compatible re-export shim over `hooks.ts`, and ships theme-manager + request-logger example plugins. ([#3473](https://github.com/diegosouzapw/OmniRoute/pull/3473) — thanks @oyi77 / @diegosouzapw)
|
||||
- **feat(browserPool):** Playwright proxy resolved from the proxy registry — browser-backed providers (claude-web/gemini-web) now route through the configured per-provider/global proxy instead of connecting directly, matching how OAuth/token-refresh already honor `resolveProxyForProvider` (closes the VPS IP-rate-limit gap for the browser path). Fully additive with graceful degradation. ([#3492](https://github.com/diegosouzapw/OmniRoute/pull/3492) — thanks @borodulin)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(executor):** Llama / OpenAI-compat base URL normalization — a `baseURL` without a path (e.g. `llama.example.foo`) or with a non-`/v1` path (e.g. `bar.example.com/foo`) now correctly gets `/v1/chat/completions` appended, fixing the 404 on message sends while `GET /model` still worked. ([#3519](https://github.com/diegosouzapw/OmniRoute/pull/3519) — thanks @hartmark)
|
||||
- **fix(sse):** empty-choices chunks without usage are dropped instead of injecting retry text — a streamed chunk carrying an empty `choices` array and no `usage` is now silently skipped rather than emitting placeholder retry text into the stream, eliminating spurious content for clients that send such keepalive-style frames. ([#3513](https://github.com/diegosouzapw/OmniRoute/pull/3513) — thanks @diegosouzapw)
|
||||
- **fix(types):** restored a clean `typecheck:core` — typed `getPendingRequests()` to its real shape (`Record<string, Record<string, number>>`) so the unified-requests view (#3401) no longer treats pending counts as `unknown`, cast the `streamChunks` log payload to its declared type, and aligned `preScreenTargets` (#3169) to the canonical `IsModelAvailable` signature (sync-or-async, normalized via `Promise.resolve`). (thanks @diegosouzapw)
|
||||
- **fix(opencode-plugin):** repaired the corrupted `index.ts` that broke the npm `publish-opencode-plugin` build (introduced by the #3435 branch) — removed two duplicated code blocks (apiFormat + debug-logging), dropped the local `normaliseFreeLabel` superseded by the `naming.ts` extraction, fixed an undefined `sdkBaseURL` reference, declared the missing `startupDebug` / `logLevel` feature-schema fields, and fixed `shortProviderLabel` dropping the prefix on a long displayName with no alias. Plugin now builds (DTS clean) with all 254 tests green. ([#3435](https://github.com/diegosouzapw/OmniRoute/pull/3435) — thanks @diegosouzapw)
|
||||
- **fix(catalog):** Codex CLI model-catalog refresh no longer errors — `GET /v1/models` now returns a top-level `models: []` array for Codex clients (detected via the `originator` / `user-agent` = `codex_*` headers it sends on `GET /v1/models?client_version=...`), so `codex_models_manager` stops failing to decode the OpenAI-standard response and no longer logs `failed to refresh available models` on every startup. 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 break Codex's agent behaviour — an empty list keeps Codex on its built-in model info (same inference as before, minus the error). Non-Codex OpenAI clients receive the unchanged `{object,data}` response. ([#3481](https://github.com/diegosouzapw/OmniRoute/pull/3481) — thanks @diegosouzapw)
|
||||
- **fix(provider):** Cursor's Responses-API-shaped bodies on `/chat/completions` are detected and handled — a body with `input` but no `messages` is now classified as `openai-responses` (instead of forcing `openai` and building from undefined `messages` → upstream 400); standard OpenAI clients are unaffected by the `messages===undefined` guard. ([#3490](https://github.com/diegosouzapw/OmniRoute/pull/3490) — thanks @borodulin)
|
||||
- **fix(sse):** numeric provider IDs normalized to strings across 4 more surfaces — extends #3427 to the Responses-API SSE passthrough (`response_id`/`item_id`/`call_id`), the buffered/flush path in `stream.ts`, the dedup-key builders, and `sseParser.ts`, preventing `undefined` lookups when IDs arrive as numbers. ([#3451](https://github.com/diegosouzapw/OmniRoute/pull/3451) — thanks @disafronov)
|
||||
- **fix(theoldllm):** `X-Request-Token` generated server-side, dropping the Playwright dependency — replicates the site's client `rie()` token (djb2 hash + `oldllm-client-2026` seed + UA prefix + 8-hex `crypto.randomUUID` suffix) directly, so The Old LLM no longer needs a headless browser to mint tokens. ([#3491](https://github.com/diegosouzapw/OmniRoute/pull/3491) — thanks @borodulin / @diegosouzapw)
|
||||
- **fix(combo):** parallel pre-screen + circuit-breaker fast-exit for priority combos — provider profiles and model availability for all targets are pre-screened concurrently (max 5), and targets whose circuit breaker is OPEN are skipped immediately, reducing first-token latency on multi-target priority combos. ([#3169](https://github.com/diegosouzapw/OmniRoute/pull/3169) — thanks @pizzav-xyz)
|
||||
- **fix(authz):** URL-tokenized client endpoints (`/api/v1/vscode/<key>/...`) authenticate again when the caller sends its own non-OmniRoute `Authorization` header — a non-`Bearer <token>` header (e.g. VS Code Copilot's own, or an empty `Bearer `) no longer short-circuits auth; it falls through to the path-scoped URL token (still validated downstream), instead of 401'ing under `REQUIRE_API_KEY=true`. ([#3504](https://github.com/diegosouzapw/OmniRoute/pull/3504) — thanks @zhiru / @diegosouzapw)
|
||||
- **fix(playground):** the dashboard provider Test playground works under `REQUIRE_API_KEY=true` — it previously sent the **masked** key (`sk-xxxx****yyyy`) as a bearer (always invalid → 401). It now authenticates via the dashboard session and sends only the key **id** (`x-omniroute-playground-key-id`); the gateway resolves the secret server-side, honored **only** for an authenticated session and never putting the key secret on the wire. ([#3503](https://github.com/diegosouzapw/OmniRoute/pull/3503) — thanks @zhiru / @diegosouzapw)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **feat(docs):** doc-accuracy gate — new `npm run check:fabricated-docs` (`scripts/check/check-fabricated-docs.mjs`) indexes the codebase (api routes, env vars, CLI commands) and flags API-path/env-var/CLI/hook/file-ref claims in `docs/**` + `AGENTS.md` that don't exist in source (soft-fail by default, `--strict` for CI; wired into `check:docs-all`). Also refreshes the AGENTS.md live counts against source. ([#3510](https://github.com/diegosouzapw/OmniRoute/pull/3510) — thanks @oyi77)
|
||||
- **chore:** ignore local quality reports and prompt artifacts (`quality-metrics.json`, `PLANO-/RELATORIO-QUALITY-GATES.md`, stray prompt `.txt` files) so they no longer surface in `git status`. (thanks @diegosouzapw)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **fix(opencode-plugin):** bounded the regex quantifiers in `normaliseFreeLabel` to close a polynomial-ReDoS (CodeQL `js/polynomial-redos`) — an unbounded `\s*` before an anchored `\s*$` allowed O(n²) backtracking on attacker-influenced provider/model display names; bounded to `{0,8}`/`{1,8}`. (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.17] — 2026-06-09
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** LMArena provider — routes requests to the LMArena battle platform via the new `lmarena` executor; supports streaming chat completions. ([#3421](https://github.com/diegosouzapw/OmniRoute/pull/3421) — thanks @oyi77)
|
||||
- **feat(providers):** ZenMux provider — adds the `zenmux` executor for ZenMux's OpenAI-compatible endpoint with streaming support. ([#3429](https://github.com/diegosouzapw/OmniRoute/pull/3429) — thanks @oyi77)
|
||||
- **feat(providers):** Gemini Business provider — adds the `gemini-business` executor (Phase 2C of the Google provider expansion), enabling Gemini models via Google Workspace accounts. ([#3436](https://github.com/diegosouzapw/OmniRoute/pull/3436) — thanks @oyi77)
|
||||
- **feat(plugin+api):** auto-combos API + free model quota display — new `GET /api/combos/auto` endpoint lists dynamically scored combos; provider pages now surface free-tier quotas inline; MCP-plugin surface extended to match. ([#3435](https://github.com/diegosouzapw/OmniRoute/pull/3435) — thanks @mrmm)
|
||||
- **feat(opencode-plugin):** per-prefix API format selection, debug logging, and free-label normaliser — three backports from the mrmm fork: each route prefix can specify its own wire format (OpenAI / Anthropic / Gemini), structured debug output is toggled via env var, and free-tier labels are normalized across providers. ([#3420](https://github.com/diegosouzapw/OmniRoute/pull/3420) — thanks @herjarsa)
|
||||
- **feat(connections):** connection pagination, health filter, batch-delete confirmation, and custom banned keywords — the provider connections table is now paginated; a health-state filter lets operators show only healthy/degraded/failed connections; multi-select + confirm dialog for bulk deletes; per-connection keyword denylist for content safety. ([#3454](https://github.com/diegosouzapw/OmniRoute/pull/3454) — thanks @sdfsdfw2)
|
||||
- **feat(settings):** Endpoint Token Saver visibility toggle — operators can now show or hide the Token Saver widget on the endpoint page from Settings → Appearance. ([#3461](https://github.com/diegosouzapw/OmniRoute/pull/3461) — thanks @rdself)
|
||||
- **feat(catalog):** model catalog name feature flag — a new feature flag controls whether the catalog exposes provider-prefixed model names, letting deployments opt into the legacy bare-name format for downstream tooling compatibility. ([#3464](https://github.com/diegosouzapw/OmniRoute/pull/3464) — thanks @rdself)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(translator):** Vertex AI tool calls no longer fail with `400 Unknown name "id"` — the OpenAI-style `id` field is stripped from `functionCall`/`functionResponse` parts for `vertex`/`vertex-partner`; the public Gemini API still receives `id` as required for Gemini 3+ signature matching. ([#3457](https://github.com/diegosouzapw/OmniRoute/pull/3457) — thanks @nullbytef0x / @diegosouzapw)
|
||||
- **fix(claude):** Claude Code `claude-opus-4-8` tool calls no longer break with `tool call could not be parsed` — OmniRoute no longer force-injects `interleaved-thinking` / `advanced-tool-use` / `effort` beta flags the client never negotiated; clients sending their own `anthropic-beta` header control those betas themselves. ([#3458](https://github.com/diegosouzapw/OmniRoute/pull/3458) — thanks @Forcerecon / @diegosouzapw)
|
||||
- **fix(catalog):** imported/custom models on no-auth providers (e.g. The Old LLM) now appear in `GET /api/v1/models` and the Playground model selector — the eligibility gate required a DB connection row which no-auth providers never have, silently dropping every imported model for them. ([#3463](https://github.com/diegosouzapw/OmniRoute/pull/3463) — thanks @tjengbudi / @diegosouzapw)
|
||||
- **fix(browser):** optional `cloakbrowser` import no longer causes bundle errors when the package is absent — the import is now wrapped in a dynamic require so the build succeeds on environments that don't install the optional dep. ([#3460](https://github.com/diegosouzapw/OmniRoute/pull/3460) — thanks @rdself)
|
||||
- **fix(claude-web):** claude-web session handling cleanup — corrects an edge case where session cookies were not properly refreshed after a Turnstile challenge, and removes stale wrapper code left over from the provider split. ([#3449](https://github.com/diegosouzapw/OmniRoute/pull/3449) — thanks @androw)
|
||||
- **fix(analytics):** SQL named params are now scoped per query context — a shared params object was being mutated across concurrent analytics queries, causing `SQLITE_MISUSE: named parameter not found` errors under load. ([#3447](https://github.com/diegosouzapw/OmniRoute/pull/3447) — thanks @ReqX)
|
||||
- **fix(command-code):** chat endpoint reverted to `/alpha/generate` and model-sync discovery fixed — a prior refactor incorrectly targeted the wrong path, causing Command Code completions to silently 404; model listing now also resolves from the correct discovery endpoint. ([#3432](https://github.com/diegosouzapw/OmniRoute/pull/3432) — thanks @TapZe)
|
||||
- **fix(command-code):** CLI version header aligned to current Command Code release — the `X-Command-Code-Version` header value was pinned to a stale version string, causing upstream version-gated features to be rejected. ([#3462](https://github.com/diegosouzapw/OmniRoute/pull/3462) — thanks @hevener10)
|
||||
- **fix(sse):** provider IDs are normalized to strings before lookup — numeric provider IDs (e.g. from legacy DB rows) caused `undefined` lookups in the executor registry; all IDs are now coerced to string at the SSE entry point. ([#3427](https://github.com/diegosouzapw/OmniRoute/pull/3427) — thanks @disafronov)
|
||||
- **fix(stream):** textual tool-call slicing index mismatch resolved and `containsTextualToolCallMarker` deduplicated — two related bugs in the rolling-buffer parser caused partial tool-call chunks to be emitted twice or sliced from the wrong offset, producing garbled JSON in streamed tool responses. ([#3413](https://github.com/diegosouzapw/OmniRoute/pull/3413) — thanks @Ardem2025)
|
||||
- **fix(stream):** OpenAI usage-only chunks (empty `choices: []`) are now passed through instead of being dropped — some providers emit a trailing stats-only chunk after the last content delta; discarding it caused usage counters to be missing in logged responses. ([#3422](https://github.com/diegosouzapw/OmniRoute/pull/3422) — thanks @xz-dev)
|
||||
- **fix(translator):** empty-string `reasoning_content` replaced with placeholder on cache miss — `injectEmptyReasoningContentForToolCalls` pre-sets `reasoning_content=""` before the cache lookup; the old guard checked for `undefined`, never firing on miss and leaving `""` in place, which DeepSeek V4+ rejects with a 400. ([#3433](https://github.com/diegosouzapw/OmniRoute/pull/3433) — thanks @ViFigueiredo)
|
||||
- **fix(catalog):** combos auto-compute `context_length` for any provider-ID form — the context-length resolution only matched exact-string provider IDs, missing combos declared with a numeric or aliased ID; the lookup now normalizes before matching. ([#3417](https://github.com/diegosouzapw/OmniRoute/pull/3417) — thanks @herjarsa)
|
||||
- **fix(healthcheck):** container bridge network IP probed correctly — the healthcheck script was hard-coded to `localhost` which resolves to IPv6 `::1` inside some container runtimes; it now queries the bridge gateway IP so the probe succeeds on both bridge and host networking modes. ([#3434](https://github.com/diegosouzapw/OmniRoute/pull/3434) — thanks @naimo84)
|
||||
- **fix(publish):** onnxruntime CUDA binary removed from npm tarball — the native `.node` binary exceeded npm's 413 payload limit and was never needed at runtime (OmniRoute uses the CPU build); the pack policy now excludes the CUDA artifact. ([#3437](https://github.com/diegosouzapw/OmniRoute/pull/3437) — thanks @herjarsa)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **docs:** critical documentation gaps closed — new guides for ACP protocol, router strategies, compression, REST API reference, and updated AUTO-COMBO deep-dive; getting-started section added with Quick Start, Providers, Free Tiers, Auto-Combo, and Troubleshooting pages. ([#3438](https://github.com/diegosouzapw/OmniRoute/pull/3438) — thanks @oyi77)
|
||||
- **docs(opencode-plugin):** plugin README rewritten to lead with the why — positions the plugin as the recommended integration path over the legacy `@omniroute/opencode-provider` package, with migration guidance. ([#3418](https://github.com/diegosouzapw/OmniRoute/pull/3418) — thanks @herjarsa)
|
||||
- **docs(env):** `COMMAND_CODE_VERSION` override documented — environment variable added to `.env.example` and reference docs so operators can pin the CLI version header without a code change. ([#3462](https://github.com/diegosouzapw/OmniRoute/pull/3462) — thanks @hevener10)
|
||||
- **test(auto-combo):** same-provider connection identity assertion added — regression test covering the case where two connections for the same provider share an account ID, verifying the combo engine selects the correct one. ([#3378](https://github.com/diegosouzapw/OmniRoute/pull/3378) — thanks @oyi77)
|
||||
- **deps:** electron upgraded to 42.3.3; electron-builder to 26.15.2; electron-updater to 6.8.9; 4 development-group and 10 production-group packages bumped via Dependabot. ([#3441](https://github.com/diegosouzapw/OmniRoute/pull/3441) / [#3442](https://github.com/diegosouzapw/OmniRoute/pull/3442) / [#3443](https://github.com/diegosouzapw/OmniRoute/pull/3443) / [#3444](https://github.com/diegosouzapw/OmniRoute/pull/3444) / [#3445](https://github.com/diegosouzapw/OmniRoute/pull/3445) — thanks @diegosouzapw)
|
||||
- **chore(release):** v3.8.17 development cycle opened from `main`. (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.16] — 2026-06-08
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(vision-bridge):** auto-routing to the fastest available vision model — when a request carries image content and the selected model does not support vision, OmniRoute now transparently delegates to the best-match vision-capable model instead of returning an error. ([#3377](https://github.com/diegosouzapw/OmniRoute/pull/3377) — thanks @herjarsa)
|
||||
- **feat(web-session):** web-session pool observability — new MCP tool `get_web_session_pool_health` and a health-matrix REST response (`GET /api/web-session-pool/health`) expose per-provider slot counts, lease ages, and error budgets so operators can diagnose pool exhaustion without digging through logs. ([#3395](https://github.com/diegosouzapw/OmniRoute/pull/3395) — thanks @oyi77)
|
||||
- **feat(web-session):** adaptive keepalive threshold — the keepalive heartbeat interval now self-adjusts based on observed provider idle-disconnect behaviour instead of using a fixed constant, reducing both unnecessary pings and unexpected session drops. ([#3397](https://github.com/diegosouzapw/OmniRoute/pull/3397) — thanks @oyi77)
|
||||
- **feat(web-session):** bulk credential import endpoint (`POST /api/web-session/import`) — import a JSON array of session credentials in one call; each entry is validated and inserted atomically, with per-entry success/failure reported in the response. ([#3403](https://github.com/diegosouzapw/OmniRoute/pull/3403) — thanks @oyi77)
|
||||
- **feat(api):** REST API for session pool health (`GET /api/session-pool/health`) — a dashboard-facing endpoint that aggregates live slot usage, wait-queue depth, and error rates across all active session pools; wired to a new dashboard widget. ([#3404](https://github.com/diegosouzapw/OmniRoute/pull/3404) — thanks @oyi77)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(sse):** eliminate race window in `usageTokenBuffer` settings update — a concurrent save + stream-start could race to apply stale settings, causing token counts to roll back by up to 2 000 tokens after a restart; the update now uses an atomic read-modify-write on the shared settings ref. ([#3405](https://github.com/diegosouzapw/OmniRoute/pull/3405) — thanks @diegosouzapw)
|
||||
- **fix(context-cache):** server-side context-cache pinning now correctly persists across restarts; proxy message content no longer leaks into the upstream prompt; and the `context_cache_protection` toggle is properly saved to the DB on change. ([#3399](https://github.com/diegosouzapw/OmniRoute/pull/3399) — thanks @k0valik)
|
||||
- **fix(providers):** the provider settings page now refreshes its model list after a successful `sync-models` call — previously the stale list remained until a full page reload. ([#3402](https://github.com/diegosouzapw/OmniRoute/pull/3402) — thanks @0xtbug)
|
||||
- **fix(stream):** empty-choices chunks (choices array present but empty, no `finish_reason`) are now silently dropped rather than emitted as a `retry:` SSE event — removes spurious retry lines from streaming responses for providers that emit heartbeat keep-alive chunks. ([#3400](https://github.com/diegosouzapw/OmniRoute/pull/3400) — thanks @0xtbug)
|
||||
- **fix(account-fallback):** the connection cooldown deduplication state is now preserved across the fallback retry chain — previously a second concurrent failure on the same account could clear the dedupe flag set by the first, allowing the cooldown window to be extended twice. ([#3381](https://github.com/diegosouzapw/OmniRoute/pull/3381) — thanks @oyi77)
|
||||
- **fix(stream):** false-positive textual tool-call marker truncation — `containsTextualToolCallMarker` now tracks how much of the accumulated streamed content has already been emitted, so it only withholds the unemitted tail rather than re-scanning from the start on every new chunk. ([#3382](https://github.com/diegosouzapw/OmniRoute/pull/3382) — thanks @Ardem2025)
|
||||
- **fix(sanitizer):** `containsTextualToolCallContent()` now requires the complete `[Tool call: name]\nArguments:` header pattern instead of a bare `.includes("[Tool call:")` check — prevents the non-streaming response sanitizer from nulling out model responses that merely quote `[Tool call:]` in prose or code examples. ([#3355](https://github.com/diegosouzapw/OmniRoute/pull/3410) — thanks @diegosouzapw)
|
||||
- **fix(stream):** the streaming textual tool-call guard now flushes any remaining buffered content as plain text when the stream ends, regardless of whether the buffer contains `"Arguments:"` — previously, a partial/incomplete tool-call header that arrived at end-of-stream was silently dropped. ([#3355](https://github.com/diegosouzapw/OmniRoute/pull/3410) — thanks @diegosouzapw)
|
||||
- **fix(executor):** Mistral (and any provider in `PROVIDERS_REQUIRING_USER_LAST_MESSAGE`) no longer receives a trailing `assistant` message with plain text content — `stripTrailingAssistantForProvider` drops it on the upstream-send path, fixing the `400: Expected last role User or Tool … but got assistant` rejection. ([#3396](https://github.com/diegosouzapw/OmniRoute/pull/3409) — thanks @diegosouzapw)
|
||||
- **fix(mitm):** `getMitmStatus()` in the build-time stub (Docker image) now returns a graceful `{ running: false }` status instead of throwing, so the Agent Bridge UI shows a clean "stopped" state rather than an error banner in containerised deployments. ([#3390](https://github.com/diegosouzapw/OmniRoute/pull/3408) — thanks @diegosouzapw)
|
||||
- **fix(env):** corrected casing of `OMNIROUTE_TRACE` in `.env.example` and all related documentation files — was previously mixed-case in some places, causing the variable to be silently ignored on case-sensitive file systems. ([#3393](https://github.com/diegosouzapw/OmniRoute/pull/3393) — thanks @androw)
|
||||
- **fix(featureFlags):** `PRICING_SYNC_ENABLED` description now clearly states that the feature requires the corresponding environment variable to be set — removes the ambiguity that led operators to enable it via the UI only and wonder why sync never ran. ([#3394](https://github.com/diegosouzapw/OmniRoute/pull/3394) — thanks @androw)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **ci(docker):** the CI pipeline now builds and publishes the `-web` image variant in the same Docker publish workflow, so both the standard and browser-backed images stay in sync on every release. ([#3389](https://github.com/diegosouzapw/OmniRoute/pull/3389) — thanks @zhiru)
|
||||
- **ci(e2e):** E2E shard suite hardened — timeout raised to 45 min for the heaviest shard; build artifact now uses an explicit `tar` bundle to avoid `upload-artifact@v4` LCA path ambiguity; `node_modules` copied into standalone after download; browser cache added to cut cold-shard time; `sync-models` endpoint mocked in `providers-management.spec.ts` so the import modal reaches "done" immediately. (thanks @diegosouzapw)
|
||||
- **docs:** Codex CLI configuration guide added to the dashboard (`/dashboard/codex-config`) — covers profile naming, model selection, and the `CODEX_*` environment variables accepted by OmniRoute. (thanks @diegosouzapw)
|
||||
- **chore(agentSkills):** catalog expanded to 43 entries — `config-codex-cli` added as a new `CONFIG_SKILL_IDS` category; all skill-count assertions updated across unit and integration test suites; `next-fetch` opts cast to satisfy the TypeScript overload signature in the skill runner. (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.15] — 2026-06-07
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(error-rules):** provider-specific error classification with scope — a declarative rules layer lets providers map upstream error shapes to the right resilience action (provider circuit-breaker vs connection cooldown vs model lockout) at the correct scope, instead of relying on generic status-code heuristics. ([#3370](https://github.com/diegosouzapw/OmniRoute/pull/3370) — thanks @herjarsa)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(combo):** add `429` to `PROVIDER_FAILURE_ERROR_CODES` so a rate-limited target no longer drives an infinite retry loop — the combo now cools the target down and moves on. ([#3366](https://github.com/diegosouzapw/OmniRoute/pull/3366) — thanks @herjarsa)
|
||||
- **fix(catalog):** add a `getTokenLimit` fallback for combo targets with an unknown context window, so a target whose context can't be resolved no longer breaks token-limit computation for the combo. ([#3369](https://github.com/diegosouzapw/OmniRoute/pull/3369) — thanks @herjarsa)
|
||||
- **fix(auto-combo):** include no-auth providers in Auto-Combo declaratively (driven by provider metadata rather than a hard-coded list), so keyless providers are eligible candidates. ([#3365](https://github.com/diegosouzapw/OmniRoute/pull/3365) — thanks @oyi77)
|
||||
- **fix(auto-combo):** validate web-session credentials before selecting a web-cookie provider as an Auto-Combo target, so an expired/empty session doesn't get picked. ([#3371](https://github.com/diegosouzapw/OmniRoute/pull/3371) — thanks @oyi77)
|
||||
- **fix(command-code):** update the Command Code base URL from `/alpha/` to `/provider/v1/` (upstream moved the endpoint). ([#3372](https://github.com/diegosouzapw/OmniRoute/pull/3372) — thanks @TapZe)
|
||||
- **fix(kiro):** probe `%APPDATA%\kiro\storage.db` on Windows during Kiro auto-import, so the import finds the credential store where Kiro actually writes it on Windows. ([#3375](https://github.com/diegosouzapw/OmniRoute/pull/3375), fixes #3363 — thanks @diegosouzapw; reported by @Gerashka2)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **fix(migrations):** restore `095_provider_node_custom_headers.sql` — it was twice deleted from the release branch by a contributor branch's `git rm` of a duplicate getting folded into the squash merge; restored and guarded. (thanks @diegosouzapw)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.15:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------ | -------------------------------------------------- |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #3366, #3369, #3370 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3365, #3371 |
|
||||
| [@TapZe](https://github.com/TapZe) | #3372 |
|
||||
| [@Gerashka2](https://github.com/Gerashka2) | reported #3363 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3375 shepherding, migration restores |
|
||||
|
||||
---
|
||||
|
||||
## [3.8.14] — 2026-06-07
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(api):** per-provider **custom headers** for OpenAI/Anthropic-compatible provider nodes — attach operator-defined headers (e.g. tenant/routing headers) to upstream requests via a new `customHeaders` field on provider nodes (`custom_headers_json` column, migration 095). Hardened on merge: values/names validated through the canonical `upstreamHeadersRecordSchema` (CRLF/control-char/length/16-max) with a single shared `isForbiddenCustomHeaderName()` denylist (hop-by-hop + auth), applied case-insensitively, and honored for `anthropic-compatible-cc-*` nodes too. ([#3338](https://github.com/diegosouzapw/OmniRoute/pull/3338) — thanks @pizzav-xyz / @diegosouzapw)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **fix(security):** provider auto-sync self-fetch now uses a trusted loopback/env-pinned origin (`getModelSyncInternalBaseUrl()`) instead of `new URL(request.url).origin`, so a management-authenticated caller can no longer redirect the credential-bearing internal request to an arbitrary host via the `Host` header (CodeQL `js/request-forgery`, critical). Shipped to Docker/Electron in v3.8.13; reaches npm here (npm `3.8.13` was immutable). ([#3336](https://github.com/diegosouzapw/OmniRoute/pull/3336), CodeQL #323 — thanks @diegosouzapw)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(translator):** every Gemini/Vertex `functionDeclaration.parameters` is now coerced to an OBJECT-typed schema before cleaning. Clients like GitHub Copilot send some tools (e.g. `terminal_last_command`) whose `parameters` is present but lacks a top-level `type: "object"` (just `{ properties }`, a scalar type, or `{}`); these slipped through `buildGeminiTools`' `params || default` guard and Vertex rejected them with `[400] ... functionDeclaration parameters schema should be of type OBJECT`. Hardens every OpenAI→Gemini tool request (Vertex / antigravity / agy / gemini). (#3357 — thanks @nullbytef0x)
|
||||
- **fix(gemini):** normalize Gemini/Antigravity textual `[Tool call: ...]` markers — stop suppressing **false positives** (legitimate assistant prose that merely mentions `[Tool call: terminal]`, e.g. in backticks, is preserved instead of being swallowed) and correctly buffer markers **split across streaming chunks** (`[Tool` + ` call: terminal]` + `Arguments: {...}`), flushing the text when it turns out not to be a tool call. Dedups the parsing/validation into a shared `open-sse/utils/textualToolCall.ts` (new `isValidToolCallHeaderPrefix`) and adds `gemini-2.5-flash`/`gemini-3.5-flash-low` model specs. ([#3358](https://github.com/diegosouzapw/OmniRoute/pull/3358) — thanks @Ardem2025 / @diegosouzapw)
|
||||
- **fix(electron):** clicking "Exit" (or applying an update) now terminates the **whole** server process tree, not just the direct child. The embedded server runs as `omniroute.exe`-as-node (`ELECTRON_RUN_AS_NODE`) and spawns grandchildren (embedded services, MITM proxy, tunnels); on Windows `ChildProcess.kill()` only terminates the direct child, so survivors kept `omniroute.exe` locked — the process "hung in memory" after Exit and updates failed with "file in use". New `killProcessTree()` helper uses `taskkill /PID <pid> /T /F` on Windows (signal-based on POSIX); wired into `stopNextServer`, the `waitForServerExit` force-kill, and `installUpdate`. (#3347 — thanks @Flexible78)
|
||||
- **fix(proxy):** proxy auto-selection is now **opt-in** (new `PROXY_AUTO_SELECT_ENABLED` flag, default off). Previously a single proxy in the registry silently became a global fallback for **all** provider connections (the Step-11 fallback listed every registry proxy, ignoring assignments and per-connection `proxy_enabled`). It now no-ops unless the operator enables the flag. (#3332 — thanks @hertznsk)
|
||||
- **fix(cli):** write the OpenCode config to `~/.config/opencode/opencode.json` on **all** platforms — on Windows OmniRoute wrote to `%APPDATA%\opencode\` but OpenCode reads from `%USERPROFILE%\.config\opencode\` (XDG), so dashboard-saved config silently had no effect. (#3330 — thanks @abdulkadirozyurt)
|
||||
- **fix(catalog):** remove `minimaxai/minimax-m3` from the **NVIDIA NIM** tier — NVIDIA does not host it yet, so every request 404'd (`404 page not found`), while sibling `minimax-m2.7` on the same provider works. MiniMax M3 stays available on the tiers that actually serve it. (#3329 — thanks @mikmaneggahommie)
|
||||
- **fix(sse):** treat **MiniMax M3** as multimodal so the compression layer no longer strips image parts from vision requests — `lite.ts modelSupportsVision` now keeps images for `minimax-m3*` (see also the registry `supportsVision` alignment in Maintenance). ([#3328](https://github.com/diegosouzapw/OmniRoute/pull/3328) — thanks @diegosouzapw)
|
||||
- **fix(oauth):** Kiro **Builder ID** token import no longer fails with "Bad credentials" — `validateImportToken` only ever tried the social-auth refresh; it now uses the cached AWS SSO `clientId`/`clientSecret` (`~/.aws/sso/cache/*.json`) and the OIDC refresh path (`authMethod: "builder-id"`), with a TDD harness. ([#3333](https://github.com/diegosouzapw/OmniRoute/pull/3333) — thanks @quanturbo / @diegosouzapw)
|
||||
- **fix(provider-proxy):** honor per-account proxy toggles — a connection with `proxy_enabled = false` is no longer forced through an assigned/registry proxy. ([#3349](https://github.com/diegosouzapw/OmniRoute/pull/3349) — thanks @rdself)
|
||||
- **fix(providers):** reduce proxy label noise on the provider page (clearer proxy assignment/state display). ([#3346](https://github.com/diegosouzapw/OmniRoute/pull/3346) — thanks @wilsonicdev)
|
||||
- **fix(noauth):** expose only **usable** model aliases for no-auth providers, so the catalog no longer advertises aliases that can't actually be called. ([#3345](https://github.com/diegosouzapw/OmniRoute/pull/3345) — thanks @oyi77)
|
||||
- **fix(duckduckgo):** restore the bare `Response` contract for the DuckDuckGo/browser-backed executor (rebased onto the cycle), fixing a wrapping-contract regression. ([#3323](https://github.com/diegosouzapw/OmniRoute/pull/3323) — thanks @oyi77 / @diegosouzapw)
|
||||
- **fix(dashboard):** drop the duplicate "Distribute Proxies" button on the provider page — it rendered twice at once (provider toolbar + accounts-list header) whenever connections existed and none were selected. The toolbar button (global) and the per-tag-group buttons remain. ([#3352](https://github.com/diegosouzapw/OmniRoute/pull/3352) — thanks @diegosouzapw)
|
||||
- **fix(electron):** ship `loginManager.js` in the packaged app — #3292 added it (and a `require("./loginManager")` in `main.js`) without adding it to electron-builder's `build.files`, so the packaged app crashed at startup with "Cannot find module" on the Linux/macOS smoke tests. Plus a regression test asserting every local `require("./x")` in the Electron entry points is shipped. ([#3334](https://github.com/diegosouzapw/OmniRoute/pull/3334) — thanks @diegosouzapw)
|
||||
- **fix(startup):** correct the #3292 auto-refresh daemon import (`@/open-sse/...` → `@omniroute/open-sse/services/autoRefreshDaemon`); the `@/` alias maps to `src/`, so the daemon silently never ran in the built standalone (non-fatal "Cannot find module", caught at runtime). Adds a regression test banning `@/open-sse/*` imports in `src/`. ([#3335](https://github.com/diegosouzapw/OmniRoute/pull/3335) — thanks @diegosouzapw)
|
||||
- **fix(electron):** wrap `autoUpdater.checkForUpdates()` so a 404/offline/rate-limited update check can no longer surface as an unhandled rejection (the `error` event still notifies the user); fixes the macOS-intel packaged-app smoke failure. ([#3339](https://github.com/diegosouzapw/OmniRoute/pull/3339) — thanks @diegosouzapw)
|
||||
- **fix(dashboard):** stop the infinite render loop on `/dashboard/cli-agents/hermes-agent` — `HermesAgentToolCard` listed `currentRoles` in the config-load effect's deps while `loadCurrentConfig()` set `currentRoles` to a fresh object on every fetch, so the effect re-fired → refetched → re-set forever (the page spun and spammed `GET /api/cli-tools/hermes-agent-settings` in the console; it manifested only on the always-expanded detail page). `loadCurrentConfig` is now memoized and the batch-seed reads `currentRoles` via a functional update, so the effect runs once. Adds a jsdom regression test asserting the settings endpoint is fetched a bounded number of times. ([#3353](https://github.com/diegosouzapw/OmniRoute/pull/3353) — thanks @diegosouzapw)
|
||||
- **fix(dashboard):** the Usage Analytics card now surfaces the **real** backend error (status + message) instead of a generic placeholder when `/api/usage/analytics` fails — a new shared `fetchError.ts` helper extracts a useful message. ([#3356](https://github.com/diegosouzapw/OmniRoute/pull/3356) — thanks @diegosouzapw)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **fix(review):** harden the per-provider custom-headers feature surfaced by the `/review-reviews` battery — `updateProviderNode` no longer wipes stored `custom_headers_json` on a partial update that omits the field; `customHeadersSchema` reuses the canonical `upstreamHeadersRecordSchema` guards (CRLF/control-char/length/16-max) and rejects auth header names via a single shared `isForbiddenCustomHeaderName()` denylist (executor + schema no longer keep divergent copies); custom headers now reach the wire for `anthropic-compatible-cc-*` nodes and override the executor's own `Content-Type`/`Accept` case-insensitively instead of duplicating them; and `rowToCamel` normalizes a NULL `_json` column to `baseKey: null`. ([#3350](https://github.com/diegosouzapw/OmniRoute/pull/3350) — thanks @diegosouzapw)
|
||||
- **fix(catalog):** flag every `minimax-m3` registry entry `supportsVision` (not just the opencode free tier) so the vision-bridge guardrail and the compression layer agree the model is multimodal on all tiers (completes #3328). (thanks @diegosouzapw)
|
||||
- **fix(oauth):** Kiro Builder ID import forwards the requested `region` to the OIDC validation refresh (no longer pinned to `us-east-1`), prefers the region-matching cached SSO client registration over the first file found, and falls `expiresIn` back to 3600 on the OIDC path. (thanks @diegosouzapw)
|
||||
- **fix(db):** migration `095` gains an `isSchemaAlreadyApplied` guard so a fresh DB (where `SCHEMA_SQL` already creates `custom_headers_json`) skips it cleanly instead of throwing-then-catching a duplicate-column error. (thanks @diegosouzapw)
|
||||
- **test:** align stale cycle tests with shipped behavior — NVIDIA `minimaxai/minimax-m3` removal (#3329), the 29th feature flag (`PROXY_AUTO_SELECT_ENABLED`, #3332), and the OpenCode `~/.config` path on Windows (#3330). (thanks @diegosouzapw)
|
||||
- **docs:** add a documentation comment to the exported `GET` handler in the context-analytics route. ([#3337](https://github.com/diegosouzapw/OmniRoute/pull/3337) — thanks @Lang-Qiu)
|
||||
- **docs(i18n):** translate 25 core documentation files to Indonesian. ([#3348](https://github.com/diegosouzapw/OmniRoute/pull/3348) — thanks @KrisnaSantosa15)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.14:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #3338 |
|
||||
| [@quanturbo](https://github.com/quanturbo) | #3333 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3323, #3345 |
|
||||
| [@rdself](https://github.com/rdself) | #3349 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3346 |
|
||||
| [@hertznsk](https://github.com/hertznsk) | #3332 |
|
||||
| [@abdulkadirozyurt](https://github.com/abdulkadirozyurt) | #3330 |
|
||||
| [@mikmaneggahommie](https://github.com/mikmaneggahommie) | #3329 |
|
||||
| [@Flexible78](https://github.com/Flexible78) | #3347 |
|
||||
| [@Lang-Qiu](https://github.com/Lang-Qiu) | #3337 |
|
||||
| [@KrisnaSantosa15](https://github.com/KrisnaSantosa15) | #3348 |
|
||||
| [@nullbytef0x](https://github.com/nullbytef0x) | #3357 |
|
||||
| [@Ardem2025](https://github.com/Ardem2025) | #3358 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3334, #3335, #3336, #3339, #3350, #3352, #3353, #3356; review/hardening across the cycle |
|
||||
|
||||
---
|
||||
|
||||
## [3.8.13] — 2026-06-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(web-cookie):** self-service login infrastructure for 21 web-cookie providers — three login pathways (Electron BrowserWindow, Playwright dashboard fallback, `POST /api/providers/{id}/login`), token-extraction configs, and a 15-min cookie-validity auto-refresh daemon. Hardened on merge: error bodies sanitized (Hard Rule #12), the spawn-capable login route classified LOCAL_ONLY (Hard Rules #15/#17), and the Electron status listener de-duplicated. ([#3292](https://github.com/diegosouzapw/OmniRoute/pull/3292), closes #3070 — thanks @oyi77 / @diegosouzapw)
|
||||
- **feat(api):** accept path-scoped API keys on client API routes — keys may arrive via `/api/v1/vscode/<key>/…` path aliases (incl. `raw`/`combos`); explicit `Authorization`/`x-api-key` headers still take precedence. Split out of #3073. ([#3300](https://github.com/diegosouzapw/OmniRoute/pull/3300) — thanks @zhiru)
|
||||
- **feat(api):** model-catalog enrichment + MCP `model-catalog` tools — richer per-model metadata (context window, capabilities) surfaced through `/v1/models` and new MCP tools, plus `readHeaderValue` header-record support. Split out of #3073; reconciled on merge with the #3309 URL-token hardening (kept the security gate — no query-string credential fallback, management auth stays header-only). ([#3306](https://github.com/diegosouzapw/OmniRoute/pull/3306) — thanks @zhiru / @diegosouzapw)
|
||||
- **feat(dashboard):** internationalize the proxy settings UI — `ProxyTab` + the proxy `DocumentationTab`/`FreePoolTab`/`VercelRelayModal` now render via `t(...)`, with matching `en`/`pt-BR` message keys. Split out of #3073. ([#3307](https://github.com/diegosouzapw/OmniRoute/pull/3307), [#3310](https://github.com/diegosouzapw/OmniRoute/pull/3310) — thanks @zhiru)
|
||||
- **feat(provider):** provider test-all endpoint + per-connection rate-limit overrides + model visibility — `POST /api/models/test-all` runs parallel model tests (chunked, timeout-skip) atop a shared `runSingleModelTest` runner; per-connection rate-limit overrides land via `PATCH /api/providers/:id` (new `rate_limit_overrides_json` column + Zod schema); a dashboard model-visibility toolbar (All / Visible / Hidden) drives a `/v1/models` catalog that excludes user-hidden models; models auto-fetch on every connection add; and passthrough (OpenRouter) models gain test buttons. Folds in dashboard fixes on merge (missing alias/delete handlers, duplicate-model-ID React keys, "Hide all" restored) and a build fix so empty `.env` values no longer override real config. ([#3267](https://github.com/diegosouzapw/OmniRoute/pull/3267) — thanks @Vinayrnani)
|
||||
- **feat(api):** VS Code Copilot Ollama-compatible BYOK endpoint — exposes an Ollama-shaped surface so VS Code Copilot's "bring your own key" Ollama provider can target OmniRoute directly, with a `VscodeTokenAliasCard` in the dashboard endpoint tab to generate the path-scoped token alias. ([#3316](https://github.com/diegosouzapw/OmniRoute/pull/3316) — thanks @zhiru)
|
||||
- **feat(combo):** Auto-Combo candidate-expansion optimization + playground model dropdown + "only configured" model toggle — reworks the `auto` strategy's candidate selection in `combo.ts` and surfaces a model picker in the playground `StudioConfigPane` / `useAvailableModels`. ([#3322](https://github.com/diegosouzapw/OmniRoute/pull/3322) — thanks @oyi77)
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **fix(auth):** follow-up hardening of the client-API key extractor (#3300) — removed the generic query-string token fallbacks (`?token=`/`?key=`/`?apiKey=`/`?api_key=`), which leak credentials into access logs / Referer headers, and gated URL-borne tokens to client routes only (management auth is now header-only) so a credential in the URL can never authenticate a management route. The path-scoped `/vscode/<key>/…` form the VS Code integration needs is unchanged. (security review follow-up to [#3300](https://github.com/diegosouzapw/OmniRoute/pull/3300) — thanks @zhiru / @diegosouzapw)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(dashboard):** Agent Bridge page (`/dashboard/tools/agent-bridge`) no longer crashes with "Internal Server Error" — the page replaced its well-shaped state with the raw `/api/tools/agent-bridge/state` response (`{ server, agents }`), leaving `serverState` undefined and throwing `Cannot read properties of undefined (reading 'running')`. A shared `normalizeAgentBridgeState()` now maps the route shape into the page contract (incl. `server.certExists → certTrusted`) and always returns safe defaults, used by both the SSR loader and the polling hook. (#3318 — thanks @tycronk20)
|
||||
- **fix(codex):** strip client-only params (`prompt_cache_retention`, `safety_identifier`, `user`) on the native `codex/` `/v1/responses` passthrough — Codex upstream rejects them with `400 Unsupported parameter`, which broke Factory Droid and any client injecting those fields. The chat-completions path already stripped them; the responses→responses passthrough now does too. (#3317 — thanks @tycronk20)
|
||||
- **fix(theoldllm):** stop the `[502]: Body is unusable: Body has already been read` error on the cached-token path — the executor read the same upstream `Response` body with `.text()` twice; it now reads it once and only re-reads after a token-rejection refetch. (#3296 — thanks @onizukashonan14-png)
|
||||
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
|
||||
- **fix(dashboard):** refresh the connection list after a Codex/Claude/Gemini auth import — the import modals called `fetchData()` (which only reloads provider metadata), so a freshly-imported connection stayed invisible until a manual reload; they now call `fetchConnections()`. ([#3320](https://github.com/diegosouzapw/OmniRoute/pull/3320) — thanks @zhiru)
|
||||
- **fix(cli):** `omniroute update` no longer always fails on a global install — `getCurrentVersion()` and `createBackup()` now resolve `package.json`/`bin` relative to the script (`import.meta.url`) instead of `process.cwd()` (the user's working dir on a global npm/brew install → _"Could not determine current version"_), and the backup copies the `cli` directory with `cpSync({recursive:true})` instead of `copyFileSync`, which threw a swallowed `EISDIR` → _"Failed to create backup. Aborting"_. (#3295 — thanks @uniQta)
|
||||
- **fix(sse):** harden the passthrough stream against empty upstream responses — emit a synthetic retry chunk on an empty `choices: []` (fixes a Copilot Chat crash) and log empty post-`tool_calls` completions; also registers **MiniMax M3** (1M context) across 8 provider tiers. ([#3297](https://github.com/diegosouzapw/OmniRoute/pull/3297), #3110 — thanks @wilsonicdev)
|
||||
- **fix(opencode-provider):** extract `contextLength` from the live `/v1/models` catalog (live > `modelContextLengths` > static map) so passthrough models outside the legacy 8-model map no longer silently truncate to OpenCode's 128K default. ([#3298](https://github.com/diegosouzapw/OmniRoute/pull/3298) — thanks @herjarsa / @diegosouzapw)
|
||||
- **fix(dev):** auto-rebuild `better-sqlite3` on a Node ABI mismatch at `npm run dev` startup (nvm 22↔24) — dev-only, no-op on the healthy path, unrelated errors not swallowed. ([#3301](https://github.com/diegosouzapw/OmniRoute/pull/3301) — thanks @zhiru)
|
||||
- **fix(api):** remove the bundled **Completions.me** provider preset — empirically verified to return Rick Astley lyrics instead of real completions for every model/prompt. ([#3302](https://github.com/diegosouzapw/OmniRoute/pull/3302), discussion #3293 — thanks @diegosouzapw; reported by @mikmaneggahommie)
|
||||
- **fix(ci):** skip the auto-deploy step when the VPS SSH port is unreachable from the GitHub runner (private LAN / firewall) instead of red-failing every release pipeline; genuine deploy/boot failures still fail honestly. ([#3299](https://github.com/diegosouzapw/OmniRoute/pull/3299) — thanks @diegosouzapw)
|
||||
- **fix(sse):** strip leaked internal tool-call envelopes (`to=functions.*` / `multi_tool_use.parallel { … }`) from visible assistant text and sanitize Responses-API streaming (drop `commentary`-phase output items) so harness syntax never reaches the client. ([#3311](https://github.com/diegosouzapw/OmniRoute/pull/3311) — thanks @zhiru)
|
||||
- **fix(sse):** expose the Claude (`claude-opus-4-6-thinking`, `claude-sonnet-4-6`) and Gemini budget tiers (`gemini-3.1-pro-{high,low}`, `gemini-3.5-flash-{low,extra-low}`) in the Antigravity catalog — they are user-callable on the Antigravity OAuth backend (agy parity), correcting an earlier assumption that Claude had been removed. ([#3303](https://github.com/diegosouzapw/OmniRoute/pull/3303), discussion #3184 — thanks @diegosouzapw)
|
||||
- **fix(catalog):** compute a combo's `context_length` from the known targets only — a single target with unknown context no longer collapses the whole combo to `undefined`; also accepts live `{id, contextLength}` model entries in the opencode-provider helper (follow-up to #3298). ([#3304](https://github.com/diegosouzapw/OmniRoute/pull/3304) — thanks @herjarsa / @diegosouzapw)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **test(catalog):** align the Antigravity preview-alias catalog test with the #3303 budget tiers — asserts the restored Claude/Gemini tiers are surfaced, locking in the behavior so a future tier change can't silently drop them again (thanks @diegosouzapw)
|
||||
- **docs:** rename the `resolve-issues` skill references to `review-issues` across the docs/skill surfaces, matching the renamed governance skill (thanks @diegosouzapw)
|
||||
- **docs:** document the VS Code / Ollama endpoints (API reference + new `docs/reference/CLI-TOOLS.md`) and improve the env-bootstrap + i18n key-coverage tooling. ([#3319](https://github.com/diegosouzapw/OmniRoute/pull/3319) — thanks @zhiru)
|
||||
- **chore(release):** open the v3.8.13 development cycle (version bump + cycle bookkeeping) and finalize this changelog (thanks @diegosouzapw)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.13:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| [@zhiru](https://github.com/zhiru) | #3300, #3306, #3307 / #3310, #3309, #3301, #3311, #3320, #3319, #3316 |
|
||||
| [@tycronk20](https://github.com/tycronk20) | #3317, #3318 |
|
||||
| [@Vinayrnani](https://github.com/Vinayrnani) | #3267 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3292 (closes #3070), #3322 |
|
||||
| [@onizukashonan14-png](https://github.com/onizukashonan14-png) | #3296 |
|
||||
| [@uniQta](https://github.com/uniQta) | #3290, #3295 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3297 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #3298, #3304 |
|
||||
| [@mikmaneggahommie](https://github.com/mikmaneggahommie) | reported the Completions.me rickroll (discussion #3293) |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3299, #3302, #3303; co-author on #3292 / #3306 / #3298 / #3304 / #3309 |
|
||||
|
||||
---
|
||||
|
||||
## [3.8.12] — 2026-06-06
|
||||
|
||||
### ✨ New Features
|
||||
@@ -42,14 +615,14 @@
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.12:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@oyi77](https://github.com/oyi77) | #3250, #3259, #3280, #3285, #3286 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3249, #3268, #3282 / #3283 (co-author, #3247 diagnosis), #3287 |
|
||||
| [@strangersp](https://github.com/strangersp) | #3261 |
|
||||
| [@MikeTuev](https://github.com/MikeTuev) | #3248 |
|
||||
| [@leninejunior](https://github.com/leninejunior) | #3271 |
|
||||
| [@zhiru](https://github.com/zhiru) | #3274 |
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| [@oyi77](https://github.com/oyi77) | #3250, #3259, #3280, #3285, #3286 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3249, #3268, #3282 / #3283 (co-author, #3247 diagnosis), #3287 |
|
||||
| [@strangersp](https://github.com/strangersp) | #3261 |
|
||||
| [@MikeTuev](https://github.com/MikeTuev) | #3248 |
|
||||
| [@leninejunior](https://github.com/leninejunior) | #3271 |
|
||||
| [@zhiru](https://github.com/zhiru) | #3274 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3256, #3263, #3270, #3275, #3277, #3278, #3279, #3281, #3284, #3289 |
|
||||
|
||||
---
|
||||
@@ -98,26 +671,26 @@ Thanks to everyone whose work landed in v3.8.12:
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.11:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3189, #3201, #3203, #3204, #3232, #3240, #3241 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #3170, #3171, #3172 |
|
||||
| [@zhiru](https://github.com/zhiru) | #3185, #3195 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3217 |
|
||||
| [@miracuves](https://github.com/miracuves) | #3116, #3226 |
|
||||
| [@ngocquynh85](https://github.com/ngocquynh85) | #3205, #3214, #3215 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #3188 |
|
||||
| [@bypanghu](https://github.com/bypanghu) | #3191 |
|
||||
| [@juandisay](https://github.com/juandisay) | #3206 |
|
||||
| [@tjengbudi](https://github.com/tjengbudi) | #3197, #3198, #3199 |
|
||||
| [@naimo84](https://github.com/naimo84) | #3151 |
|
||||
| [@yeardie](https://github.com/yeardie) | #3025 |
|
||||
| [@pulyankote](https://github.com/pulyankote) | #3202 |
|
||||
| [@YoursSweetDom](https://github.com/YoursSweetDom) | #3180 |
|
||||
| [@Guru01100101](https://github.com/Guru01100101) | #3091 |
|
||||
| [@androw](https://github.com/androw) | #3167 (co-author) |
|
||||
| [@ibanunmangun](https://github.com/ibanunmangun) | #3193 (co-author) |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3187, #3200, issue-fix batches |
|
||||
| Contributor | PRs / Issues |
|
||||
| -------------------------------------------------- | ----------------------------------------------- |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | #3189, #3201, #3203, #3204, #3232, #3240, #3241 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #3170, #3171, #3172 |
|
||||
| [@zhiru](https://github.com/zhiru) | #3185, #3195 |
|
||||
| [@oyi77](https://github.com/oyi77) | #3217 |
|
||||
| [@miracuves](https://github.com/miracuves) | #3116, #3226 |
|
||||
| [@ngocquynh85](https://github.com/ngocquynh85) | #3205, #3214, #3215 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #3188 |
|
||||
| [@bypanghu](https://github.com/bypanghu) | #3191 |
|
||||
| [@juandisay](https://github.com/juandisay) | #3206 |
|
||||
| [@tjengbudi](https://github.com/tjengbudi) | #3197, #3198, #3199 |
|
||||
| [@naimo84](https://github.com/naimo84) | #3151 |
|
||||
| [@yeardie](https://github.com/yeardie) | #3025 |
|
||||
| [@pulyankote](https://github.com/pulyankote) | #3202 |
|
||||
| [@YoursSweetDom](https://github.com/YoursSweetDom) | #3180 |
|
||||
| [@Guru01100101](https://github.com/Guru01100101) | #3091 |
|
||||
| [@androw](https://github.com/androw) | #3167 (co-author) |
|
||||
| [@ibanunmangun](https://github.com/ibanunmangun) | #3193 (co-author) |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — #3187, #3200, issue-fix batches |
|
||||
|
||||
---
|
||||
|
||||
@@ -333,9 +906,9 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
refreshed siblings concurrently, so Auth0 revoked the whole token family
|
||||
(`openai/codex#9648`) and every account but the last died with
|
||||
`[403] <!DOCTYPE html>`. The quota path now skips proactive refresh for
|
||||
rotating providers (`rotationGroupFor`) and reuses the current access_token,
|
||||
rotating providers (`rotationGroupFor`) and reuses the current access*token,
|
||||
deferring genuine expiry to the reactive, serialized 401 path. Defense in
|
||||
depth: `serializeRefresh` now leaves a settle gap between two *queued* sibling
|
||||
depth: `serializeRefresh` now leaves a settle gap between two \_queued* sibling
|
||||
refreshes (default 2000 ms, tunable via `CODEX_REFRESH_SPACING_MS`, `"0"` to
|
||||
opt out) while releasing a lone refresh immediately, so the reactive path adds
|
||||
no latency.
|
||||
@@ -398,7 +971,7 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
via a new `RegistryModel.interleavedField` field, so follow-up/tool-use turns
|
||||
replay reasoning_content. Previously `big-pickle` matched no replay pattern and
|
||||
failed with `[400] The reasoning_content in the thinking mode must be passed
|
||||
back to the API` (its DeepSeek-thinking upstream is not detectable from the
|
||||
back to the API` (its DeepSeek-thinking upstream is not detectable from the
|
||||
model id, and `requiresReasoningReplay` does not consume `supportsReasoning`).
|
||||
`getResolvedModelCapabilities` now surfaces the registry `interleavedField`. (#2900)
|
||||
- **providers/github-copilot:** built-in GitHub Copilot Claude Opus and Gemini
|
||||
@@ -431,7 +1004,7 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
instead of failing with "No credentials for provider: opencode-zen". A
|
||||
configured, active key is still used when present. (#2962)
|
||||
- **translator/responses:** fixed an upstream `[400] Messages with role 'tool'
|
||||
must be a response to a preceding message with 'tool_calls'` when a Codex
|
||||
must be a response to a preceding message with 'tool_calls'` when a Codex
|
||||
client sent a `function_call` with an empty/missing `call_id`. The orphaned
|
||||
`function_call_output` previously slipped past the orphan filter. Now
|
||||
empty-`call_id` function calls are skipped (no dangling assistant tool_call)
|
||||
@@ -473,7 +1046,7 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
- **sse/chatCore:** the heap-pressure guard now auto-calibrates its threshold to 85%
|
||||
of the live V8 heap ceiling (floor 400 MB) instead of a fixed 200 MB that sat below
|
||||
the app's ~260 MB baseline and returned `503 Service temporarily unavailable due to
|
||||
resource pressure` for every request once the heap warmed up. It now tracks
|
||||
resource pressure` for every request once the heap warmed up. It now tracks
|
||||
`--max-old-space-size` across 1 GB / 2 GB / large VPS; `HEAP_PRESSURE_THRESHOLD_MB`
|
||||
still overrides. (#3052)
|
||||
- **proxy:** fail closed for OAuth usage-account proxies (#3051 — thanks @terence71-glitch)
|
||||
@@ -488,33 +1061,33 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
|
||||
A special thanks to everyone who contributed to this release — 746 commits since `v3.8.7`:
|
||||
|
||||
| Contributor | PRs / Contribution |
|
||||
| --- | --- |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — AgentBridge, Traffic Inspector, Quota Share Engine, Nav Restructure, Plugins integration, releases & upstream ports |
|
||||
| [@oyi77](https://github.com/oyi77) | #2913, #2947, #2954, #2978, #3015, #3018, #3039, #3041, #3045, #3046, #3049 |
|
||||
| [@terence71-glitch](https://github.com/terence71-glitch) | #2956, #2960, #2963, #2984, #3000, #3006, #3012, #3048, #3051 |
|
||||
| [@soyelmismo](https://github.com/soyelmismo) | #2951, #2965, #2973 |
|
||||
| [@branben](https://github.com/branben) | #2958, #2959 |
|
||||
| [@makcimbx](https://github.com/makcimbx) | #2937, #2938 |
|
||||
| [@guanbear](https://github.com/guanbear) | #2931, #3031 |
|
||||
| [@Lion-killer](https://github.com/Lion-killer) | #2981, #2988 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | per-API-key stream default mode |
|
||||
| [@androw](https://github.com/androw) | #3017 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #2975, #3064 |
|
||||
| [@S0yora](https://github.com/S0yora) | #2964 |
|
||||
| [@NekoMonci12](https://github.com/NekoMonci12) | #3008 |
|
||||
| [@Tentoxa](https://github.com/Tentoxa) | #3010 |
|
||||
| [@ReqX](https://github.com/ReqX) | #2957 |
|
||||
| [@NomenAK](https://github.com/NomenAK) | #2943 |
|
||||
| [@charithharshana](https://github.com/charithharshana) | #2940 |
|
||||
| [@dhaern](https://github.com/dhaern) | #2927 |
|
||||
| [@dangeReis](https://github.com/dangeReis) | #3021 |
|
||||
| [@bobbyunknown](https://github.com/bobbyunknown) | #3029 |
|
||||
| [@CitrusIce](https://github.com/CitrusIce) | #3035, #3058 |
|
||||
| [@wussh](https://github.com/wussh) | #3036 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #3037 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #3043 |
|
||||
| [@freefrank](https://github.com/freefrank) | #3066 (reported the Docker build failure) |
|
||||
| Contributor | PRs / Contribution |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer — AgentBridge, Traffic Inspector, Quota Share Engine, Nav Restructure, Plugins integration, releases & upstream ports |
|
||||
| [@oyi77](https://github.com/oyi77) | #2913, #2947, #2954, #2978, #3015, #3018, #3039, #3041, #3045, #3046, #3049 |
|
||||
| [@terence71-glitch](https://github.com/terence71-glitch) | #2956, #2960, #2963, #2984, #3000, #3006, #3012, #3048, #3051 |
|
||||
| [@soyelmismo](https://github.com/soyelmismo) | #2951, #2965, #2973 |
|
||||
| [@branben](https://github.com/branben) | #2958, #2959 |
|
||||
| [@makcimbx](https://github.com/makcimbx) | #2937, #2938 |
|
||||
| [@guanbear](https://github.com/guanbear) | #2931, #3031 |
|
||||
| [@Lion-killer](https://github.com/Lion-killer) | #2981, #2988 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | per-API-key stream default mode |
|
||||
| [@androw](https://github.com/androw) | #3017 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #2975, #3064 |
|
||||
| [@S0yora](https://github.com/S0yora) | #2964 |
|
||||
| [@NekoMonci12](https://github.com/NekoMonci12) | #3008 |
|
||||
| [@Tentoxa](https://github.com/Tentoxa) | #3010 |
|
||||
| [@ReqX](https://github.com/ReqX) | #2957 |
|
||||
| [@NomenAK](https://github.com/NomenAK) | #2943 |
|
||||
| [@charithharshana](https://github.com/charithharshana) | #2940 |
|
||||
| [@dhaern](https://github.com/dhaern) | #2927 |
|
||||
| [@dangeReis](https://github.com/dangeReis) | #3021 |
|
||||
| [@bobbyunknown](https://github.com/bobbyunknown) | #3029 |
|
||||
| [@CitrusIce](https://github.com/CitrusIce) | #3035, #3058 |
|
||||
| [@wussh](https://github.com/wussh) | #3036 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #3037 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #3043 |
|
||||
| [@freefrank](https://github.com/freefrank) | #3066 (reported the Docker build failure) |
|
||||
|
||||
A special thanks to everyone who contributed code, reviews, and tests for this release:
|
||||
@androw, @bobbyunknown, @branben, @charithharshana, @Chewji9875, @CitrusIce, @dangeReis, @dhaern, @diegosouzapw, @freefrank, @guanbear, @herjarsa, @JxnLexn, @Lion-killer, @makcimbx, @NekoMonci12, @NomenAK, @oyi77, @ReqX, @S0yora, @soyelmismo, @Tentoxa, @terence71-glitch, @wussh, @xz-dev
|
||||
@@ -619,7 +1192,7 @@ A special thanks to everyone who contributed code, reviews, and tests for this r
|
||||
- **warning-cleanup:** relax node engine constraint to `>=22.0.0` and clean dependencies (keeping `marked-terminal` to prevent TUI REPL crash) (#2792 — thanks @oyi77)
|
||||
- **combo:** normalize upstream Headers into a plain object before classification to avoid Node 24 / undici cross-instance `Cannot read private member #headers` crash on combo failover (#2751)
|
||||
- **translator:** silently drop `tool_search` built-in tool type instead of returning 400 — newer Codex clients send `tool_search` as a Responses API built-in with no Chat Completions equivalent (#2766)
|
||||
- **usage:** un-invert GitHub Copilot Free / limited plan quota — `limited_user_quotas` is the *remaining* count, not used, so the dashboard now shows 100% when the quota is untouched and 0% when fully exhausted (#2876 — thanks @androw)
|
||||
- **usage:** un-invert GitHub Copilot Free / limited plan quota — `limited_user_quotas` is the _remaining_ count, not used, so the dashboard now shows 100% when the quota is untouched and 0% when fully exhausted (#2876 — thanks @androw)
|
||||
- **fix(cli):** register openclaw in the CLI tool-detector so it appears in `omniroute status` alongside its existing API and config support ([#2833](https://github.com/diegosouzapw/OmniRoute/issues/2833))
|
||||
- **oauth (windsurf):** hotfix Windsurf login — drop the dead PKCE flow and promote the import-token flow as the default ([#2884](https://github.com/diegosouzapw/OmniRoute/pull/2884) — thanks @yunaamelia)
|
||||
- **antigravity:** normalize textual SSE tool calls and classify Gemini Antigravity resource exhaustion as a model lockout instead of a connection failure ([#2828](https://github.com/diegosouzapw/OmniRoute/pull/2828) — thanks @Ardem2025)
|
||||
@@ -654,33 +1227,33 @@ A special thanks to everyone who contributed code, reviews, and tests for this r
|
||||
|
||||
A special thanks to everyone who contributed to this release. Ranked by commits since `v3.8.6` (105 commits total):
|
||||
|
||||
| Contributor | Commits | PRs |
|
||||
| --- | ---: | --- |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | 38 | maintainer — releases, upstream ports & fixes |
|
||||
| [@oyi77](https://github.com/oyi77) | 10 | #2887, #2862, #2866, #2837, #2885, #2792, #2793 |
|
||||
| [@yunaamelia](https://github.com/yunaamelia) | 7 | #2884 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | 6 | #2868, #2886, #2865, #2860, #2857, #2801 |
|
||||
| [@leninejunior](https://github.com/leninejunior) | 4 | #2818, #2824, #2825, #2816 |
|
||||
| [@jeferssonlemes](https://github.com/jeferssonlemes) | 3 | #2791, #2802, #2815, #2817 |
|
||||
| [@rdself](https://github.com/rdself) | 3 | #2874, #2875, #2880 |
|
||||
| Dmitry Kuznetsov | 3 | textual tool-call & lockout hardening |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | 2 | #2799, #2800 |
|
||||
| [@unitythemaker](https://github.com/unitythemaker) | 2 | #2904 |
|
||||
| Nikolay Alafuzov | 2 | reasoning interleaved gating |
|
||||
| [@Tushar49](https://github.com/Tushar49) | 2 | #2854, #2855, #2807 |
|
||||
| [@guanbear](https://github.com/guanbear) | 2 | #2908 |
|
||||
| [@soyelmismo](https://github.com/soyelmismo) | 2 | #2903, #2842 |
|
||||
| [@RajvardhanPatil07](https://github.com/RajvardhanPatil07) | 1 | #2861 |
|
||||
| [@mugnimaestra](https://github.com/mugnimaestra) | 1 | #2888 |
|
||||
| [@dhaern](https://github.com/dhaern) | 1 | #2878 |
|
||||
| [@hartmark](https://github.com/hartmark) | 1 | #2795, #2771 |
|
||||
| [@marchlhw](https://github.com/marchlhw) | 1 | #2821 |
|
||||
| [@alltomatos](https://github.com/alltomatos) | 1 | i18n pt-BR |
|
||||
| [@akarray](https://github.com/akarray) | 1 | #2796 |
|
||||
| [@gogones](https://github.com/gogones) | 1 | #2845 |
|
||||
| [@disonjer](https://github.com/disonjer) | 1 | #2840 |
|
||||
| [@nickwizard](https://github.com/nickwizard) | 1 | #2841 |
|
||||
| [@levonk](https://github.com/levonk) | 1 | #2806 |
|
||||
| Contributor | Commits | PRs |
|
||||
| ---------------------------------------------------------- | ------: | ----------------------------------------------- |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | 38 | maintainer — releases, upstream ports & fixes |
|
||||
| [@oyi77](https://github.com/oyi77) | 10 | #2887, #2862, #2866, #2837, #2885, #2792, #2793 |
|
||||
| [@yunaamelia](https://github.com/yunaamelia) | 7 | #2884 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | 6 | #2868, #2886, #2865, #2860, #2857, #2801 |
|
||||
| [@leninejunior](https://github.com/leninejunior) | 4 | #2818, #2824, #2825, #2816 |
|
||||
| [@jeferssonlemes](https://github.com/jeferssonlemes) | 3 | #2791, #2802, #2815, #2817 |
|
||||
| [@rdself](https://github.com/rdself) | 3 | #2874, #2875, #2880 |
|
||||
| Dmitry Kuznetsov | 3 | textual tool-call & lockout hardening |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | 2 | #2799, #2800 |
|
||||
| [@unitythemaker](https://github.com/unitythemaker) | 2 | #2904 |
|
||||
| Nikolay Alafuzov | 2 | reasoning interleaved gating |
|
||||
| [@Tushar49](https://github.com/Tushar49) | 2 | #2854, #2855, #2807 |
|
||||
| [@guanbear](https://github.com/guanbear) | 2 | #2908 |
|
||||
| [@soyelmismo](https://github.com/soyelmismo) | 2 | #2903, #2842 |
|
||||
| [@RajvardhanPatil07](https://github.com/RajvardhanPatil07) | 1 | #2861 |
|
||||
| [@mugnimaestra](https://github.com/mugnimaestra) | 1 | #2888 |
|
||||
| [@dhaern](https://github.com/dhaern) | 1 | #2878 |
|
||||
| [@hartmark](https://github.com/hartmark) | 1 | #2795, #2771 |
|
||||
| [@marchlhw](https://github.com/marchlhw) | 1 | #2821 |
|
||||
| [@alltomatos](https://github.com/alltomatos) | 1 | i18n pt-BR |
|
||||
| [@akarray](https://github.com/akarray) | 1 | #2796 |
|
||||
| [@gogones](https://github.com/gogones) | 1 | #2845 |
|
||||
| [@disonjer](https://github.com/disonjer) | 1 | #2840 |
|
||||
| [@nickwizard](https://github.com/nickwizard) | 1 | #2841 |
|
||||
| [@levonk](https://github.com/levonk) | 1 | #2806 |
|
||||
|
||||
_Reviews & additional contributions: @androw, @Ardem2025, @InkshadeWoods._
|
||||
A special thanks to everyone who contributed code, reviews, and tests for this release:
|
||||
@@ -718,7 +1291,6 @@ A special thanks to everyone who contributed code, reviews, and tests for this r
|
||||
A special thanks to everyone who contributed code, reviews, and tests for this release:
|
||||
@akarray, @hartmark, @hijak, @JxnLexn, @kjhq, @rdself, @thanet-s
|
||||
|
||||
|
||||
---
|
||||
|
||||
## [3.8.4] — 2026-05-26
|
||||
@@ -1850,7 +2422,7 @@ Thank you to all **55+ community contributors** who made v3.8.0 possible! 🎉
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- **chore(workflow):** mandate implementation plan generation in `/resolve-issues` workflow before coding
|
||||
- **chore(workflow):** mandate implementation plan generation in `/review-issues` workflow before coding
|
||||
- **chore(release):** expand contributor credits to 155 PRs across full project history
|
||||
|
||||
### 🏆 Community Contributors Acknowledgment
|
||||
|
||||
70
CLAUDE.md
70
CLAUDE.md
@@ -37,20 +37,20 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
|
||||
|
||||
**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 (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 |
|
||||
| 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 (83 files, 97 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 87 tools (33 base + memory/skill/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 |
|
||||
|
||||
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`): 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.
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 15 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -332,7 +332,7 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| 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` |
|
||||
| Auto-Combo (9-factor scoring, 15 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` |
|
||||
@@ -353,6 +353,7 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| 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 (35 gates, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -373,9 +374,10 @@ 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 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 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.
|
||||
|
||||
**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.
|
||||
@@ -402,14 +404,14 @@ git push -u origin feat/your-feature
|
||||
**Husky hooks**:
|
||||
|
||||
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
|
||||
- **pre-push**: `npm run test:unit`
|
||||
- **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).
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
- **Runtime**: Node.js ≥20.20.2 <21 || ≥22.22.2 <23 || ≥24 <25, ES Modules
|
||||
- **TypeScript**: 5.9+, target ES2022, module esnext, resolution bundler
|
||||
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules
|
||||
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
|
||||
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
|
||||
- **Default port**: 20128 (API + dashboard on same port)
|
||||
- **Data directory**: `DATA_DIR` env var, defaults to `~/.omniroute/`
|
||||
@@ -418,6 +420,29 @@ git push -u origin feat/your-feature
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates & Ratchets
|
||||
|
||||
OmniRoute has **35 CI quality gates** wired across 6 jobs in `.github/workflows/ci.yml`.
|
||||
Full inventory, per-job breakdown, and operational procedures are in
|
||||
[`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md).
|
||||
|
||||
**Quick reference:**
|
||||
|
||||
- Gates in job `lint` (18 checks) + `docs-sync-strict` (12 checks): 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
|
||||
@@ -428,7 +453,7 @@ git push -u origin feat/your-feature
|
||||
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 stay ≥60% (statements, lines, functions, branches).
|
||||
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`.
|
||||
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`.
|
||||
@@ -444,10 +469,13 @@ git push -u origin feat/your-feature
|
||||
## 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.
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -124,6 +124,14 @@ FROM runner-base AS runner-web
|
||||
|
||||
USER root
|
||||
|
||||
# Copy playwright and playwright-core from the builder stage.
|
||||
# The slim runtime image does not have playwright in node_modules, so npx falls
|
||||
# back to a registry download — unreliable on CI runners (exits 127 on failure).
|
||||
# Copying from the builder avoids any network access at image-build time and also
|
||||
# ensures the same playwright version is available at runtime for web-session providers.
|
||||
COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core
|
||||
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
|
||||
|
||||
# Install Playwright browser binaries + OS dependencies under root, then hand
|
||||
# ownership of the browsers cache to the node user.
|
||||
# PLAYWRIGHT_BROWSERS_PATH overrides the default ~/.cache/ms-playwright so the
|
||||
@@ -133,7 +141,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
||||
apt-get update \
|
||||
&& npx playwright install chromium --with-deps \
|
||||
&& node node_modules/playwright/cli.js install chromium --with-deps \
|
||||
&& chown -R node:node /home/node/.cache \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
148
README.md
148
README.md
@@ -23,7 +23,7 @@
|
||||
[](#-177-ai-providers--50-free)
|
||||
[](docs/reference/FREE_TIERS.md)
|
||||
[](#%EF%B8%8F-save-1595-tokens--automatically)
|
||||
[](#-combos--the-flagship)
|
||||
[](#-combos--the-flagship)
|
||||
[](#-quick-start)
|
||||
|
||||
<br/>
|
||||
@@ -33,9 +33,9 @@
|
||||
[](https://discord.gg/hmexnhgE)
|
||||
[](https://t.me/omnirouteOficial)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
[](https://chat.whatsapp.com/CeGCxdFzqBe5Uki288wOvf)
|
||||
[](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)**
|
||||
**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/BTGJXIyjeNIIgExvTMGGhI)**
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
[](https://www.npmjs.com/package/omniroute)
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
[](https://github.com/diegosouzapw/OmniRoute)
|
||||
|
||||
<div align="center">
|
||||
@@ -147,7 +147,7 @@
|
||||
<tr>
|
||||
<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 (37 tools), A2A, memory, guardrails, evals. 4,690+ tests.</sub></td>
|
||||
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (87 tools), A2A, memory, guardrails, evals. 4,690+ tests.</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ OmniRoute — Smart Router │
|
||||
│ RTK + Caveman compression · 14 routing strategies │
|
||||
│ RTK + Caveman compression · 15 routing strategies │
|
||||
│ Circuit breakers · TLS stealth · MCP · A2A · Guardrails │
|
||||
└─────────────────────────┬──────────────────────────────────┘
|
||||
┌─────────────┬────┴────────┬─────────────┐
|
||||
@@ -219,7 +219,7 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
|
||||
|
||||
##
|
||||
|
||||
### 🔀 Or build your own — 14 routing strategies
|
||||
### 🔀 Or build your own — 15 routing strategies
|
||||
|
||||
| Goal | Strategy / combo |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
@@ -265,10 +265,10 @@ Result: 4 layers of fallback = zero downtime
|
||||
| -------------------------------------- | ----------------------------------------------------------- | ------------- |
|
||||
| 🌐 Providers | **177** | 20–100 |
|
||||
| 🆓 Free providers | **50+ (11 free forever)** | 1–5 |
|
||||
| 🔀 Routing strategies | **14** (priority, weighted, cost-optimized, context-relay…) | 1–3 |
|
||||
| 🔀 Routing strategies | **15** (priority, weighted, cost-optimized, context-relay…) | 1–3 |
|
||||
| 🗜️ Token compression | **RTK + Caveman stacked (15–95%)** | None / 20–40% |
|
||||
| 🧰 Built-in MCP server | **37 tools, 3 transports, 13 scopes** | Rare |
|
||||
| 🤝 A2A agent protocol | **5 skills, JSON-RPC 2.0** | None |
|
||||
| 🧰 Built-in MCP server | **87 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, Devin, Jules** | None |
|
||||
@@ -420,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 — **37 tools**, 13 scopes, full audit trail |
|
||||
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **87 tools**, 30 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, 5 skills |
|
||||
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 6 skills |
|
||||
|
||||
```bash
|
||||
# Give Claude Code the full OmniRoute toolset over MCP:
|
||||
@@ -468,7 +468,7 @@ claude mcp add-server omniroute --type http --url http://localhost:20128/api/mcp
|
||||
|
||||
<br/>
|
||||
|
||||
### 📖 How it works — pipeline, architecture & savings math</b></summary>
|
||||
### 📖 How it works — pipeline, architecture & savings math
|
||||
|
||||
```
|
||||
Client (10,000 tok) ──▶ OmniRoute Compression (7 options) ──▶ Provider (~1,080 tok, up to 95% saved)
|
||||
@@ -523,9 +523,22 @@ curl http://localhost:20128/v1/models -H "Authorization: Bearer YOUR_KEY"
|
||||
|
||||
You should see your connected models listed. 🎉 That's it — start coding, and OmniRoute auto-routes & falls back for you.
|
||||
|
||||
If your client cannot send custom headers, OmniRoute also exposes tokenized compatibility aliases:
|
||||
|
||||
```txt
|
||||
OpenAI catalog: http://localhost:20128/vscode/YOUR_KEY/
|
||||
OpenAI models: http://localhost:20128/vscode/YOUR_KEY/models
|
||||
OpenAI chat: http://localhost:20128/vscode/YOUR_KEY/chat/completions
|
||||
OpenAI responses: http://localhost:20128/vscode/YOUR_KEY/responses
|
||||
Ollama chat: http://localhost:20128/vscode/YOUR_KEY/api/chat
|
||||
Ollama tags: http://localhost:20128/vscode/YOUR_KEY/api/tags
|
||||
```
|
||||
|
||||
Use these only for clients that cannot attach `Authorization: Bearer ...`. Header auth remains the preferred mode.
|
||||
|
||||
<br/>
|
||||
|
||||
## 📦 More install methods — Docker, source, pnpm, Arch</b></summary>
|
||||
## 📦 More install methods — Docker, source, pnpm, Arch
|
||||
|
||||
**🐳 Docker**
|
||||
|
||||
@@ -582,8 +595,6 @@ podman compose --profile base up -d
|
||||
|
||||
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
@@ -696,11 +707,13 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
|
||||
<br/>
|
||||
|
||||
**Routing:** 14 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
|
||||
**Routing:** 15 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 (37 tools, 3 transports, 13 scopes) · A2A (JSON-RPC 2.0, SSE, skills) · ACP · cloud agents (Codex, Devin, Jules).
|
||||
**Protocols:** MCP (87 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, 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).
|
||||
**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 `skills/omniroute/SKILL.md`. 10 skills available.
|
||||
**AI Agent Skills:** drop-in markdown manifests — point any agent at a `skills/*/SKILL.md` manifest. 43 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)
|
||||
|
||||
@@ -738,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 `>=20.20.2 <21`, `>=22.22.2 <23`, or `>=24 <25` |
|
||||
| Node runtime errors | Use Node `>=22.0.0 <23` or `>=24.0.0 <27` |
|
||||
|
||||
🐛 **Reporting a bug?** Run `npm run system-info` and attach `system-info.txt`. 📖 [`docs/guides/TROUBLESHOOTING.md`](docs/guides/TROUBLESHOOTING.md)
|
||||
|
||||
@@ -782,8 +795,8 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
|
||||
</div>
|
||||
|
||||
- **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)
|
||||
- **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)
|
||||
- **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)
|
||||
@@ -808,61 +821,62 @@ 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 |
|
||||
| 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 |
|
||||
| [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) | 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 |
|
||||
| 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 |
|
||||
|
||||
### 🤖 Protocols & APIs
|
||||
|
||||
| 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 |
|
||||
| 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) | 87 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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
|
||||
| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 4,690+ test suite |
|
||||
|
||||
@@ -1000,6 +1014,14 @@ Special thanks to **[RTK - Rust Token Killer](https://github.com/rtk-ai/rtk)** b
|
||||
|
||||
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.
|
||||
|
||||
## ❤️ 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)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
@@ -1010,7 +1032,7 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
**[⬆ Back to top](#-omniroute)** · Built with ❤️ for the open-source AI community.
|
||||
|
||||
<sub>OmniRoute v3.8.6 · Node ≥22.22.2 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
|
||||
<sub>OmniRoute v3.8.24 · Node ≥22.0.0 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
|
||||
|
||||
</div>
|
||||
<!-- GitHub Discussions enabled for community Q&A -->
|
||||
|
||||
70
bin/cli/api-commands/agent-skills.mjs
Normal file
70
bin/cli/api-commands/agent-skills.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
155
bin/cli/api-commands/agentbridge.mjs
Normal file
155
bin/cli/api-commands/agentbridge.mjs
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,26 +26,16 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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) => {
|
||||
@@ -40,12 +33,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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")
|
||||
@@ -42,17 +35,11 @@ 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) => {
|
||||
@@ -64,12 +51,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,82 +5,56 @@ 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) => {
|
||||
@@ -92,45 +66,29 @@ 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) => {
|
||||
@@ -142,31 +100,20 @@ 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) => {
|
||||
@@ -178,45 +125,29 @@ 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) => {
|
||||
@@ -228,45 +159,29 @@ 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) => {
|
||||
@@ -278,17 +193,11 @@ 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) => {
|
||||
@@ -300,45 +209,29 @@ 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) => {
|
||||
@@ -350,45 +243,29 @@ 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) => {
|
||||
@@ -400,45 +277,29 @@ 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) => {
|
||||
@@ -450,45 +311,29 @@ 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) => {
|
||||
@@ -500,26 +345,16 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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) => {
|
||||
@@ -40,17 +33,11 @@ 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) => {
|
||||
@@ -62,31 +49,20 @@ 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) => {
|
||||
@@ -98,12 +74,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,68 +26,43 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,17 +26,11 @@ 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) => {
|
||||
@@ -54,59 +42,38 @@ 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) => {
|
||||
@@ -118,31 +85,20 @@ 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) => {
|
||||
@@ -154,28 +110,18 @@ 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);
|
||||
});
|
||||
|
||||
202
bin/cli/api-commands/embedded-services.mjs
Normal file
202
bin/cli/api-commands/embedded-services.mjs
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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")
|
||||
@@ -42,12 +35,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,17 +26,11 @@ 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) => {
|
||||
@@ -54,12 +42,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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")
|
||||
@@ -42,12 +35,7 @@ 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);
|
||||
});
|
||||
|
||||
251
bin/cli/api-commands/memory.mjs
Normal file
251
bin/cli/api-commands/memory.mjs
Normal file
@@ -0,0 +1,251 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,17 +17,11 @@ 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) => {
|
||||
@@ -40,12 +33,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,55 +5,36 @@ 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) => {
|
||||
@@ -65,45 +46,29 @@ 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")
|
||||
@@ -117,12 +82,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,12 +17,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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>", "")
|
||||
@@ -15,44 +14,29 @@ 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) => {
|
||||
@@ -64,45 +48,29 @@ 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) => {
|
||||
@@ -114,31 +82,20 @@ 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) => {
|
||||
@@ -150,12 +107,7 @@ 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);
|
||||
});
|
||||
|
||||
83
bin/cli/api-commands/playground.mjs
Normal file
83
bin/cli/api-commands/playground.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
@@ -5,59 +5,39 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,87 +5,57 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,31 +26,20 @@ 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) => {
|
||||
@@ -68,96 +51,97 @@ 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 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 data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
|
||||
154
bin/cli/api-commands/quota.mjs
Normal file
154
bin/cli/api-commands/quota.mjs
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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_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);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
// 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";
|
||||
@@ -19,37 +21,17 @@ 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 = [
|
||||
"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 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 function registerApiCommands(program) {
|
||||
const api = program
|
||||
@@ -58,9 +40,9 @@ export function registerApiCommands(program) {
|
||||
api
|
||||
.command("tags")
|
||||
.description("List available API tag groups")
|
||||
.action(() => {
|
||||
API_TAGS.forEach((t) => console.log(t));
|
||||
});
|
||||
.action(() => { API_TAGS.forEach((t) => console.log(t)); });
|
||||
register_playground(api);
|
||||
register_memory(api);
|
||||
register_chat(api);
|
||||
register_messages(api);
|
||||
register_responses(api);
|
||||
@@ -81,8 +63,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,12 +17,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ 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) => {
|
||||
@@ -18,12 +17,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,22 +5,16 @@ 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) => {
|
||||
@@ -32,31 +26,20 @@ 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) => {
|
||||
@@ -68,101 +51,65 @@ 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) => {
|
||||
@@ -174,31 +121,20 @@ 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) => {
|
||||
@@ -210,31 +146,20 @@ 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) => {
|
||||
@@ -246,31 +171,20 @@ 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) => {
|
||||
@@ -282,12 +196,32 @@ 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 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 data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
|
||||
@@ -5,36 +5,25 @@ 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) => {
|
||||
@@ -46,157 +35,101 @@ 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) => {
|
||||
@@ -208,143 +141,92 @@ 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) => {
|
||||
@@ -356,47 +238,31 @@ 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) => {
|
||||
@@ -408,58 +274,46 @@ 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);
|
||||
});
|
||||
|
||||
@@ -5,31 +5,21 @@ 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);
|
||||
});
|
||||
|
||||
307
bin/cli/api-commands/traffic-inspector.mjs
Normal file
307
bin/cli/api-commands/traffic-inspector.mjs
Normal file
@@ -0,0 +1,307 @@
|
||||
// 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_traffic_inspector(parent) {
|
||||
const tag = parent.command("traffic-inspector").description("Traffic Inspector endpoints");
|
||||
tag.command("get-api-tools-traffic-inspector-requests")
|
||||
.description("List intercepted requests (filterable)")
|
||||
.option("--profile <profile>", "")
|
||||
.option("--host <host>", "")
|
||||
.option("--agent <agent>", "")
|
||||
.option("--status <status>", "")
|
||||
.option("--source <source>", "")
|
||||
.option("--session-id <sessionId>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/requests";
|
||||
const qs = new URLSearchParams();
|
||||
if (opts.profile != null) qs.set("profile", String(opts.profile));
|
||||
if (opts.host != null) qs.set("host", String(opts.host));
|
||||
if (opts.agent != null) qs.set("agent", String(opts.agent));
|
||||
if (opts.status != null) qs.set("status", String(opts.status));
|
||||
if (opts.source != null) qs.set("source", String(opts.source));
|
||||
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
|
||||
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("delete-api-tools-traffic-inspector-requests")
|
||||
.description("Clear the in-memory traffic buffer")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/requests";
|
||||
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-tools-traffic-inspector-requests-id-")
|
||||
.description("Get a single intercepted request by ID")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/requests/{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("post-api-tools-traffic-inspector-requests-id-replay")
|
||||
.description("Replay a captured request through OmniRoute router")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/requests/{id}/replay";
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
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("put-api-tools-traffic-inspector-requests-id-annotation")
|
||||
.description("Save or update annotation on a request")
|
||||
.requiredOption("--id <id>", "")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/requests/{id}/annotation";
|
||||
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
|
||||
let body;
|
||||
if (opts.body) {
|
||||
body = opts.body.startsWith("@")
|
||||
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
|
||||
: JSON.parse(opts.body);
|
||||
}
|
||||
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
tag.command("get-api-tools-traffic-inspector-ws")
|
||||
.description("Live WebSocket stream of intercepted requests")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/ws";
|
||||
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-traffic-inspector-export-har")
|
||||
.description("Export current filtered request list as HAR 1.2")
|
||||
.option("--profile <profile>", "")
|
||||
.option("--session-id <sessionId>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/export.har";
|
||||
const qs = new URLSearchParams();
|
||||
if (opts.profile != null) qs.set("profile", String(opts.profile));
|
||||
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
|
||||
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-tools-traffic-inspector-hosts")
|
||||
.description("List custom capture hosts")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/hosts";
|
||||
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-traffic-inspector-hosts")
|
||||
.description("Add a custom capture host (edits /etc/hosts)")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/hosts";
|
||||
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("delete-api-tools-traffic-inspector-hosts-host-")
|
||||
.description("Remove a custom capture host")
|
||||
.requiredOption("--host <host>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/hosts/{host}";
|
||||
url = url.replace("{host}", encodeURIComponent(opts.host ?? ""));
|
||||
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("patch-api-tools-traffic-inspector-hosts-host-")
|
||||
.description("Toggle enabled state of a custom host")
|
||||
.requiredOption("--host <host>", "")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/hosts/{host}";
|
||||
url = url.replace("{host}", encodeURIComponent(opts.host ?? ""));
|
||||
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("get-api-tools-traffic-inspector-capture-modes")
|
||||
.description("Get state of all 4 capture modes")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/capture-modes";
|
||||
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-traffic-inspector-capture-modes-http-proxy")
|
||||
.description("Start or stop the HTTP_PROXY listener (port 8080)")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/capture-modes/http-proxy";
|
||||
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-traffic-inspector-capture-modes-system-proxy")
|
||||
.description("Apply or revert system-wide proxy settings")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/capture-modes/system-proxy";
|
||||
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-traffic-inspector-capture-modes-tls-intercept")
|
||||
.description("Toggle TLS body decryption in HTTP_PROXY mode")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/capture-modes/tls-intercept";
|
||||
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-traffic-inspector-sessions")
|
||||
.description("List all saved recording sessions")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions";
|
||||
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-traffic-inspector-sessions")
|
||||
.description("Start a new recording session")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions";
|
||||
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-traffic-inspector-sessions-id-")
|
||||
.description("Get session snapshot (all captured requests)")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions/{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-tools-traffic-inspector-sessions-id-")
|
||||
.description("Stop or rename a recording session")
|
||||
.requiredOption("--id <id>", "")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions/{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-tools-traffic-inspector-sessions-id-")
|
||||
.description("Delete a recording session")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions/{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-tools-traffic-inspector-sessions-id-export-har")
|
||||
.description("Export a recorded session as HAR 1.2")
|
||||
.requiredOption("--id <id>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/sessions/{id}/export.har";
|
||||
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("post-api-tools-traffic-inspector-internal-ingest")
|
||||
.description("Internal ingest endpoint for server.cjs passthrough path")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/tools/traffic-inspector/internal/ingest";
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
export function register_translator(parent) {
|
||||
const tag = parent.command("translator").description("Translator endpoints");
|
||||
tag
|
||||
.command("post-api-translator-detect")
|
||||
tag.command("post-api-translator-detect")
|
||||
.description("Detect request format")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
@@ -18,17 +17,11 @@ export function register_translator(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-translator-translate")
|
||||
tag.command("post-api-translator-translate")
|
||||
.description("Translate between formats")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
@@ -40,17 +33,11 @@ export function register_translator(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-translator-send")
|
||||
tag.command("post-api-translator-send")
|
||||
.description("Send translated request to provider")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
@@ -62,26 +49,16 @@ export function register_translator(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-translator-history")
|
||||
tag.command("get-api-translator-history")
|
||||
.description("Get translation history")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/translator/history";
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
export function register_usage(parent) {
|
||||
const tag = parent.command("usage").description("Usage endpoints");
|
||||
tag
|
||||
.command("get-api-usage-analytics")
|
||||
tag.command("get-api-usage-analytics")
|
||||
.description("Get usage analytics")
|
||||
.option("--period <period>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
@@ -15,16 +14,11 @@ export function register_usage(parent) {
|
||||
const qs = new URLSearchParams();
|
||||
if (opts.period != null) qs.set("period", String(opts.period));
|
||||
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-usage-call-logs")
|
||||
tag.command("get-api-usage-call-logs")
|
||||
.description("Get call logs")
|
||||
.option("--limit <limit>", "")
|
||||
.option("--offset <offset>", "")
|
||||
@@ -35,116 +29,76 @@ export function register_usage(parent) {
|
||||
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-usage-call-logs-id-")
|
||||
tag.command("get-api-usage-call-logs-id-")
|
||||
.description("Get a specific call log")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/call-logs/{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("get-api-usage-connection-id-")
|
||||
tag.command("get-api-usage-connection-id-")
|
||||
.description("Get usage for a specific connection")
|
||||
.requiredOption("--connection-id <connectionId>", "")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/{connectionId}";
|
||||
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
|
||||
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-usage-history")
|
||||
tag.command("get-api-usage-history")
|
||||
.description("Get usage history")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/history";
|
||||
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-usage-logs")
|
||||
tag.command("get-api-usage-logs")
|
||||
.description("Get usage logs")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/logs";
|
||||
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-usage-proxy-logs")
|
||||
tag.command("get-api-usage-proxy-logs")
|
||||
.description("Get proxy logs")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/proxy-logs";
|
||||
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-usage-request-logs")
|
||||
tag.command("get-api-usage-request-logs")
|
||||
.description("Get request logs")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/request-logs";
|
||||
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-usage-budget")
|
||||
tag.command("get-api-usage-budget")
|
||||
.description("Get usage budget status")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/usage/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("post-api-usage-budget")
|
||||
tag.command("post-api-usage-budget")
|
||||
.description("Configure usage budget")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
@@ -156,12 +110,7 @@ export function register_usage(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);
|
||||
});
|
||||
|
||||
@@ -6,8 +6,13 @@ export function registerAutostart(program) {
|
||||
.command("autostart")
|
||||
.description(t("autostart.description") || "Manage OmniRoute autostart at login");
|
||||
|
||||
// #3331 — autostart could previously only be toggled from the tray
|
||||
// (`serve --tray`) or the Electron Appearance tab; a plain `omniroute serve`
|
||||
// user had no path. These subcommands (with `on`/`off`/`true`/`false`
|
||||
// aliases, e.g. `omniroute autostart on`) make it a first-class CLI action.
|
||||
cmd
|
||||
.command("enable")
|
||||
.aliases(["on", "true"])
|
||||
.description(t("autostart.enable") || "Enable autostart at login")
|
||||
.action(async (opts, c) => {
|
||||
const globalOpts = c.optsWithGlobals();
|
||||
@@ -19,6 +24,7 @@ export function registerAutostart(program) {
|
||||
|
||||
cmd
|
||||
.command("disable")
|
||||
.aliases(["off", "false"])
|
||||
.description(t("autostart.disable") || "Disable autostart at login")
|
||||
.action(async (opts, c) => {
|
||||
const globalOpts = c.optsWithGlobals();
|
||||
@@ -29,7 +35,19 @@ export function registerAutostart(program) {
|
||||
});
|
||||
|
||||
cmd
|
||||
.command("status")
|
||||
.command("toggle")
|
||||
.description(t("autostart.toggle") || "Toggle autostart at login")
|
||||
.action(async (opts, c) => {
|
||||
const globalOpts = c.optsWithGlobals();
|
||||
const { enable, disable, isAutostartEnabled } = await import("../tray/autostart.mjs");
|
||||
const next = !isAutostartEnabled();
|
||||
const ok = next ? enable() : disable();
|
||||
emit(next ? { enabled: ok } : { disabled: ok }, globalOpts);
|
||||
if (!ok) process.exit(1);
|
||||
});
|
||||
|
||||
cmd
|
||||
.command("status", { isDefault: true })
|
||||
.description(t("autostart.status") || "Show autostart status")
|
||||
.action(async (opts, c) => {
|
||||
const globalOpts = c.optsWithGlobals();
|
||||
|
||||
384
bin/cli/commands/setup-open-code.mjs
Normal file
384
bin/cli/commands/setup-open-code.mjs
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* omniroute setup opencode — Wire the bundled @omniroute/opencode-plugin
|
||||
* into a local OpenCode install.
|
||||
*
|
||||
* Closes the gap where `npm install -g omniroute` ships the plugin
|
||||
* inside the omniroute package (`@omniroute/opencode-plugin/dist/`) but
|
||||
* OpenCode discovers plugins via `~/.config/opencode/plugins/` or
|
||||
* via entries in `opencode.json`. Without this command, the user has
|
||||
* to extract the tarball and wire it up by hand (see the plugin README,
|
||||
* "Install" section).
|
||||
*
|
||||
* What it does, in order:
|
||||
* 1. Resolves the bundled plugin path (source + built dist).
|
||||
* 2. Resolves the OpenCode config directory (XDG-aware).
|
||||
* 3. Copies the built plugin into `<opencode>/plugins/omniroute/`.
|
||||
* 4. Creates or updates `opencode.json` with a single `plugin` entry
|
||||
* pointing at the local copy (so OC ≥1.15 picks it up).
|
||||
* 5. Optionally runs `opencode auth login --provider omniroute`
|
||||
* so the next `opencode` invocation already has the API key.
|
||||
*
|
||||
* Idempotent: re-running with the same `--provider-id` updates the
|
||||
* entry in place (path + baseURL) without duplicating it.
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import os from "node:os";
|
||||
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// We walk up from this file to find the omniroute package root. The script
|
||||
// lives at `<omniroute>/bin/cli/commands/setup-open-code.mjs`, so the
|
||||
// package root is three levels up. Using import.meta.url (not process.cwd())
|
||||
// means the command works the same way whether you run it from the source
|
||||
// repo, a global install, or a symlinked location.
|
||||
const PACKAGE_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
|
||||
// The bundled plugin ships at PACKAGE_ROOT/@omniroute/opencode-plugin/
|
||||
// (see root package.json `files`: ["@omniroute/", ...]). The env override
|
||||
// exists so tests can point at a fixture without building the real plugin.
|
||||
const BUNDLED_PLUGIN_DIR =
|
||||
process.env.OMNIROUTE_OPENCODE_PLUGIN_DIR ||
|
||||
join(PACKAGE_ROOT, "@omniroute", "opencode-plugin");
|
||||
|
||||
/**
|
||||
* Resolve the OpenCode config directory. Honours XDG_CONFIG_HOME and the
|
||||
* platform-specific defaults documented at https://opencode.ai/.
|
||||
*
|
||||
* @returns {{ configDir: string, dataDir: string }}
|
||||
*/
|
||||
function resolveOpenCodeDirs() {
|
||||
const home = os.homedir();
|
||||
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
||||
const xdgData = process.env.XDG_DATA_HOME;
|
||||
const platform = process.platform;
|
||||
|
||||
let configDir;
|
||||
let dataDir;
|
||||
if (platform === "darwin") {
|
||||
// macOS: ~/Library/Application Support/opencode
|
||||
configDir = join(home, "Library", "Application Support", "opencode");
|
||||
dataDir = configDir; // OC uses the same root for config + data on macOS
|
||||
} else if (platform === "win32") {
|
||||
const appdata = process.env.APPDATA || join(home, "AppData", "Roaming");
|
||||
const localAppdata = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
||||
configDir = join(appdata, "opencode");
|
||||
dataDir = join(localAppdata, "opencode");
|
||||
} else {
|
||||
// Linux + everything else: XDG-style
|
||||
configDir = xdgConfig ? join(xdgConfig, "opencode") : join(home, ".config", "opencode");
|
||||
dataDir = xdgData ? join(xdgData, "opencode") : join(home, ".local", "share", "opencode");
|
||||
}
|
||||
return { configDir, dataDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the bundled @omniroute/opencode-plugin dist. The plugin may be
|
||||
* present in two states:
|
||||
*
|
||||
* - Built (`dist/index.cjs` + `dist/index.js` exist) — preferred,
|
||||
* ships from a published omniroute tarball after Step 8.8 of
|
||||
* `scripts/build/prepublish.ts` runs.
|
||||
* - Unbuilt (only `src/index.ts`) — local dev / fresh clone. We surface
|
||||
* a clear error instead of running tsup here, because the CLI runtime
|
||||
* may not have tsup available (it's a devDependency).
|
||||
*
|
||||
* @returns {{ distEntry: string, cjsEntry: string, packageDir: string }}
|
||||
*/
|
||||
function resolveBundledPlugin() {
|
||||
if (!existsSync(BUNDLED_PLUGIN_DIR)) {
|
||||
throw new Error(
|
||||
`Bundled @omniroute/opencode-plugin not found at ${BUNDLED_PLUGIN_DIR}.\n` +
|
||||
`This usually means omniroute was installed from a source tree that does not ` +
|
||||
`include the workspace package. Try reinstalling omniroute (npm install -g omniroute) ` +
|
||||
`or run \`cd @omniroute/opencode-plugin && npm install && npm run build\` from the source repo.`
|
||||
);
|
||||
}
|
||||
|
||||
const esmEntry = join(BUNDLED_PLUGIN_DIR, "dist", "index.js");
|
||||
const cjsEntry = join(BUNDLED_PLUGIN_DIR, "dist", "index.cjs");
|
||||
|
||||
if (!existsSync(esmEntry) || !existsSync(cjsEntry)) {
|
||||
throw new Error(
|
||||
`@omniroute/opencode-plugin dist/ not built (looked for ${esmEntry}).\n` +
|
||||
`Run \`cd ${BUNDLED_PLUGIN_DIR} && npm install && npm run build\` and re-run this command.`
|
||||
);
|
||||
}
|
||||
|
||||
// Prefer ESM. OpenCode (≥1.15) loads ESM modules natively.
|
||||
return { distEntry: esmEntry, cjsEntry, packageDir: BUNDLED_PLUGIN_DIR };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the plugin package into `<opencodeConfig>/plugins/omniroute/`. We
|
||||
* copy the entire package (dist/ + package.json) so the dist file's
|
||||
* require/import of `zod` and `@opencode-ai/plugin` resolves against the
|
||||
* copy's own node_modules. Without the copy, OpenCode would need to
|
||||
* resolve the peer deps from the omniroute package's tree, which is
|
||||
* unreliable.
|
||||
*/
|
||||
function installPluginToOpenCode(pluginInfo, opencodeConfigDir) {
|
||||
const targetDir = join(opencodeConfigDir, "plugins", "omniroute");
|
||||
mkdirSync(dirname(targetDir), { recursive: true });
|
||||
mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
// Copy package.json + dist/. We intentionally do NOT recursively copy
|
||||
// node_modules from the source — `peerDependenciesMeta` declares zod +
|
||||
// @opencode-ai/plugin as peers, and the user's OpenCode install already
|
||||
// provides them. Copying our own node_modules would risk duplicate zod
|
||||
// instances (the @opencode-ai/plugin contract uses a singleton).
|
||||
const packageJsonSrc = join(pluginInfo.packageDir, "package.json");
|
||||
const distSrc = join(pluginInfo.packageDir, "dist");
|
||||
cpSync(packageJsonSrc, join(targetDir, "package.json"));
|
||||
cpSync(distSrc, join(targetDir, "dist"), { recursive: true });
|
||||
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update `opencode.json` to register the plugin. Idempotent: if an entry
|
||||
* for the same `providerId` already exists, replace it in place. If the
|
||||
* user has any other plugin entries, preserve them.
|
||||
*
|
||||
* @returns {{ configPath: string, changed: boolean }}
|
||||
*/
|
||||
function registerPluginInOpenCodeConfig({
|
||||
opencodeConfigDir,
|
||||
pluginTargetDir,
|
||||
providerId,
|
||||
baseURL,
|
||||
displayName,
|
||||
}) {
|
||||
const configPath = join(opencodeConfigDir, "opencode.json");
|
||||
let cfg = {};
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to parse existing ${configPath}: ${err.message}\n` +
|
||||
`Fix or remove the file manually, then re-run \`omniroute setup opencode\`.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = Array.isArray(cfg.plugin) ? cfg.plugin : [];
|
||||
|
||||
// Plugin entries can be either a string ("@some/pkg") or a tuple
|
||||
// ("@some/pkg", { options }). The README documents the tuple form, so
|
||||
// we use that. The "module path" is a file:// URL relative to the
|
||||
// opencode config dir — that is what opencode ≥1.15 resolves.
|
||||
const entry = [
|
||||
`./plugins/omniroute/dist/index.js`,
|
||||
{
|
||||
providerId,
|
||||
baseURL,
|
||||
...(displayName ? { displayName } : {}),
|
||||
},
|
||||
];
|
||||
|
||||
// Idempotency: drop any prior entry for the same providerId. We also
|
||||
// drop a legacy `opencode-omniroute-auth` entry if present — that
|
||||
// package is the obsolete predecessor of @omniroute/opencode-plugin
|
||||
// and was the root cause of issue #3711.
|
||||
const filtered = plugins.filter((p) => {
|
||||
if (typeof p === "string") {
|
||||
return !p.includes("opencode-omniroute-auth");
|
||||
}
|
||||
if (Array.isArray(p) && p[1] && typeof p[1] === "object") {
|
||||
const pid = p[1].providerId;
|
||||
if (pid === providerId) return false;
|
||||
// Also drop the legacy auth plugin if it's there.
|
||||
if (typeof p[0] === "string" && p[0].includes("opencode-omniroute-auth")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
filtered.push(entry);
|
||||
cfg.plugin = filtered;
|
||||
|
||||
// Make sure the config dir exists, then write the updated config.
|
||||
mkdirSync(dirname(configPath), { recursive: true });
|
||||
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
||||
|
||||
return { configPath, changed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally invoke `opencode auth login --provider <providerId>`. We
|
||||
* shell out (instead of importing) so this command works even if
|
||||
* OpenCode's CLI surface shifts between minor versions — the user gets
|
||||
* a clear "could not run opencode" message instead of a hard import
|
||||
* failure.
|
||||
*/
|
||||
function runOpenCodeAuth(providerId) {
|
||||
const isWin = process.platform === "win32";
|
||||
const opencodeBin = isWin ? "opencode.cmd" : "opencode";
|
||||
const res = spawnSync(opencodeBin, ["auth", "login", "--provider", providerId], {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
});
|
||||
if (res.error) {
|
||||
// ENOENT = opencode is not on PATH
|
||||
if (res.error.code === "ENOENT") {
|
||||
printInfo(
|
||||
`opencode CLI not found on PATH. Run \`opencode auth login --provider ${providerId}\` manually after installing OpenCode.`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
printError(`opencode auth login failed: ${res.error.message}`);
|
||||
return 1;
|
||||
}
|
||||
return typeof res.status === "number" ? res.status : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level action handler. Kept exported so the integration test can
|
||||
* drive it without spawning a subprocess.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} [opts.providerId="omniroute"]
|
||||
* @param {string} [opts.baseURL="http://localhost:20128"] (Commander camelCases
|
||||
* `--base-url` into `baseUrl`, so both spellings are accepted.)
|
||||
* @param {string} [opts.configDir] Override the OpenCode config dir (tests / non-standard installs).
|
||||
* @param {string} [opts.displayName]
|
||||
* @param {boolean} [opts.auth=false] Run `opencode auth login` after wiring.
|
||||
* @param {boolean} [opts.nonInteractive=false] Skip prompts.
|
||||
* @returns {Promise<{ exitCode: number, configPath?: string, pluginTargetDir?: string }>}
|
||||
*/
|
||||
export async function runSetupOpenCodeCommand(opts = {}) {
|
||||
const providerId = opts.providerId || "omniroute";
|
||||
const baseURL = opts.baseURL || opts.baseUrl || "http://localhost:20128";
|
||||
const displayName = opts.displayName || null;
|
||||
const wantsAuth = Boolean(opts.auth);
|
||||
const nonInteractive = Boolean(opts.nonInteractive);
|
||||
|
||||
printHeading("OmniRoute → OpenCode Plugin Setup");
|
||||
|
||||
const resolvedDirs = resolveOpenCodeDirs();
|
||||
const opencodeConfigDir = opts.configDir || resolvedDirs.configDir;
|
||||
const opencodeDataDir = resolvedDirs.dataDir;
|
||||
printInfo(`OpenCode config dir: ${opencodeConfigDir}`);
|
||||
printInfo(`OpenCode data dir: ${opencodeDataDir}`);
|
||||
|
||||
// 1. Resolve bundled plugin
|
||||
let pluginInfo;
|
||||
try {
|
||||
pluginInfo = resolveBundledPlugin();
|
||||
} catch (err) {
|
||||
printError(err.message);
|
||||
return { exitCode: 1 };
|
||||
}
|
||||
printInfo(`Bundled plugin: ${pluginInfo.distEntry}`);
|
||||
|
||||
// 2. Ensure OpenCode config dir exists (opencode will create it on
|
||||
// first run, but creating it now means we can write opencode.json
|
||||
// even if OC has never been launched).
|
||||
if (!existsSync(opencodeConfigDir)) {
|
||||
mkdirSync(opencodeConfigDir, { recursive: true });
|
||||
printInfo(`Created OpenCode config dir (didn't exist yet).`);
|
||||
}
|
||||
|
||||
// 3. Copy plugin into OpenCode's plugin dir
|
||||
let pluginTargetDir;
|
||||
try {
|
||||
pluginTargetDir = installPluginToOpenCode(pluginInfo, opencodeConfigDir);
|
||||
printSuccess(`Plugin installed at ${pluginTargetDir}`);
|
||||
} catch (err) {
|
||||
printError(`Failed to install plugin: ${err.message}`);
|
||||
return { exitCode: 1 };
|
||||
}
|
||||
|
||||
// 4. Register in opencode.json
|
||||
let configPath;
|
||||
try {
|
||||
const reg = registerPluginInOpenCodeConfig({
|
||||
opencodeConfigDir,
|
||||
pluginTargetDir,
|
||||
providerId,
|
||||
baseURL,
|
||||
displayName,
|
||||
});
|
||||
configPath = reg.configPath;
|
||||
printSuccess(`opencode.json updated at ${configPath}`);
|
||||
} catch (err) {
|
||||
printError(`Failed to update opencode.json: ${err.message}`);
|
||||
return { exitCode: 1, pluginTargetDir };
|
||||
}
|
||||
|
||||
// 5. Optionally run auth login
|
||||
if (wantsAuth) {
|
||||
if (nonInteractive) {
|
||||
printInfo(`Skipping \`opencode auth login\` (non-interactive mode).`);
|
||||
printInfo(`Run manually: opencode auth login --provider ${providerId}`);
|
||||
} else {
|
||||
printHeading("Authenticating with OpenCode");
|
||||
const authExit = runOpenCodeAuth(providerId);
|
||||
if (authExit !== 0) {
|
||||
return { exitCode: authExit, configPath, pluginTargetDir };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printInfo(
|
||||
`Next step: opencode auth login --provider ${providerId} (pass --auth to do this automatically)`
|
||||
);
|
||||
}
|
||||
|
||||
printSuccess("OpenCode plugin setup complete");
|
||||
printInfo(`Restart OpenCode to pick up the new plugin entry.`);
|
||||
return { exitCode: 0, configPath, pluginTargetDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the `omniroute setup opencode` subcommand on the parent
|
||||
* `setup` command. Commander builds the doc/help from the chain, so
|
||||
* `omniroute setup --help` automatically shows the new subcommand.
|
||||
*
|
||||
* @param {import("commander").Command} setupCommand the registered `setup` command
|
||||
*/
|
||||
export function registerSetupOpenCode(setupCommand) {
|
||||
setupCommand
|
||||
.command("opencode")
|
||||
.description(
|
||||
t("setup.opencode") ||
|
||||
"Install and register the bundled @omniroute/opencode-plugin with a local OpenCode install"
|
||||
)
|
||||
.option(
|
||||
"--provider-id <id>",
|
||||
"OpenCode provider id to register (default: omniroute)",
|
||||
"omniroute"
|
||||
)
|
||||
.option(
|
||||
"--base-url <url>",
|
||||
"OmniRoute base URL the plugin should talk to (default: http://localhost:20128)",
|
||||
"http://localhost:20128"
|
||||
)
|
||||
.option("--display-name <name>", "Display name in the OpenCode UI (optional)")
|
||||
.option(
|
||||
"--auth",
|
||||
"Run `opencode auth login --provider <providerId>` after wiring (interactive)",
|
||||
false
|
||||
)
|
||||
.option("--non-interactive", "Do not prompt; skip the auth login step", false)
|
||||
.action(async (opts, cmd) => {
|
||||
// The parent `setup` command uses cmd.optsWithGlobals(); we mirror
|
||||
// that here so global flags (--json, --base-url, --api-key) still
|
||||
// flow through to the runner.
|
||||
const globalOpts = cmd.parent?.parent?.optsWithGlobals?.() ?? {};
|
||||
const merged = {
|
||||
...opts,
|
||||
output: globalOpts.output,
|
||||
apiKey: opts.apiKey ?? globalOpts.apiKey,
|
||||
baseUrl: opts.baseUrl ?? globalOpts.baseUrl,
|
||||
};
|
||||
const { exitCode } = await runSetupOpenCodeCommand(merged);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getProviderDisplayName,
|
||||
resolveProviderChoice,
|
||||
} from "../provider-catalog.mjs";
|
||||
import { registerSetupOpenCode } from "./setup-open-code.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
@@ -150,6 +151,11 @@ export function registerSetup(program) {
|
||||
const exitCode = await runSetupCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
// Wire up `omniroute setup opencode` subcommand. Kept inside registerSetup
|
||||
// so it always travels with the parent command (avoids a separate register
|
||||
// call in the registry that would silently break if the parent renames).
|
||||
registerSetupOpenCode(program.commands.find((c) => c.name() === "setup"));
|
||||
}
|
||||
|
||||
export async function runSetupCommand(opts = {}) {
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function getCurrentVersion() {
|
||||
// This file lives at <pkgRoot>/bin/cli/commands/update.mjs — resolve package
|
||||
// paths relative to the script, NOT process.cwd(). On a global npm/brew install
|
||||
// the user's cwd is not the package root, so cwd-relative lookups break (#3295).
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PKG_ROOT = path.resolve(SCRIPT_DIR, "..", "..", "..");
|
||||
const BIN_DIR = path.join(PKG_ROOT, "bin");
|
||||
|
||||
export async function getCurrentVersion() {
|
||||
try {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const pkg = JSON.parse(readFileSync(path.join(process.cwd(), "package.json"), "utf-8"));
|
||||
const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
||||
return pkg.version;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -38,12 +46,12 @@ function compareVersions(a, b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function createBackup() {
|
||||
const binPath = path.join(process.cwd(), "bin");
|
||||
export async function createBackup() {
|
||||
const binPath = BIN_DIR;
|
||||
const backupDir = path.join(homedir(), ".omniroute", "backups", `omniroute-${Date.now()}`);
|
||||
|
||||
try {
|
||||
const { mkdirSync, copyFileSync, existsSync } = await import("node:fs");
|
||||
const { mkdirSync, cpSync, existsSync } = await import("node:fs");
|
||||
if (!existsSync(binPath)) return null;
|
||||
|
||||
mkdirSync(backupDir, { recursive: true });
|
||||
@@ -51,7 +59,9 @@ async function createBackup() {
|
||||
for (const f of files) {
|
||||
const src = path.join(binPath, f);
|
||||
if (existsSync(src)) {
|
||||
copyFileSync(src, path.join(backupDir, f));
|
||||
// cpSync handles both files and directories; the old copyFileSync threw
|
||||
// EISDIR on the "cli" directory, which was swallowed by the catch (#3295).
|
||||
cpSync(src, path.join(backupDir, f), { recursive: true });
|
||||
}
|
||||
}
|
||||
return backupDir;
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"testFailed": "Provider test failed: {error}",
|
||||
"loginEnabled": "Login: enabled (password updated)",
|
||||
"loginDisabled": "Login: disabled",
|
||||
"providerInfo": "Provider: {info}"
|
||||
"providerInfo": "Provider: {info}",
|
||||
"opencode": "Install and configure the bundled @omniroute/opencode-plugin for OpenCode"
|
||||
},
|
||||
"doctor": {
|
||||
"title": "OmniRoute Doctor",
|
||||
@@ -1228,6 +1229,7 @@
|
||||
"description": "Manage OmniRoute autostart at boot (Linux: systemd user service)",
|
||||
"enable": "Enable autostart at boot",
|
||||
"disable": "Disable autostart at boot",
|
||||
"toggle": "Toggle autostart at boot",
|
||||
"status": "Show autostart status"
|
||||
},
|
||||
"runtime": {
|
||||
|
||||
@@ -1228,6 +1228,7 @@
|
||||
"description": "Gerenciar inicialização automática do OmniRoute no login",
|
||||
"enable": "Habilitar inicialização automática no login",
|
||||
"disable": "Desabilitar inicialização automática no login",
|
||||
"toggle": "Alternar inicialização automática no login",
|
||||
"status": "Mostrar status de inicialização automática"
|
||||
},
|
||||
"runtime": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,17 +42,20 @@ export class ServerSupervisor {
|
||||
});
|
||||
}
|
||||
|
||||
this.child.on("error", (err) => this.handleExit(err.code ?? -1, err));
|
||||
this.child.on("error", (err) => this.handleExit(-1, err));
|
||||
this.child.on("exit", (code) => this.handleExit(code));
|
||||
|
||||
return this.child;
|
||||
}
|
||||
|
||||
handleExit(code) {
|
||||
// Node.js v24+ requires process.exit() to receive a number. Spawn-error events
|
||||
// deliver err.code (a string like 'ENOENT') via the 'error' listener; normalise here.
|
||||
const exitCode = typeof code === "number" ? code : null;
|
||||
cleanupPidFile("server");
|
||||
|
||||
if (this.isShuttingDown || code === 0) {
|
||||
process.exit(code || 0);
|
||||
if (this.isShuttingDown || exitCode === 0) {
|
||||
process.exit(exitCode ?? 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ export class ServerSupervisor {
|
||||
}
|
||||
}
|
||||
this.dumpCrashLog();
|
||||
process.exit(code ?? 1);
|
||||
process.exit(exitCode ?? 1);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,22 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
function resolveMcpEntry(rootDir = ROOT) {
|
||||
/**
|
||||
* @param {string} [rootDir] - project root directory (defaults to package root)
|
||||
* @param {(path: string) => boolean} [existsSyncFn] - injectable fs.existsSync
|
||||
* for testing; defaults to the real existsSync
|
||||
* @returns {string|null} resolved absolute path to the MCP server entry, or null
|
||||
*/
|
||||
export function resolveMcpEntry(rootDir = ROOT, existsSyncFn = existsSync) {
|
||||
const candidates = [
|
||||
// Preferred distributable JS entry (npm publish artifact)
|
||||
join(rootDir, "app", "open-sse", "mcp-server", "server.js"),
|
||||
// Preferred distributable JS entry (npm publish artifact, built by prepublish.ts)
|
||||
join(rootDir, "dist", "open-sse", "mcp-server", "server.js"),
|
||||
// Local workspace TypeScript source fallback
|
||||
join(rootDir, "open-sse", "mcp-server", "server.ts"),
|
||||
];
|
||||
|
||||
for (const entry of candidates) {
|
||||
if (existsSync(entry)) return entry;
|
||||
if (existsSyncFn(entry)) return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -32,7 +38,7 @@ export async function startMcpCli(rootDir = ROOT) {
|
||||
const mcpEntry = resolveMcpEntry(rootDir);
|
||||
if (!mcpEntry) {
|
||||
throw new Error(
|
||||
"MCP server entrypoint not found. Expected app/open-sse/mcp-server/server.js or open-sse/mcp-server/server.ts."
|
||||
"MCP server entrypoint not found. Expected dist/open-sse/mcp-server/server.js or open-sse/mcp-server/server.ts."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
0
bin/reset-password.mjs
Normal file → Executable file
0
bin/reset-password.mjs
Normal file → Executable file
6
complexity-baseline.json
Normal file
6
complexity-baseline.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 1794,
|
||||
"_rebaseline_2026_06_10": "Re-baseline consciente: 1739 foi medido na branch das Fases 0-6 (base ~v3.8.17); a v3.8.18 publicada ja carrega 1746 (provado: o commit-base 5f2722bd6, anterior a qualquer commit do ciclo v3.8.19, mede 1746 — funcoes complexas dos reworks RequestLoggerV2/stream/combo). Mesma familia dos re-baselines de eslintWarnings/file-size. Reducao = Fase 6A (2026-06-16).",
|
||||
"_rebaseline_2026_06_13_6a11": "Re-baseline consciente Task 6A.11: escopo ampliado para src+open-sse+electron+bin (electron/bin contribuem 0 violacoes novas — todos os 4 arquivos .ts em bin/ estao abaixo dos thresholds). Drift 1746→1794 pre-existente de features mergeadas nos ciclos v3.8.22/v3.8.23 (nao causado por esta task). Congelado no valor real medido para destrancar o gate."
|
||||
}
|
||||
121
dependency-allowlist.json
Normal file
121
dependency-allowlist.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"_comment": "Allowlist anti-slopsquatting (check-deps.mjs). Toda dep nova exige adicao EXPLICITA aqui apos verificar que e legitima.",
|
||||
"allowed": [
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
"@dnd-kit/core",
|
||||
"@dnd-kit/sortable",
|
||||
"@dnd-kit/utilities",
|
||||
"@huggingface/transformers",
|
||||
"@lobehub/icons",
|
||||
"@modelcontextprotocol/sdk",
|
||||
"@monaco-editor/react",
|
||||
"@ngrok/ngrok",
|
||||
"@opencode-ai/plugin",
|
||||
"@playwright/test",
|
||||
"@swc/helpers",
|
||||
"@tailwindcss/postcss",
|
||||
"@testing-library/jest-dom",
|
||||
"@testing-library/react",
|
||||
"@types/bcryptjs",
|
||||
"@types/better-sqlite3",
|
||||
"@types/bun",
|
||||
"@types/keytar",
|
||||
"@types/mdx",
|
||||
"@types/node",
|
||||
"@types/react",
|
||||
"@types/react-dom",
|
||||
"@types/ws",
|
||||
"@vitejs/plugin-react",
|
||||
"@xyflow/react",
|
||||
"axios",
|
||||
"bcryptjs",
|
||||
"better-sqlite3",
|
||||
"bottleneck",
|
||||
"c8",
|
||||
"commander",
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"csv-stringify",
|
||||
"dpdm",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
"electron-updater",
|
||||
"eslint",
|
||||
"eslint-config-next",
|
||||
"eslint-plugin-sonarjs",
|
||||
"express",
|
||||
"fetch-socks",
|
||||
"fflate",
|
||||
"fumadocs-core",
|
||||
"fumadocs-mdx",
|
||||
"fumadocs-ui",
|
||||
"glob",
|
||||
"gray-matter",
|
||||
"http-proxy-middleware",
|
||||
"https-proxy-agent",
|
||||
"husky",
|
||||
"ink",
|
||||
"ink-spinner",
|
||||
"ink-text-input",
|
||||
"ioredis",
|
||||
"jose",
|
||||
"js-yaml",
|
||||
"jscpd",
|
||||
"jsdom",
|
||||
"jsonc-parser",
|
||||
"keytar",
|
||||
"knip",
|
||||
"license-checker-rseidelsohn",
|
||||
"lint-staged",
|
||||
"lockfile-lint",
|
||||
"lowdb",
|
||||
"lucide-react",
|
||||
"marked",
|
||||
"marked-terminal",
|
||||
"material-symbols",
|
||||
"mermaid",
|
||||
"monaco-editor",
|
||||
"next",
|
||||
"next-intl",
|
||||
"next-themes",
|
||||
"node-loader",
|
||||
"node-machine-id",
|
||||
"open",
|
||||
"ora",
|
||||
"parse5",
|
||||
"pino",
|
||||
"pino-abstract-transport",
|
||||
"pino-pretty",
|
||||
"playwright",
|
||||
"prettier",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-is",
|
||||
"react-markdown",
|
||||
"react-reconciler",
|
||||
"recharts",
|
||||
"selfsigned",
|
||||
"size-limit",
|
||||
"sql.js",
|
||||
"sqlite-vec",
|
||||
"tailwindcss",
|
||||
"tls-client-node",
|
||||
"tsup",
|
||||
"tsx",
|
||||
"type-coverage",
|
||||
"typescript",
|
||||
"typescript-eslint",
|
||||
"undici",
|
||||
"update-notifier",
|
||||
"uuid",
|
||||
"vitest",
|
||||
"wait-on",
|
||||
"wreq-js",
|
||||
"ws",
|
||||
"wtfnode",
|
||||
"xxhash-wasm",
|
||||
"yazl",
|
||||
"zod",
|
||||
"zustand"
|
||||
]
|
||||
}
|
||||
@@ -121,15 +121,15 @@ provider.
|
||||
For reference, the cc-compatible bridge sends the following on each upstream
|
||||
request (see `open-sse/services/claudeCodeCompatible.ts`):
|
||||
|
||||
| Header | Value |
|
||||
| ------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `Authorization` | `Bearer <api-key>` |
|
||||
| `User-Agent` | `claude-cli/2.1.137 (external, sdk-cli)` |
|
||||
| `anthropic-version` | `2023-06-01` |
|
||||
| `anthropic-beta` | `claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24` |
|
||||
| `anthropic-dangerous-direct-browser-access` | `true` |
|
||||
| `x-app` | `cli` |
|
||||
| `X-Stainless-*` | Various Stainless SDK headers (lang, package version, OS, arch, etc.) |
|
||||
| Header | Value |
|
||||
| ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `Authorization` | `Bearer <api-key>` |
|
||||
| `User-Agent` | `claude-cli/2.1.158 (external, sdk-cli)` |
|
||||
| `anthropic-version` | `2023-06-01` |
|
||||
| `anthropic-beta` | `claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24,redact-thinking-2026-02-12` |
|
||||
| `anthropic-dangerous-direct-browser-access` | `true` |
|
||||
| `x-app` | `cli` |
|
||||
| `X-Stainless-*` | Various Stainless SDK headers (lang, package version, OS, arch, etc.) |
|
||||
|
||||
This is what allows requests to pass the upstream WAF / client whitelist.
|
||||
|
||||
|
||||
334
docs/DOCUMENTATION_OVERHAUL_PLAN.md
Normal file
334
docs/DOCUMENTATION_OVERHAUL_PLAN.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# OmniRoute Documentation Overhaul Plan
|
||||
|
||||
> Goal: Make all docs accessible to non-tech users while preserving technical depth for developers.
|
||||
> Strategy: Two-tier docs (User Guide + Technical Reference) with cross-links.
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **User Guide** (new files in `docs/guides/`)
|
||||
- Plain language, no jargon
|
||||
- "Why → What → How" structure
|
||||
- Tables for comparisons
|
||||
- Copy-paste examples
|
||||
- "What's next?" at the end
|
||||
|
||||
2. **Technical Reference** (existing files)
|
||||
- Keep as-is (already well-written)
|
||||
- Add "For Users" callout at top linking to user guide
|
||||
- Add "Prerequisites" section
|
||||
- Ensure consistent formatting
|
||||
|
||||
3. **Cross-Links**
|
||||
- User guides link to technical docs for "learn more"
|
||||
- Technical docs link to user guides for "quick start"
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: User-Facing Quick Start Docs (Highest Impact)
|
||||
|
||||
### 1.1 `docs/guides/QUICK-START.md` — NEW
|
||||
**Replaces**: Condensed version of README.md
|
||||
**Content**:
|
||||
- Install (npm, Docker, source)
|
||||
- Connect a free provider (3 steps)
|
||||
- Point your IDE to OmniRoute
|
||||
- Verify it works
|
||||
- "What's next?" → link to other guides
|
||||
|
||||
### 1.2 `docs/guides/AUTO-COMBO-GUIDE.md` — NEW
|
||||
**Complements**: `docs/routing/AUTO-COMBO.md`
|
||||
**Content**:
|
||||
- What auto-combo does (30-second version)
|
||||
- Which `auto` should I use? (table)
|
||||
- How it picks providers (simple version)
|
||||
- Common questions (FAQ)
|
||||
- "Learn more" → link to technical reference
|
||||
|
||||
### 1.3 `docs/guides/PROVIDERS-GUIDE.md` — NEW
|
||||
**Complements**: `docs/reference/PROVIDER_REFERENCE.md`
|
||||
**Content**:
|
||||
- What is a provider? (analogy)
|
||||
- How to connect a provider (step-by-step)
|
||||
- Free vs paid providers (table)
|
||||
- OAuth vs API key (which do I need?)
|
||||
- Troubleshooting connection issues
|
||||
- "Learn more" → link to provider reference
|
||||
|
||||
### 1.4 `docs/guides/FREE-TIERS-GUIDE.md` — NEW
|
||||
**Complements**: `docs/reference/FREE_TIERS.md`
|
||||
**Content**:
|
||||
- What are free tiers? (analogy)
|
||||
- Best free providers (table with quotas)
|
||||
- How to stack free tiers for unlimited usage
|
||||
- Common questions (FAQ)
|
||||
- "Learn more" → link to free tiers reference
|
||||
|
||||
### 1.5 `docs/guides/TROUBLESHOOTING.md` — REWRITE
|
||||
**Current**: Technical, code-heavy
|
||||
**New**: "I see X → Do Y" format
|
||||
**Content**:
|
||||
- Problem → Solution table
|
||||
- Common error messages (with screenshots)
|
||||
- "Still stuck?" → link to community
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Refine Existing User Docs
|
||||
|
||||
### 2.1 `docs/guides/USER_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "What you'll learn" section at top
|
||||
- Remove jargon, explain terms
|
||||
- Add step-by-step screenshots
|
||||
- Add "Common mistakes" section
|
||||
|
||||
### 2.2 `docs/guides/SETUP_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "Prerequisites" section
|
||||
- Simplify commands (one-liners)
|
||||
- Add "Verify it works" step
|
||||
- Add "Troubleshooting" section
|
||||
|
||||
### 2.3 `docs/guides/FEATURES.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "Why this matters" for each feature
|
||||
- Add use-case examples
|
||||
- Add "How to enable" for each feature
|
||||
- Add screenshots
|
||||
|
||||
### 2.4 `docs/guides/DOCKER_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "Docker for beginners" section
|
||||
- Simplify commands
|
||||
- Add "Common Docker issues" section
|
||||
- Add "What's next?" section
|
||||
|
||||
### 2.5 `docs/guides/ELECTRON_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "What is Electron?" explanation
|
||||
- Add screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.6 `docs/guides/TERMUX_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "What is Termux?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.7 `docs/guides/PWA_GUIDE.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "What is a PWA?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.8 `docs/guides/I18N.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "How to change language" (user perspective)
|
||||
- Add "How to contribute translations" (contributor perspective)
|
||||
|
||||
### 2.9 `docs/guides/KIRO_SETUP.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "What is Kiro?" explanation
|
||||
- Add step-by-step with screenshots
|
||||
- Add "Common issues" section
|
||||
|
||||
### 2.10 `docs/guides/UNINSTALL.md` — UPDATE
|
||||
**Changes**:
|
||||
- Add "Why uninstall?" section (common reasons)
|
||||
- Add "Before you uninstall" checklist
|
||||
- Simplify commands
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Technical Docs (Add Cross-Links, Keep As-Is)
|
||||
|
||||
### 3.1 `docs/architecture/` (6 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- ARCHITECTURE.md
|
||||
- AUTHZ_GUIDE.md
|
||||
- CODEBASE_DOCUMENTATION.md
|
||||
- MONITORING_SECTIONS.md
|
||||
- REPOSITORY_MAP.md
|
||||
- RESILIENCE_GUIDE.md
|
||||
|
||||
### 3.2 `docs/frameworks/` (16 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- A2A-SERVER.md
|
||||
- AGENT-SKILLS.md
|
||||
- AGENTBRIDGE.md
|
||||
- AGENT_PROTOCOLS_GUIDE.md
|
||||
- CLOUD_AGENT.md
|
||||
- EMBEDDED-SERVICES.md
|
||||
- EVALS.md
|
||||
- GAMIFICATION.md
|
||||
- MCP-SERVER.md
|
||||
- MEMORY.md
|
||||
- OPENCODE.md
|
||||
- PLAYGROUND_STUDIO.md
|
||||
- SEARCH_TOOLS_STUDIO.md
|
||||
- SKILLS.md
|
||||
- TRAFFIC_INSPECTOR.md
|
||||
- WEBHOOKS.md
|
||||
|
||||
### 3.3 `docs/security/` (9 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- CLI_TOKEN.md
|
||||
- CLI_TOKEN_AUTH.md
|
||||
- COMPLIANCE.md
|
||||
- ERROR_SANITIZATION.md
|
||||
- GUARDRAILS.md
|
||||
- PUBLIC_CREDS.md
|
||||
- ROUTE_GUARD_TIERS.md
|
||||
- SOCKET_DEV_FINDINGS.md
|
||||
- STEALTH_GUIDE.md
|
||||
|
||||
### 3.4 `docs/ops/` (8 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- COVERAGE_PLAN.md
|
||||
- E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md
|
||||
- FLY_IO_DEPLOYMENT_GUIDE.md
|
||||
- PROXY_GUIDE.md
|
||||
- RELEASE_CHECKLIST.md
|
||||
- SQLITE_RUNTIME.md
|
||||
- TUNNELS_GUIDE.md
|
||||
- VM_DEPLOYMENT_GUIDE.md
|
||||
|
||||
### 3.5 `docs/compression/` (5 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- COMPRESSION_ENGINES.md
|
||||
- COMPRESSION_GUIDE.md
|
||||
- COMPRESSION_LANGUAGE_PACKS.md
|
||||
- COMPRESSION_RULES_FORMAT.md
|
||||
- RTK_COMPRESSION.md
|
||||
|
||||
### 3.6 `docs/routing/` (3 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- AUTO-COMBO.md (→ link to AUTO-COMBO-GUIDE.md)
|
||||
- QUOTA_SHARE.md
|
||||
- REASONING_REPLAY.md
|
||||
|
||||
### 3.7 `docs/reference/` (5 files)
|
||||
**Changes**: Add "For Users" callout at top → link to user guide
|
||||
- API_REFERENCE.md
|
||||
- CLI-TOOLS.md
|
||||
- ENVIRONMENT.md
|
||||
- FREE_TIERS.md (→ link to FREE-TIERS-GUIDE.md)
|
||||
- PROVIDER_REFERENCE.md (→ link to PROVIDERS-GUIDE.md)
|
||||
|
||||
### 3.8 Other Docs (keep as-is)
|
||||
- `docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md` — Already user-friendly
|
||||
- `docs/marketing/TIERS.md` — Already user-friendly
|
||||
- `docs/diagrams/README.md` — Keep as-is
|
||||
- `docs/dev/plugins.md` — Developer-only
|
||||
- `docs/plugins/PLUGIN_SDK.md` — Developer-only
|
||||
- `docs/providers/ZED-DOCKER.md` — Provider-specific
|
||||
- `docs/AGENTROUTER.md` — Provider-specific
|
||||
- `docs/PROVIDERS.md` — Provider-specific
|
||||
- `docs/README.md` — Keep as-is
|
||||
- `docs/SUBMIT_PR.md` — Contributor-only
|
||||
- `docs/releases/v3.8.0.md` — Release notes
|
||||
- `docs/research/` — Internal research
|
||||
- `docs/specs/` — Internal specs
|
||||
- `docs/openspec/` — Internal specs
|
||||
- `docs/superpowers/` — Internal plans
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Update Main README.md
|
||||
|
||||
**Changes**:
|
||||
- Add "Quick Start" section (3 steps)
|
||||
- Add "Which `auto` should I use?" table
|
||||
- Add "Free providers" table
|
||||
- Add "Common questions" section
|
||||
- Link to user guides
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
### Week 1: Phase 1 (User-Facing Quick Start Docs)
|
||||
1. `docs/guides/QUICK-START.md` — NEW
|
||||
2. `docs/guides/AUTO-COMBO-GUIDE.md` — NEW
|
||||
3. `docs/guides/PROVIDERS-GUIDE.md` — NEW
|
||||
4. `docs/guides/FREE-TIERS-GUIDE.md` — NEW
|
||||
5. `docs/guides/TROUBLESHOOTING.md` — REWRITE
|
||||
|
||||
### Week 2: Phase 2 (Refine Existing User Docs)
|
||||
1. `docs/guides/USER_GUIDE.md` — UPDATE
|
||||
2. `docs/guides/SETUP_GUIDE.md` — UPDATE
|
||||
3. `docs/guides/FEATURES.md` — UPDATE
|
||||
4. `docs/guides/DOCKER_GUIDE.md` — UPDATE
|
||||
5. `docs/guides/ELECTRON_GUIDE.md` — UPDATE
|
||||
6. `docs/guides/TERMUX_GUIDE.md` — UPDATE
|
||||
7. `docs/guides/PWA_GUIDE.md` — UPDATE
|
||||
8. `docs/guides/I18N.md` — UPDATE
|
||||
9. `docs/guides/KIRO_SETUP.md` — UPDATE
|
||||
10. `docs/guides/UNINSTALL.md` — UPDATE
|
||||
|
||||
### Week 3: Phase 3 (Technical Docs Cross-Links)
|
||||
1. `docs/architecture/` — Add cross-links
|
||||
2. `docs/frameworks/` — Add cross-links
|
||||
3. `docs/security/` — Add cross-links
|
||||
4. `docs/ops/` — Add cross-links
|
||||
5. `docs/compression/` — Add cross-links
|
||||
6. `docs/routing/` — Add cross-links
|
||||
7. `docs/reference/` — Add cross-links
|
||||
|
||||
### Week 4: Phase 4 (Update Main README.md)
|
||||
1. Update README.md with user-friendly sections
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- [ ] User can install OmniRoute in < 5 minutes (QUICK-START.md)
|
||||
- [ ] User can connect a provider in < 3 minutes (PROVIDERS-GUIDE.md)
|
||||
- [ ] User can use auto-combo in < 1 minute (AUTO-COMBO-GUIDE.md)
|
||||
- [ ] User can find free providers in < 2 minutes (FREE-TIERS-GUIDE.md)
|
||||
- [ ] User can troubleshoot common issues in < 5 minutes (TROUBLESHOOTING.md)
|
||||
- [ ] All technical docs link to user guides
|
||||
- [ ] All user guides link to technical docs
|
||||
|
||||
---
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- User guides: `docs/guides/[FEATURE]-GUIDE.md` (e.g., `AUTO-COMBO-GUIDE.md`)
|
||||
- Technical docs: Keep existing names (e.g., `docs/routing/AUTO-COMBO.md`)
|
||||
- Cross-links: "For Users" callout at top of technical docs
|
||||
|
||||
---
|
||||
|
||||
## Template for User Guides
|
||||
|
||||
```markdown
|
||||
# [Feature Name]: [One-Line Description]
|
||||
|
||||
## What It Does
|
||||
[2-3 sentences explaining what the feature does in plain language]
|
||||
|
||||
## Quick Start
|
||||
[Step-by-step instructions to get started]
|
||||
|
||||
## [Main Section]
|
||||
[Detailed explanation with tables, examples, screenshots]
|
||||
|
||||
## Common Questions
|
||||
[FAQ section with common questions and answers]
|
||||
|
||||
## What's Next?
|
||||
[Links to related guides and technical docs]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template for Technical Doc Cross-Links
|
||||
|
||||
```markdown
|
||||
> **For Users**: Looking for a quick start? See the [User Guide](../guides/[FEATURE]-GUIDE.md).
|
||||
|
||||
> **Prerequisites**: [List prerequisites]
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "OmniRoute Documentation"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.24
|
||||
lastUpdated: 2026-06-13
|
||||
---
|
||||
|
||||
# OmniRoute Documentation
|
||||
@@ -22,9 +22,19 @@ How the system is put together — read these to understand the runtime, code la
|
||||
- [AUTHZ_GUIDE.md](architecture/AUTHZ_GUIDE.md) — authorization pipeline (route classifier + policy engine).
|
||||
- [RESILIENCE_GUIDE.md](architecture/RESILIENCE_GUIDE.md) — provider circuit breaker, connection cooldown, and model lockout.
|
||||
|
||||
## guides/
|
||||
## For Non-Tech Users
|
||||
|
||||
Task-focused walkthroughs for operators and end users.
|
||||
Simple guides for using OmniRoute — no technical background needed.
|
||||
|
||||
### getting-started/
|
||||
|
||||
- [QUICK-START.md](getting-started/QUICK-START.md) — install and run OmniRoute in 3 minutes.
|
||||
- [AUTO-COMBO-GUIDE.md](getting-started/AUTO-COMBO-GUIDE.md) — let OmniRoute pick the best AI for you.
|
||||
- [PROVIDERS-GUIDE.md](getting-started/PROVIDERS-GUIDE.md) — how to connect AI providers.
|
||||
- [FREE-TIERS-GUIDE.md](getting-started/FREE-TIERS-GUIDE.md) — get free AI with no credit card.
|
||||
- [TROUBLESHOOTING.md](getting-started/TROUBLESHOOTING.md) — fix common issues.
|
||||
|
||||
### guides/
|
||||
|
||||
- [SETUP_GUIDE.md](guides/SETUP_GUIDE.md) — first-time setup of OmniRoute.
|
||||
- [USER_GUIDE.md](guides/USER_GUIDE.md) — daily usage of the dashboard and API.
|
||||
@@ -32,11 +42,14 @@ Task-focused walkthroughs for operators and end users.
|
||||
- [ELECTRON_GUIDE.md](guides/ELECTRON_GUIDE.md) — desktop (Electron) builds.
|
||||
- [TERMUX_GUIDE.md](guides/TERMUX_GUIDE.md) — running on Android via Termux.
|
||||
- [PWA_GUIDE.md](guides/PWA_GUIDE.md) — installing the dashboard as a PWA.
|
||||
- [TROUBLESHOOTING.md](guides/TROUBLESHOOTING.md) — common issues and fixes.
|
||||
- [UNINSTALL.md](guides/UNINSTALL.md) — clean removal steps.
|
||||
- [I18N.md](guides/I18N.md) — translation and locale workflow.
|
||||
- [FEATURES.md](guides/FEATURES.md) — dashboard feature gallery.
|
||||
|
||||
## For Tech Users
|
||||
|
||||
Technical documentation for developers and contributors.
|
||||
|
||||
## reference/
|
||||
|
||||
Lookup material — API surface, environment variables, CLI flags, provider catalog.
|
||||
|
||||
@@ -22,18 +22,18 @@ npm install
|
||||
|
||||
## 2 — Sync with the current release branch
|
||||
|
||||
PRs go to **`release/v3.8.3`**, not `main`.
|
||||
PRs go to the **current release branch** — `release/v3.8.24` at the time of writing. Always target the latest `release/v*` branch (the repository's active development branch), never `main`. Check the repo's branch list or `CONTRIBUTING.md` if a newer release cycle has opened.
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git checkout -b fix/your-description upstream/release/v3.8.3
|
||||
git checkout -b fix/your-description upstream/release/v3.8.24
|
||||
```
|
||||
|
||||
If you already made your changes on another branch, rebase on top of it:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/release/v3.8.3
|
||||
git rebase upstream/release/v3.8.24
|
||||
```
|
||||
|
||||
---
|
||||
@@ -59,7 +59,7 @@ Examples: `fix/codex-token-refresh`, `feat/provider-xyz`, `docs/update-readme`
|
||||
npm run lint # must pass (0 errors)
|
||||
npm run typecheck:core # must pass
|
||||
npm run test:unit # must pass
|
||||
npm run test:coverage # coverage gate: 75/75/75/70
|
||||
npm run test:coverage # coverage gate: 60/60/60/60 (statements/lines/functions/branches)
|
||||
```
|
||||
|
||||
If you changed production code in `src/`, `open-sse/`, `electron/`, or `bin/`, include or update tests in the same PR.
|
||||
@@ -87,7 +87,7 @@ Common scopes: `api`, `dashboard`, `db`, `sse`, `oauth`, `providers`, `combo`, `
|
||||
git push -u origin fix/your-description
|
||||
```
|
||||
|
||||
Then open a PR on GitHub targeting **`diegosouzapw/OmniRoute`** → **`release/v3.8.3`**.
|
||||
Then open a PR on GitHub targeting **`diegosouzapw/OmniRoute`** → **`release/v3.8.24`**.
|
||||
|
||||
PR description checklist:
|
||||
|
||||
@@ -105,7 +105,7 @@ PR description checklist:
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/release/v3.8.3
|
||||
git rebase upstream/release/v3.8.24
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (177 providers, 55 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (177 providers, 60 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
|
||||
187
docs/architecture/QUALITY_GATES.md
Normal file
187
docs/architecture/QUALITY_GATES.md
Normal file
@@ -0,0 +1,187 @@
|
||||
---
|
||||
title: Quality Gates Reference
|
||||
---
|
||||
|
||||
# Quality Gates Reference
|
||||
|
||||
This document is the authoritative reference for all CI quality gates in OmniRoute.
|
||||
It describes each gate, what it validates, which CI job it runs in, whether it uses
|
||||
a ratchet baseline or a pass/fail policy, and whether it blocks the build or is advisory.
|
||||
|
||||
For a short summary and the allowlist policy, see the "Quality Gates & Ratchets" section
|
||||
in `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Gate Inventory (35 scripts)
|
||||
|
||||
Scripts live under `scripts/check/` (policy gates) and `scripts/quality/` (ratchet engine).
|
||||
The CI source of truth is `.github/workflows/ci.yml`.
|
||||
|
||||
### Job: `lint`
|
||||
|
||||
Runs on every PR to `main`. Blocks merge on failure.
|
||||
|
||||
| Script (`npm run ...`) | Validates | Blocking |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
|
||||
| `check:node-runtime` | Node.js version is within the supported range | Yes |
|
||||
| `check:cycles` | Circular imports — all `src/` + `open-sse/` modules | Yes |
|
||||
| `check:route-validation:t06` | Zod schemas present on all routes (Tier 6 policy) | Yes |
|
||||
| `check:any-budget:t11` | `@ts-expect-error // any` count does not exceed budget (Tier 11 catraca) | Yes |
|
||||
| `check:provider-consistency` | Every provider in `providers.ts` has a matching entry in `providerRegistry.ts` (and vice-versa, within the allowlist) | Yes |
|
||||
| `check:fetch-targets` | Every `fetch("/api/...")` in client-side `src/` resolves to a real `route.ts` | Yes |
|
||||
| `check:deps` | All `npm install`-able deps across every `package.json` in the repo are in `dependency-allowlist.json`; new unpinned or slopsquatted packages flagged | Yes |
|
||||
| `check:file-size` | No source file exceeds the per-extension cap (ratchet: frozen large files in `frozen` list) | Yes |
|
||||
| `check:error-helper` | Error responses in executors/handlers use `buildErrorBody()` / `sanitizeErrorMessage()` (Hard Rule #12) | Yes |
|
||||
| `check:migration-numbering` | Migration SQL files are sequentially numbered, no gaps or duplicates | Yes |
|
||||
| `check:public-creds` | No literal OAuth `client_id`/`client_secret` or Firebase Web keys outside `publicCreds.ts` (Hard Rule #11) | Yes |
|
||||
| `check:db-rules` | No raw SQL outside `src/lib/db/` modules; no barrel-imports from `localDb.ts` (Hard Rules #2/#5) | Yes |
|
||||
| `check:known-symbols` | Provider executors, routing strategies, and translators registered in their dispatch tables match the files on disk — no orphaned or undeclared symbols | Yes |
|
||||
| `check:route-guard-membership` | Every route that spawns a child process is classified by `isLocalOnlyPath()` (Hard Rules #15/#17) | Yes |
|
||||
| `check:test-discovery` | Every `*.test.ts` / `*.spec.ts` file in the repo is collected by at least one test runner (ratchet: orphan list in `test-discovery-baseline.json` can only shrink) | Yes |
|
||||
| `check:docs-sync` | CHANGELOG version, OpenAPI version, and `llm.txt` are in sync | Yes |
|
||||
| `typecheck:core` | TypeScript compilation without errors (advisory warnings only) | Yes |
|
||||
| `typecheck:noimplicit:core` | Strict `noImplicitAny` — forward-looking; many pre-existing call sites still need annotations | **Advisory** (`continue-on-error: true`) |
|
||||
|
||||
### Job: `quality-gate`
|
||||
|
||||
Runs after `test-coverage`. Blocks merge on failure.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `quality:collect` | Emits `quality-metrics.json` (ESLint warning count, coverage from merged shard report) | Yes (upstream of ratchet) |
|
||||
| `quality:ratchet` | Each metric in `quality-baseline.json` has not regressed (ESLint warnings ≤ baseline; coverage ≥ baseline) | Yes |
|
||||
| `check:duplication` | Code duplication (jscpd@4) does not exceed baseline in `quality-baseline.json` | Yes |
|
||||
| `check:complexity` | File-level cyclomatic complexity does not exceed the cap | Yes |
|
||||
|
||||
### Job: `docs-sync-strict`
|
||||
|
||||
Runs on every PR to `main`. Blocks merge on failure.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| `check:docs-all` | Meta-gate that runs the 6 sub-gates below sequentially | Yes |
|
||||
| ↳ `check:docs-sync` | CHANGELOG / OpenAPI / llm.txt version consistency | Yes |
|
||||
| ↳ `check:docs-counts` | Counts in prose (provider count, migration count, etc.) are within the ratchet window of the real counts | Yes |
|
||||
| ↳ `check:env-doc-sync` | Every env var in `.env.example` is documented in a docs table, and vice versa | Yes |
|
||||
| ↳ `check:deprecated-versions` | No deprecated version strings in docs | Yes |
|
||||
| ↳ `check:doc-links` | Internal markdown links in docs resolve to real files (`[text]`/`(path)` form) | Yes |
|
||||
| ↳ `check:fabricated-docs` | Routes, env vars, CLI commands, hook names, and file paths cited in docs exist in the codebase. Hard gate via `--strict`; soft-fail without flag. | Yes (via `--strict` in CI) |
|
||||
| `check:cli-i18n` | CLI command strings are present in all i18n locale files | Yes |
|
||||
| `check:openapi-coverage` | OpenAPI spec covers at least a ratcheted floor of real routes | Yes |
|
||||
| `check:openapi-security-tiers` | Security tier annotations in `openapi.yaml` are consistent with `routeGuard.ts` classifications | **Advisory** |
|
||||
| `check:openapi-routes` | Every path in `openapi.yaml` resolves to a real `route.ts` (anti-hallucination) | Yes |
|
||||
| `check:docs-symbols` | Every `/api/...` reference in `docs/**/*.md` resolves to a real `route.ts` (anti-hallucination) | Yes |
|
||||
| `i18n translation drift` | Untranslated keys in i18n locale files — warn only | **Advisory** |
|
||||
|
||||
### Job: `i18n-ui-coverage`
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| --------------------------------- | ----------------------------- | -------- |
|
||||
| `check-ui-keys-coverage` (inline) | UI i18n key coverage is ≥ 65% | Yes |
|
||||
|
||||
### Job: `i18n`
|
||||
|
||||
Full i18n validation matrix (one job per locale). Entire job is advisory.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ------------------------------- | ----------------------------------- | ----------------------------------------------------- |
|
||||
| `validate_translation.py quick` | Translation completeness per locale | **Advisory** (`continue-on-error: true` on whole job) |
|
||||
|
||||
### Job: `pr-test-policy`
|
||||
|
||||
Runs on pull requests only.
|
||||
|
||||
| Script | Validates | Blocking |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `check:pr-test-policy` | PRs that change production code in `src/`, `open-sse/`, `electron/`, or `bin/` must include or update tests (Hard Rule #8) | Yes |
|
||||
| `check:test-masking` | Changed test files do not reduce net assert count or add `assert.ok(true)` tautologies | Yes |
|
||||
|
||||
### Job: `test-vitest`
|
||||
|
||||
Runs after `build`. Blocks merge on failure.
|
||||
|
||||
| Suite | Validates | Blocking |
|
||||
| ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `test:vitest` | MCP server (43 tools), autoCombo, cache — vitest runner | Yes |
|
||||
| `test:vitest:ui` | UI component tests — vitest runner | **Advisory** (`continue-on-error: true`) — failing until Fase 6A UI triage |
|
||||
|
||||
---
|
||||
|
||||
## Ratchet Baseline (`quality-baseline.json`)
|
||||
|
||||
The ratchet engine (`scripts/quality/check-quality-ratchet.mjs`) reads `quality-baseline.json`
|
||||
and compares it against the freshly collected `quality-metrics.json`. Any metric that regresses
|
||||
beyond its epsilon fails the build.
|
||||
|
||||
Current tracked metrics:
|
||||
|
||||
| Metric | Direction | Meaning |
|
||||
| --------------------- | --------- | ---------------------------------- |
|
||||
| `eslintWarnings` | `down` | ESLint warning count must not grow |
|
||||
| `coverage.statements` | `up` | Statement coverage must not fall |
|
||||
| `coverage.lines` | `up` | Line coverage must not fall |
|
||||
| `coverage.functions` | `up` | Function coverage must not fall |
|
||||
| `coverage.branches` | `up` | Branch coverage must not fall |
|
||||
|
||||
To update the baseline after a genuine improvement:
|
||||
|
||||
```bash
|
||||
npm run quality:ratchet -- --update
|
||||
git add quality-baseline.json
|
||||
```
|
||||
|
||||
The `--update` flag writes the current measured values into `quality-baseline.json`.
|
||||
Commit this file alongside the change that improved the metric. A PR that improves a
|
||||
metric without updating the baseline will be caught by `--require-tighten` (Fase 6A.5,
|
||||
pending implementation).
|
||||
|
||||
---
|
||||
|
||||
## Allowlist Policy
|
||||
|
||||
Every gate that cannot fail on pre-existing violations uses a frozen allowlist
|
||||
(e.g., `KNOWN_STALE_DOC_REFS`, `KNOWN_MISSING`, `KNOWN_RAW_SQL`). The policy is:
|
||||
|
||||
**Fix the root cause; use the allowlist only when the violation is pre-existing and
|
||||
cannot be fixed in the same PR.**
|
||||
|
||||
When adding an entry to an allowlist:
|
||||
|
||||
1. Include a comment with the justification.
|
||||
2. Reference the tracking issue (e.g., `// #3498 — Phase 2 feature, not yet implemented`).
|
||||
3. Remove the entry in the same PR that fixes the violation — a stale entry that no longer
|
||||
suppresses an active violation is itself a defect (6A.3 stale-enforcement will
|
||||
fail the gate on an orphaned allowlist entry once implemented).
|
||||
|
||||
Do **not** add allowlist entries to make tests pass faster. A green gate with a growing
|
||||
allowlist is a false sense of quality.
|
||||
|
||||
### When a gate fails on your PR
|
||||
|
||||
1. **Read the gate output carefully** — it tells you exactly which file or symbol violated
|
||||
the rule.
|
||||
2. **Fix the violation** — most gates are deterministic filesystem checks that pass as soon
|
||||
as the code is correct.
|
||||
3. **If the violation is pre-existing** (i.e., you did not introduce it but the gate now
|
||||
covers it): add an allowlist entry with a justification comment and a tracking issue.
|
||||
4. **If the gate is a ratchet** (coverage, ESLint warnings, duplication, complexity):
|
||||
your change made the metric worse. Fix the underlying issue, or (rarely) run
|
||||
`npm run quality:ratchet -- --update` if the change is intentional and the metric
|
||||
degradation is acceptable — but document why in the PR description.
|
||||
5. **Advisory gates** (`continue-on-error: true`) are informational — they do not block
|
||||
merge but appear in the CI summary. Fix them anyway.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Gate
|
||||
|
||||
1. Create `scripts/check/check-<name>.mjs` (or `.ts`). Policy gates exit 0/1.
|
||||
Ratchet-style gates emit a metric to `quality-metrics.json` via `collect-metrics.mjs`.
|
||||
2. Add `"check:<name>": "node scripts/check/check-<name>.mjs"` to `package.json`.
|
||||
3. Wire it in `.github/workflows/ci.yml` under the appropriate job
|
||||
(policy → `lint` or `docs-sync-strict`; ratchet → `quality-gate`).
|
||||
4. If it has an allowlist, apply `reportStaleEntries()` from
|
||||
`scripts/check/lib/allowlist.mjs` so stale entries are detected automatically.
|
||||
5. Write a test in `tests/unit/build/` covering the gate's detection logic.
|
||||
6. Update this document (add a row to the relevant job table).
|
||||
@@ -491,7 +491,7 @@ Shipped configuration templates and sample files (referenced by setup wizard).
|
||||
| `commands/deploy-vps-{local,akamai,both}-cc.md` | Deploy to VPS |
|
||||
| `commands/capture-release-evidences-cc.md` | Browser-record new features as WebP |
|
||||
| `commands/review-{prs,discussions}-cc.md` | Triage GitHub PRs/discussions |
|
||||
| `commands/{issue-triage,resolve-issues,implement-features}-cc.md` | Issue workflows |
|
||||
| `commands/{review-issues,implement-features}-cc.md` | Issue workflows |
|
||||
| `settings.local.json` | Per-project Claude Code settings |
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Resilience Guide"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.18
|
||||
lastUpdated: 2026-06-09
|
||||
---
|
||||
|
||||
# Resilience Guide
|
||||
@@ -30,16 +30,19 @@ OmniRoute has three distinct but related resilience mechanisms. Each has a diffe
|
||||
**States:**
|
||||
|
||||
- `CLOSED` — normal traffic allowed
|
||||
- `DEGRADED` — traffic still allowed, but elevated provider failures are being tracked
|
||||
- `OPEN` — provider temporarily blocked; combo routing skips it
|
||||
- `HALF_OPEN` — reset timeout elapsed; probe request allowed
|
||||
|
||||
**Defaults (`open-sse/config/constants.ts`):**
|
||||
**Configurable defaults (`open-sse/config/constants.ts`, exposed in Dashboard → Settings → Resilience):**
|
||||
|
||||
| Class | Threshold | Reset timeout |
|
||||
| ------- | ---------- | ------------- |
|
||||
| OAuth | 3 failures | 60s |
|
||||
| API-key | 5 failures | 30s |
|
||||
| Local | 2 failures | 15s |
|
||||
| Class | Degraded at | Opens at | Reset timeout |
|
||||
| ------- | ----------- | ----------- | ------------- |
|
||||
| OAuth | 5 failures | 8 failures | 60s |
|
||||
| API-key | 7 failures | 12 failures | 30s |
|
||||
| Local | derived | 2 failures | 15s |
|
||||
|
||||
`degradationThreshold` controls when a provider enters `DEGRADED`; `failureThreshold` controls when it opens and is skipped. Local provider profiles are not exposed on the Resilience settings page yet.
|
||||
|
||||
**Trip codes:** only provider-level statuses `[408, 500, 502, 503, 504]`. Do NOT trip for account-level errors (most 401/403/429 — those belong to cooldown or lockout).
|
||||
|
||||
@@ -121,6 +124,7 @@ Lists active lockouts with: provider, connection, model, reason, expiresAt. Oper
|
||||
- **Reset-aware routing** (v3.8.0) — prioritizes connections by quota reset time.
|
||||
- **Background mode degradation** — Responses API `background: true` degraded to sync with warning.
|
||||
- **Dynamic tool limit detection** — backs off providers when tool count limits hit.
|
||||
- **Emergency fallback** — controlled by `OMNIROUTE_EMERGENCY_FALLBACK`; operators can override it from the Feature Flags page without a restart.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"AUTHZ_GUIDE",
|
||||
"CODEBASE_DOCUMENTATION",
|
||||
"REPOSITORY_MAP",
|
||||
"RESILIENCE_GUIDE"
|
||||
"RESILIENCE_GUIDE",
|
||||
"QUALITY_GATES"
|
||||
]
|
||||
}
|
||||
|
||||
29
docs/bdd/proxy-egress-isolation.feature
Normal file
29
docs/bdd/proxy-egress-isolation.feature
Normal file
@@ -0,0 +1,29 @@
|
||||
Feature: Proxy egress isolation and IPv6-only enforcement
|
||||
As an operator routing provider traffic through proxies
|
||||
I want every request to egress only through the assigned proxy (and only over IPv6 when an IPv6 proxy is set)
|
||||
So that the real host IP and IPv4 are never leaked
|
||||
|
||||
Scenario: IPv6-literal proxy de-brackets and connects over IPv6
|
||||
Given a proxy configured as socks5://[2001:db8::1]:1080
|
||||
When the dispatcher options are built
|
||||
Then the SOCKS host is 2001:db8::1 and the resolved family is 6
|
||||
|
||||
Scenario: IPv6 hostname proxy forces IPv6-only egress
|
||||
Given a proxy hostname with family=ipv6
|
||||
When the dispatcher family is resolved
|
||||
Then the connect family is pinned to 6
|
||||
|
||||
Scenario: IPv6-only egress is fail-closed when no AAAA exists
|
||||
Given a proxy hostname with family=ipv6 and no AAAA record
|
||||
When the family is asserted
|
||||
Then it is refused and never egresses over IPv4
|
||||
|
||||
Scenario: Family directive contradicting a literal is rejected
|
||||
Given a proxy 203.0.113.7 with family=ipv6
|
||||
When the dispatcher family is resolved
|
||||
Then it throws a configuration error
|
||||
|
||||
Scenario: Web TLS clients are fail-closed
|
||||
Given a web TLS client whose proxy resolution throws
|
||||
When the proxy URL is resolved
|
||||
Then it throws instead of connecting directly
|
||||
@@ -1,144 +0,0 @@
|
||||
Feature: Self-service API key usage and account quota visibility
|
||||
|
||||
Background:
|
||||
Given OmniRoute has usage accounting enabled
|
||||
And management APIs require a dashboard session or a key with "manage" or "admin"
|
||||
|
||||
Scenario: A delegated key reads its own cost and token usage
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" already has a monthly USD budget of 50 configured in the existing budget UI
|
||||
And "team-a" has current-period spend of 12.50 USD
|
||||
And "team-a" has current-period token usage:
|
||||
| input | output | cache_read | cache_creation | reasoning |
|
||||
| 900000 | 32000 | 120000 | 10000 | 5000 |
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response apiKey.name should be "team-a"
|
||||
And the response usage.cost.limitUsd should be 50
|
||||
And the response usage.cost.usedUsd should be 12.50
|
||||
And the response usage.cost.usedPercent should be 25
|
||||
And the response usage.tokens.totalTokens should be 1067000
|
||||
|
||||
Scenario: A delegated key cannot query another key by id
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And an API key named "team-b" has the scope "self:usage"
|
||||
And "team-b" has current-period spend of 99.00 USD
|
||||
When "team-a" calls GET "/api/v1/me/status?apiKeyId=<team-b-id>" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response apiKey.name should be "team-a"
|
||||
And the response should not contain "team-b"
|
||||
And the response should not contain "99.00" as team-b usage
|
||||
|
||||
Scenario: Anonymous client API mode does not expose self-service status
|
||||
Given global client API auth allows anonymous local traffic
|
||||
When an anonymous caller calls GET "/api/v1/me/status"
|
||||
Then the response status should be 401
|
||||
|
||||
Scenario: Self-service usage scope does not grant management access
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" does not have the scope "manage"
|
||||
And "team-a" does not have the scope "admin"
|
||||
When "team-a" calls GET "/api/usage/history" with its Bearer token
|
||||
Then the response status should be 403
|
||||
|
||||
Scenario: Own usage visibility can be disabled
|
||||
Given an API key named "team-a" does not have the scope "self:usage"
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 403
|
||||
|
||||
Scenario: Existing ordinary keys are backfilled for own usage visibility
|
||||
Given an ordinary API key named "legacy-key" existed before self-service usage scopes
|
||||
And "legacy-key" does not have the scope "self:usage"
|
||||
When OmniRoute runs the compatibility migration
|
||||
Then "legacy-key" should have the scope "self:usage"
|
||||
And "legacy-key" should not have the scope "self:account-quota"
|
||||
|
||||
Scenario: Shared account quota is hidden by default
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" does not have the scope "self:account-quota"
|
||||
And "team-a" is restricted to a Codex connection with available quota
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response should not include shared account quota details
|
||||
|
||||
Scenario: Shared provider account quotas are visible with explicit permission
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" has the scope "self:account-quota"
|
||||
And "team-a" is restricted to a Codex connection and a Claude connection
|
||||
And Codex reports a session quota with 1 percent used
|
||||
And Claude reports a daily quota with 35 percent used
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response accountQuotas should contain 2 entries
|
||||
And the first response accountQuotas entry provider should be "codex"
|
||||
And the first response accountQuotas entry quotas.session.remainingPercentage should be 99
|
||||
And the second response accountQuotas entry provider should be "claude"
|
||||
And the second response accountQuotas entry quotas.daily.remainingPercentage should be 65
|
||||
|
||||
Scenario: A single allowed provider also keeps the compatibility accountQuota field
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" has the scope "self:account-quota"
|
||||
And "team-a" is restricted to exactly one Codex connection
|
||||
And Codex reports a weekly quota with 97 percent used
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response accountQuotas should contain 1 entry
|
||||
And the response accountQuota.provider should be "codex"
|
||||
And the response accountQuota.quotas.weekly.remainingPercentage should be 3
|
||||
|
||||
Scenario: Unrestricted keys can see all active provider account quotas
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" has the scope "self:account-quota"
|
||||
And "team-a" has no explicit allowed connection restrictions
|
||||
And OmniRoute has active Codex and Cursor provider connections with quota data
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response accountQuotas should contain the Codex account quota
|
||||
And the response accountQuotas should contain the Cursor account quota
|
||||
|
||||
Scenario: Provider connection lookup failures do not hide own usage
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" has the scope "self:account-quota"
|
||||
And "team-a" is restricted to a Codex connection and another provider connection
|
||||
And OmniRoute cannot resolve the other provider connection metadata
|
||||
When "team-a" calls GET "/api/v1/me/status" with its Bearer token
|
||||
Then the response status should be 200
|
||||
And the response should still include own cost and token usage
|
||||
And the unresolved response accountQuotas entry should have available false
|
||||
And the unresolved response accountQuotas entry reason should be "connection_lookup_failed"
|
||||
|
||||
Scenario: Existing budget endpoint stays management-only
|
||||
Given an API key named "team-a" has the scope "self:usage"
|
||||
And "team-a" does not have the scope "manage"
|
||||
When "team-a" calls GET "/api/usage/budget?apiKeyId=<another-key-id>" with its Bearer token
|
||||
Then the response status should be 403
|
||||
|
||||
Scenario: API Manager defaults are privacy-preserving
|
||||
Given an operator opens the create API key dialog
|
||||
Then the own cost and token usage visibility control should be checked
|
||||
And the shared account quota visibility control should be unchecked
|
||||
And management access should be unchecked
|
||||
And the dialog should not include a second budget editor
|
||||
|
||||
Scenario: API Manager preserves unrelated scopes
|
||||
Given an API key has scopes:
|
||||
| scope |
|
||||
| self:usage |
|
||||
| custom:scope |
|
||||
When an operator enables shared account quota in the permissions dialog
|
||||
And saves the permissions
|
||||
Then the API key scopes should include "self:usage"
|
||||
And the API key scopes should include "self:account-quota"
|
||||
And the API key scopes should include "custom:scope"
|
||||
|
||||
Scenario: API Manager uses existing budget configuration
|
||||
Given an operator wants to set a monthly USD budget for an API key
|
||||
When the operator uses the dashboard
|
||||
Then the operator should use the existing budget configuration surface
|
||||
And the create key dialog should not save budget limits
|
||||
|
||||
Scenario: New API Manager text is localized
|
||||
Given the dashboard locale is not English
|
||||
When the API Manager renders self-service visibility controls
|
||||
Then the labels should come from the API Manager translation namespace
|
||||
And the component should not render hard-coded English strings for the new controls
|
||||
@@ -271,6 +271,206 @@ RTK mode is inspired by **[RTK - Rust Token Killer](https://github.com/rtk-ai/rt
|
||||
|
||||
---
|
||||
|
||||
## Advanced Compression Systems
|
||||
|
||||
Beyond the 7 standard modes, OmniRoute includes several advanced compression
|
||||
systems that work automatically based on context.
|
||||
|
||||
### Cache-Aware Compression
|
||||
|
||||
Some providers (like Anthropic with prompt caching) support **prompt caching**,
|
||||
which lets them cache parts of the prompt to reduce costs and latency. When
|
||||
caching is enabled, aggressive compression can actually **hurt** performance
|
||||
because it changes the cached tokens, invalidating the cache.
|
||||
|
||||
The `cachingAware.ts` module solves this by **detecting caching context** and
|
||||
**adjusting the compression strategy** accordingly.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. **Detect caching context** — Scans the request body for `cache_control` markers
|
||||
2. **Identify caching providers** — Checks if the target provider supports caching
|
||||
3. **Adjust strategy** — Downgrades `aggressive`/`ultra` to `standard` for caching providers
|
||||
4. **Skip system prompt** — System prompts are usually cached, so don't compress them
|
||||
5. **Use deterministic transformations** — Only use transformations that produce consistent output
|
||||
|
||||
#### Code example
|
||||
|
||||
```ts
|
||||
import { detectCachingContext, getCacheAwareStrategy } from "@omniroute/open-sse/services/compression/cachingAware";
|
||||
|
||||
const body = {
|
||||
model: "anthropic/claude-sonnet-4.5",
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
cache_control: { type: "ephemeral" }, // ← Cache marker
|
||||
};
|
||||
|
||||
const ctx = detectCachingContext(body, { provider: "anthropic" });
|
||||
// → { hasCacheControl: true, provider: "anthropic", isCachingProvider: true }
|
||||
|
||||
const strategy = getCacheAwareStrategy("aggressive", ctx);
|
||||
// → { strategy: "standard", skipSystemPrompt: true, deterministicOnly: true }
|
||||
```
|
||||
|
||||
#### When to use
|
||||
|
||||
Cache-aware compression is **always on** — no configuration needed. It only kicks in
|
||||
when:
|
||||
- The request has `cache_control` markers
|
||||
- The target provider supports prompt caching (Anthropic, OpenAI, etc.)
|
||||
|
||||
### Progressive Aging
|
||||
|
||||
Long conversations accumulate many message turns, but older turns become less
|
||||
relevant. The `progressiveAging.ts` module **degrades messages by turn distance**:
|
||||
|
||||
- **Recent turns (0-3)**: Kept verbatim (full detail)
|
||||
- **Medium turns (4-8)**: Lite compression (whitespace, formatting cleanup)
|
||||
- **Old turns (9+)**: Caveman compression (filler removal, summarization)
|
||||
- **Very old turns (20+)**: Heavily summarized or dropped
|
||||
|
||||
#### Code example
|
||||
|
||||
```ts
|
||||
import { applyAging } from "@omniroute/open-sse/services/compression/progressiveAging";
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: "You are a helpful assistant" },
|
||||
{ role: "user", content: "What is 2+2?" },
|
||||
{ role: "assistant", content: "4" },
|
||||
// ... 50 more turns ...
|
||||
];
|
||||
|
||||
const { messages: aged, saved } = applyAging(messages, {
|
||||
verbatim: 3, // First 3 turns: verbatim
|
||||
light: 8, // Turns 4-8: lite compression
|
||||
moderate: 20, // Turns 9-20: caveman compression
|
||||
// Turns 21+: heavy summarization
|
||||
});
|
||||
|
||||
// saved = number of tokens saved
|
||||
```
|
||||
|
||||
#### When to use
|
||||
|
||||
Progressive aging is **always on** for `aggressive` and `ultra` modes. It's
|
||||
particularly effective for:
|
||||
- Long-running coding sessions
|
||||
- Multi-day conversations
|
||||
- Agentic workflows with many tool calls
|
||||
|
||||
### Caveman Output Mode
|
||||
|
||||
The `outputMode.ts` module injects **system prompt instructions** to make the
|
||||
model itself produce compressed, terse output (a "caveman" style).
|
||||
|
||||
#### How it works
|
||||
|
||||
Instead of compressing the input, this mode adds a system prompt like:
|
||||
> "Reply in minimal words. Skip pleasantries. Use short sentences."
|
||||
|
||||
This works particularly well for:
|
||||
- Code generation (terser output = fewer tokens)
|
||||
- Quick Q&A (no need for elaborate explanations)
|
||||
- Batch processing (maximize throughput)
|
||||
|
||||
#### When to use
|
||||
|
||||
Caveman output mode is **opt-in** — set it via the combo config:
|
||||
```json
|
||||
{
|
||||
"strategy": "auto",
|
||||
"config": {
|
||||
"auto": {
|
||||
"outputMode": "caveman"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Result Compression
|
||||
|
||||
The `toolResultCompressor.ts` module provides **5 specialized compression strategies**
|
||||
for tool results (function calls, agent outputs, search results, etc.):
|
||||
|
||||
1. **Search result compression** — Removes redundant results, keeps top-N
|
||||
2. **File read compression** — Truncates large files, preserves headers/imports
|
||||
3. **Code execution compression** — Keeps only essential stdout/stderr
|
||||
4. **Database query compression** — Limits rows, removes verbose metadata
|
||||
5. **API response compression** — Strips null fields, condenses arrays
|
||||
|
||||
#### When to use
|
||||
|
||||
Tool result compression is **always on** when tool calls are present. No
|
||||
configuration needed.
|
||||
|
||||
### Stacked Pipeline
|
||||
|
||||
The stacked mode runs **multiple engines in sequence** — usually RTK first
|
||||
(60-90% savings on tool output), then Caveman (30% additional savings on the
|
||||
remaining text). This achieves **78-95% total savings**.
|
||||
|
||||
#### How it works
|
||||
|
||||
```
|
||||
Input (1000 tokens)
|
||||
→ RTK (command-aware filter) → 200 tokens
|
||||
→ Caveman (filler removal) → 140 tokens
|
||||
→ Output (140 tokens, 86% savings)
|
||||
```
|
||||
|
||||
#### When to use
|
||||
|
||||
Use stacked mode for:
|
||||
- Tool-heavy workflows (agentic coding, research)
|
||||
- Cost-sensitive batch processing
|
||||
- When you need maximum token savings
|
||||
|
||||
Configure via combo:
|
||||
```json
|
||||
{
|
||||
"strategy": "auto",
|
||||
"config": {
|
||||
"auto": {
|
||||
"modePack": "stacked"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compression Combo Overrides
|
||||
|
||||
You can override the global compression mode **per combo** to fine-tune behavior
|
||||
for different use cases:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "coding-combo",
|
||||
"strategy": "priority",
|
||||
"config": {
|
||||
"auto": {
|
||||
"weights": { "taskFit": 0.5 },
|
||||
"modePack": "quality-first"
|
||||
}
|
||||
},
|
||||
"compressionOverride": {
|
||||
"mode": "aggressive",
|
||||
"stackedPipelines": ["rtk", "caveman"],
|
||||
"preserveToolDefinitions": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- **Coding combos**: Use `aggressive` mode for long sessions
|
||||
- **Quick Q&A combos**: Use `lite` mode for fast responses
|
||||
- **Tool-heavy combos**: Use `stacked` mode for max savings
|
||||
- **Production combos**: Use `cache-aware` mode for caching providers
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Environment Config](../reference/ENVIRONMENT.md) — Compression environment variables
|
||||
|
||||
550
docs/compression/EXTENDING_COMPRESSION.md
Normal file
550
docs/compression/EXTENDING_COMPRESSION.md
Normal file
@@ -0,0 +1,550 @@
|
||||
---
|
||||
title: "Extending the Compression Pipeline"
|
||||
version: 3.8.16
|
||||
lastUpdated: 2026-06-08
|
||||
---
|
||||
|
||||
# Extending the Compression Pipeline
|
||||
|
||||
> **TL;DR**: OmniRoute's compression engine is **pluggable** — you can register custom engines, ship language packs for new languages, and compose stacked pipelines. This guide shows how.
|
||||
|
||||
**Related guides:**
|
||||
- [COMPRESSION_GUIDE.md](./COMPRESSION_GUIDE.md) — Full pipeline overview
|
||||
- [COMPRESSION_ENGINES.md](./COMPRESSION_ENGINES.md) — Engine registry and built-in engines
|
||||
- [RTK_COMPRESSION.md](./RTK_COMPRESSION.md) — RTK engine and custom filters
|
||||
- [COMPRESSION_RULES_FORMAT.md](./COMPRESSION_RULES_FORMAT.md) — Rule pack format reference
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The compression system has **3 extension points**:
|
||||
|
||||
| Extension point | Use case | Difficulty |
|
||||
|-----------------|----------|------------|
|
||||
| **Custom engine** | Add a brand-new compression algorithm (e.g., domain-specific summarizer) | Advanced |
|
||||
| **Language pack** | Add support for a new natural language (e.g., Hindi, Arabic) | Medium |
|
||||
| **Stacked pipeline** | Compose existing engines in a custom order | Beginner |
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Compression Strategy │
|
||||
│ │
|
||||
│ Input messages ──▶ getEffectiveMode() ──▶ mode │
|
||||
│ │ │
|
||||
│ ┌───────────────────────┼──────────┐ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │ │
|
||||
│ "rtk" "lite" "standard" "stacked" │
|
||||
│ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │ │
|
||||
│ RTK Lite Caveman engines[] │
|
||||
│ engine engine engine chained │
|
||||
│ │ │ │ │ │ │
|
||||
│ └─────────┴─────────┴─────────┘ │ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Compressed output │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
The strategy selector is MODE-BASED: each request selects ONE mode
|
||||
(rtk / lite / standard / aggressive / ultra / stacked / off).
|
||||
Only mode "stacked" chains multiple engines in sequence.
|
||||
Default auto-trigger mode is "lite" (not a 3-tier priority chain).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Writing a Custom Compression Engine
|
||||
|
||||
The engine interface (`open-sse/services/compression/engines/types.ts`) is the contract every engine must satisfy. It has 5 required methods.
|
||||
|
||||
### The `CompressionEngine` Interface
|
||||
|
||||
```ts
|
||||
interface CompressionEngine {
|
||||
id: string; // Unique engine ID
|
||||
name: string; // Display name
|
||||
description: string; // Short description
|
||||
icon: string; // Icon (emoji or URL)
|
||||
targets: CompressionEngineTarget[]; // ["messages", "tool_results", "code_blocks"]
|
||||
stackable: boolean; // Can be used in a stacked pipeline
|
||||
stackPriority: number; // Order in stacked pipelines (lower = earlier)
|
||||
metadata: CompressionEngineMetadata;
|
||||
|
||||
apply(body, options?): CompressionResult;
|
||||
compress(body, config?): CompressionResult;
|
||||
getConfigSchema(): EngineConfigField[];
|
||||
validateConfig(config): EngineValidationResult;
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Example: Whitespace Engine
|
||||
|
||||
The simplest possible engine — strip extra whitespace from messages.
|
||||
|
||||
```ts
|
||||
import type { CompressionEngine } from "omniroute/compression/engines/types";
|
||||
import { registerCompressionEngine } from "omniroute/compression/engines/registry";
|
||||
|
||||
function preserveCodeBlocks(text: string): string {
|
||||
// Split by code block markers and preserve whitespace inside them
|
||||
const parts = text.split(/(```[\s\S]*?```)/);
|
||||
return parts
|
||||
.map((part) => {
|
||||
if (part.startsWith("```")) {
|
||||
return part; // Don't modify code blocks
|
||||
}
|
||||
return part.replace(/\n{3,}/g, "\n\n"); // Only apply to prose
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const whitespaceEngine: CompressionEngine = {
|
||||
id: "whitespace",
|
||||
name: "Whitespace Stripper",
|
||||
description: "Removes extra whitespace and blank lines",
|
||||
icon: "📝",
|
||||
targets: ["messages", "tool_results"],
|
||||
stackable: true,
|
||||
stackPriority: 100, // Run AFTER caveman/rtk
|
||||
|
||||
metadata: {
|
||||
id: "whitespace",
|
||||
name: "Whitespace Stripper",
|
||||
description: "Removes extra whitespace and blank lines",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 5,
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
|
||||
apply(body, options) {
|
||||
return this.compress(body, options?.config);
|
||||
},
|
||||
|
||||
compress(body, config = {}) {
|
||||
let originalLength = 0;
|
||||
let compressedLength = 0;
|
||||
|
||||
// Traverse message array — handle both string and multipart content
|
||||
const compressedBody = (body.messages || []).map((msg) => {
|
||||
if (typeof msg.content === "string") {
|
||||
originalLength += msg.content.length;
|
||||
let compressed = msg.content
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.replace(/^\s+|\s+$/gm, "");
|
||||
compressedLength += compressed.length;
|
||||
return { ...msg, content: compressed };
|
||||
}
|
||||
// Multipart content: traverse parts, compress text parts only
|
||||
if (Array.isArray(msg.content)) {
|
||||
const newParts = msg.content.map((part) => {
|
||||
if (part.type === "text" && typeof part.text === "string") {
|
||||
originalLength += part.text.length;
|
||||
let compressed = part.text
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.replace(/^\s+|\s+$/gm, "");
|
||||
compressedLength += compressed.length;
|
||||
return { ...part, text: compressed };
|
||||
}
|
||||
return part; // preserve image_url, tool_use, etc.
|
||||
});
|
||||
return { ...msg, content: newParts };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
|
||||
return {
|
||||
body: { ...body, messages: compressedBody },
|
||||
stats: {
|
||||
originalTokens: Math.ceil(originalLength / 4),
|
||||
compressedTokens: Math.ceil(compressedLength / 4),
|
||||
savingsPercent: originalLength > 0 ? 100 * (1 - compressedLength / originalLength) : 0,
|
||||
techniques: ["whitespace-collapse"],
|
||||
engineId: "whitespace",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
getConfigSchema() {
|
||||
return [
|
||||
{
|
||||
key: "preserveCodeBlocks",
|
||||
type: "boolean",
|
||||
label: "Preserve code blocks",
|
||||
defaultValue: true,
|
||||
description: "Don't touch whitespace inside ```code``` blocks",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
validateConfig(config) {
|
||||
if (config.preserveCodeBlocks !== undefined && typeof config.preserveCodeBlocks !== "boolean") {
|
||||
return { valid: false, errors: ["preserveCodeBlocks must be a boolean"] };
|
||||
}
|
||||
return { valid: true, errors: [] };
|
||||
},
|
||||
};
|
||||
|
||||
// Register globally
|
||||
registerCompressionEngine(whitespaceEngine);
|
||||
```
|
||||
|
||||
### Where to Place Custom Engines
|
||||
|
||||
```
|
||||
~/.omniroute/compression/engines/my-engine.ts # User-level
|
||||
<project>/compression-engines/my-engine.ts # Project-level (loaded on startup)
|
||||
```
|
||||
|
||||
Or load programmatically from a plugin:
|
||||
|
||||
```ts
|
||||
// In your plugin
|
||||
import { registerCompressionEngine, unregisterCompressionEngine } from "@omniroute/open-sse/services/compression/engines/registry";
|
||||
import { myEngine } from "./engines/my-engine";
|
||||
|
||||
export default definePlugin({
|
||||
name: "my-compression-plugin",
|
||||
// The plugin SDK exposes onRequest / onResponse / onError hooks. Register the
|
||||
// engine when the plugin module loads (or on first onRequest); unregister it
|
||||
// from your own teardown path.
|
||||
onRequest: async (ctx) => {
|
||||
registerCompressionEngine(myEngine);
|
||||
},
|
||||
});
|
||||
|
||||
// On teardown:
|
||||
// unregisterCompressionEngine("my-engine");
|
||||
```
|
||||
|
||||
### Testing Your Engine
|
||||
|
||||
Register your engine in a plugin or startup function. Once registered, the engine will be available
|
||||
in the strategy selector via its `id`. Test integration by composing it in a stacked pipeline:
|
||||
|
||||
---
|
||||
|
||||
## Creating Language Packs
|
||||
|
||||
Caveman-style compression uses **language-specific rule packs** to handle fillers, hedging, and verbose patterns in each natural language. OmniRoute ships with **6 language packs**: `en`, `es`, `fr`, `de`, `ja`, `pt-BR`.
|
||||
|
||||
### Pack Structure
|
||||
|
||||
A language pack is a directory of **JSON files** under `open-sse/services/compression/rules/<language>/`:
|
||||
|
||||
```
|
||||
open-sse/services/compression/rules/
|
||||
├── en/
|
||||
│ ├── filler.json # Pleasantries, hedging, politeness
|
||||
│ ├── context.json # Context-reducing rules
|
||||
│ ├── dedup.json # Deduplication rules
|
||||
│ ├── structural.json # Punctuation, formatting
|
||||
│ └── ultra.json # Aggressive compression rules
|
||||
├── es/ (same structure)
|
||||
├── fr/ (same structure)
|
||||
├── de/ (same structure)
|
||||
├── ja/ (same structure)
|
||||
└── pt-BR/ (same structure)
|
||||
```
|
||||
|
||||
### Rule Anatomy
|
||||
|
||||
Each rule has this shape (from `open-sse/services/compression/ruleLoader.ts`):
|
||||
|
||||
```ts
|
||||
interface FileRule {
|
||||
name: string; // Human-readable name (kebab-case)
|
||||
pattern: string; // JavaScript regex pattern
|
||||
replacement?: string; // What to replace the match with
|
||||
replacementMap?: Record<string, string>; // OR a key→replacement map
|
||||
flags?: string; // Regex flags ("gi" typically)
|
||||
context?: "all" | "user" | "system" | "assistant";
|
||||
category?: "filler" | "context" | "structural" | "dedup" | "terse" | "ultra";
|
||||
minIntensity?: "lite" | "full" | "ultra"; // Skip below this intensity
|
||||
description?: string; // Documentation
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Adding Hindi Filler Rules
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "hi",
|
||||
"category": "filler",
|
||||
"rules": [
|
||||
{
|
||||
"name": "polite_opener",
|
||||
"pattern": "\\b(?:नमस्ते|नमस्कार|आदरणीय)\\b[,!\\s]*",
|
||||
"replacement": "",
|
||||
"context": "all",
|
||||
"category": "filler",
|
||||
"minIntensity": "lite",
|
||||
"description": "Strip polite openers like 'नमस्ते'"
|
||||
},
|
||||
{
|
||||
"name": "filler_actually",
|
||||
"pattern": "\\b(?:असल में|वास्तव में|दरअसल)\\b\\s*",
|
||||
"replacement": "",
|
||||
"context": "all",
|
||||
"category": "filler",
|
||||
"minIntensity": "lite",
|
||||
"description": "Strip 'actually' fillers"
|
||||
},
|
||||
{
|
||||
"name": "verbose_plea",
|
||||
"pattern": "\\b(?:कृपया|कृपया आप|अनुरोध है कि आप)\\b\\s*",
|
||||
"replacement": "",
|
||||
"context": "all",
|
||||
"category": "filler",
|
||||
"minIntensity": "full",
|
||||
"description": "Strip 'please' in Hindi"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Rule packs are validated against `_schema.json` on load. A pack with bad structure will fail to load and log an error:
|
||||
|
||||
```
|
||||
RULE_LOADER: pack "hi/filler.json" failed validation:
|
||||
- rules.0.pattern: Invalid regex
|
||||
- rules.1.context: must be one of [all, user, system, assistant]
|
||||
```
|
||||
|
||||
Validation runs automatically when a pack is loaded (against `_schema.json`); an
|
||||
invalid pack is rejected and the error above is logged. There is no separate
|
||||
`npm run` script for pack validation — load the pack (e.g. start the server or
|
||||
exercise the compression path) and watch the logs.
|
||||
|
||||
### Loading a Custom Language Pack
|
||||
|
||||
```ts
|
||||
import { loadRulePack } from "omniroute/compression/ruleLoader";
|
||||
|
||||
await loadRulePack("./my-custom-rules/hi/filler.json");
|
||||
```
|
||||
|
||||
Or place in a recognized location:
|
||||
|
||||
```
|
||||
~/.omniroute/compression/rules/hi/filler.json # User-level
|
||||
<project>/.compression/rules/hi/filler.json # Project-level
|
||||
```
|
||||
|
||||
### Best Practices for Language Packs
|
||||
|
||||
1. **Start with `filler`** — these are the highest-impact rules
|
||||
2. **Use `minIntensity`** to gate aggressive rules — protects against over-compression
|
||||
3. **Include test cases** — add `tests[]` array in the JSON to verify behavior
|
||||
4. **Order matters** — earlier rules apply first; place high-impact rules first
|
||||
5. **Be conservative with `replacement`** — empty string is usually correct; never introduce new content
|
||||
|
||||
### Translation Strategy
|
||||
|
||||
When localizing rule packs to a new language:
|
||||
|
||||
1. **Translate the rule names** — they appear in debug output
|
||||
2. **Adapt the regex patterns** — direct translation often fails (word boundaries differ)
|
||||
3. **Test against real conversations** — the pack should be safe on actual input
|
||||
4. **Match cultural conventions** — Japanese packs, for instance, have more honorific fillers than English
|
||||
|
||||
---
|
||||
|
||||
## Stacked Pipelines
|
||||
|
||||
A **stacked pipeline** runs multiple engines in sequence, with each engine's output feeding the next. This is how `mode: stacked` works internally.
|
||||
|
||||
### How Stacking Works
|
||||
|
||||
```
|
||||
Input (10,000 tokens)
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Engine │ priority 10
|
||||
│ A │ ──▶ output: 6,000 tokens (-40%)
|
||||
└────┬─────┘
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Engine │ priority 50
|
||||
│ B │ ──▶ output: 2,400 tokens (-60%)
|
||||
└────┬─────┘
|
||||
▼
|
||||
┌──────────┐
|
||||
│ Engine │ priority 100
|
||||
│ C │ ──▶ output: 1,200 tokens (-80%)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
Final output (1,200 tokens, ~88% savings combined)
|
||||
```
|
||||
|
||||
When `mode: "stacked"` is selected, engines execute sequentially in the order specified in the `pipeline` array.
|
||||
The output of engine N becomes the input of engine N+1.
|
||||
|
||||
### Compression Modes
|
||||
|
||||
OmniRoute selects **ONE mode per request** based on configuration, auto-trigger thresholds, and combo overrides.
|
||||
The available modes are defined in `open-sse/services/compression/types.ts` (type `CompressionMode`):
|
||||
|
||||
| Mode | Engines | Use case |
|
||||
|------|---------|----------|
|
||||
| `off` | None | Disable all compression |
|
||||
| `rtk` | RTK only | Command-output heavy sessions (80%+ savings) |
|
||||
| `lite` | Lite only | Conservative compression (fast, safe) |
|
||||
| `standard` | Caveman | Prose compression with language packs |
|
||||
| `aggressive` | Caveman + Aggressive | Aggressive prose + aggressive final pass |
|
||||
| `ultra` | Ultra | Maximum compression (lossy, last resort) |
|
||||
| `stacked` | Custom pipeline | Compose engines in any order (see below) |
|
||||
|
||||
Mode selection is determined by `getEffectiveMode()` in `open-sse/services/compression/strategySelector.ts`:
|
||||
|
||||
1. If compression is disabled: `"off"`
|
||||
2. If a combo override exists: use the override
|
||||
3. If auto-trigger threshold is exceeded: use `autoTriggerMode` (default: `"lite"`)
|
||||
4. Otherwise: use `defaultMode`
|
||||
|
||||
### The Default Stacked Pipeline
|
||||
|
||||
When `mode: "stacked"` is explicitly configured, the default pipeline composes:
|
||||
|
||||
1. **RTK** — strip command output noise (~80% savings on terminal output)
|
||||
2. **Caveman** — remove fillers, terse-ify prose (~46% on remaining text)
|
||||
3. **Lite** — final whitespace + dedup pass
|
||||
|
||||
This composition achieves **78-95% savings** on tool-heavy sessions.
|
||||
|
||||
### Configuring Stacked Pipelines
|
||||
|
||||
In combo config:
|
||||
|
||||
```json
|
||||
{
|
||||
"compression": {
|
||||
"mode": "stacked",
|
||||
"pipeline": [
|
||||
{ "engine": "rtk", "config": { "intensity": "aggressive" } },
|
||||
{ "engine": "caveman", "config": { "intensity": "full" } },
|
||||
{ "engine": "lite", "config": {} }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can omit engines, add custom ones, or reorder them.
|
||||
|
||||
### State Passing
|
||||
|
||||
Engines can read metadata from the request context (in `options`):
|
||||
|
||||
```ts
|
||||
compress(body, config) {
|
||||
// Read metadata from previous engines
|
||||
const original = options?.compressionComboId; // "my-coding-combo"
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The metadata is **read-only** — engines cannot mutate the request context, only their own body output.
|
||||
|
||||
### Execution Order Gotchas
|
||||
|
||||
| Engine order | Effect |
|
||||
|--------------|--------|
|
||||
| RTK → Caveman → Lite | **Recommended** (strips noise first, then language, then whitespace) |
|
||||
| Lite → RTK → Caveman | Bad — Lite strips whitespace from raw output, making RTK pattern matching fail |
|
||||
| Caveman → RTK | Bad — Caveman may rewrite text in ways that RTK doesn't recognize |
|
||||
| Any order with `tool_results` first | Better — tool output is the noisiest content |
|
||||
|
||||
### When NOT to Stack
|
||||
|
||||
Stacking isn't always better:
|
||||
- **Simple messages** (no tool output) — single Caveman or Lite is enough
|
||||
- **Cost-sensitive** — each engine adds ~5-50ms latency
|
||||
- **Specific tools** — RTK alone is usually sufficient for shell output
|
||||
|
||||
### Building a Custom Pipeline
|
||||
|
||||
There is no named-pipeline registry. A stacked pipeline is just an **inline array
|
||||
of steps** passed to `applyStackedCompression()` (exported from
|
||||
`@omniroute/open-sse/services/compression/strategySelector`):
|
||||
|
||||
```ts
|
||||
import { applyStackedCompression } from "@omniroute/open-sse/services/compression/strategySelector";
|
||||
|
||||
const result = applyStackedCompression(body, [
|
||||
{ engine: "rtk", intensity: "aggressive" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
]);
|
||||
```
|
||||
|
||||
When you don't pass a pipeline, it defaults to `rtk(standard) → caveman(full)`.
|
||||
|
||||
To drive it from config, set `mode: "stacked"` and provide the step array under
|
||||
`stackedPipeline` (read from `config.stackedPipeline`):
|
||||
|
||||
```json
|
||||
{
|
||||
"compression": {
|
||||
"mode": "stacked",
|
||||
"stackedPipeline": [
|
||||
{ "engine": "rtk", "intensity": "aggressive" },
|
||||
{ "engine": "caveman", "intensity": "full" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Engine Development
|
||||
|
||||
1. **Always implement `validateConfig`** — engines without validation cause silent failures
|
||||
2. **Set realistic `targetLatencyMs`** — used by the strategy selector to choose engines
|
||||
3. **Use `getConfigSchema` for the dashboard** — never hide config from users
|
||||
4. **Support `stackable: true` if your engine is pure** — engines with side effects shouldn't stack
|
||||
5. **Write inline tests** — engines should be verifiable in <1s
|
||||
|
||||
### Language Pack Development
|
||||
|
||||
1. **Start with `lite` intensity** — your rules should be safe at the lowest setting
|
||||
2. **Use `context` to scope rules** — `user` only rules can't accidentally affect system prompts
|
||||
3. **Avoid capturing JSON keys** — `\\bword\\b` can match inside JSON, breaking structured data
|
||||
4. **Test with edge cases** — empty input, unicode, RTL text, emojis
|
||||
5. **Use existing packs as templates** — `en/filler.json` is the most-developed example
|
||||
|
||||
### Pipeline Design
|
||||
|
||||
1. **Profile before optimizing** — measure with `compression_stats` first
|
||||
2. **Prefer composition over reimplementation** — extend Caveman rules before writing a new engine
|
||||
3. **Document the order rationale** — comment why engine A before engine B
|
||||
4. **Test at all 3 intensity levels** — `lite` is fast but lossy, `ultra` is slow but precise
|
||||
|
||||
---
|
||||
|
||||
## Reference: Built-in Engines
|
||||
|
||||
| Engine ID | Stackable | Default stackPriority | Targets |
|
||||
|-----------|-----------|-----------------------|---------|
|
||||
| `lite` | Yes | 5 | messages, tool_results |
|
||||
| `rtk` | Yes | 10 | tool_results |
|
||||
| `standard` (caveman) | Yes | 20 | messages, tool_results, code_blocks |
|
||||
| `aggressive` | Yes | 30 | messages |
|
||||
| `ultra` | Yes | 40 | messages, code_blocks |
|
||||
### See Also
|
||||
|
||||
- [COMPRESSION_GUIDE.md](./COMPRESSION_GUIDE.md) — Pipeline overview
|
||||
- [COMPRESSION_ENGINES.md](./COMPRESSION_ENGINES.md) — Engine registry reference
|
||||
- [COMPRESSION_RULES_FORMAT.md](./COMPRESSION_RULES_FORMAT.md) — Rule format spec
|
||||
- [COMPRESSION_LANGUAGE_PACKS.md](./COMPRESSION_LANGUAGE_PACKS.md) — Language pack details
|
||||
- [RTK_COMPRESSION.md](./RTK_COMPRESSION.md) — RTK engine and custom filters
|
||||
- Source: `open-sse/services/compression/` (117 files, ~250KB)
|
||||
@@ -238,3 +238,367 @@ node --import tsx/esm --test \
|
||||
4. Add command detection coverage when introducing a new output class.
|
||||
5. Run the verify and broad RTK gates.
|
||||
6. If the filter is project-local, commit `.rtk/filters.json` and refresh `.rtk/trust.json` only after review.
|
||||
|
||||
---
|
||||
|
||||
## Intensity Levels (v3.8.16+)
|
||||
|
||||
RTK supports **3 intensity levels** that trade off between **compression aggressiveness** and **safety**. The level is set via `config.intensity` in the engine config.
|
||||
|
||||
### The 3 Levels
|
||||
|
||||
| Level | Truncation threshold | Token savings | Risk | Best for |
|
||||
|-------|---------------------|---------------|------|----------|
|
||||
| `minimal` | 24 lines per section | ~20-40% | Very low | Production with critical context |
|
||||
| `standard` (default) | 24 lines per section | ~50-70% | Low | Daily coding sessions |
|
||||
| `aggressive` | 16 lines per section | ~70-90% | Medium | Long sessions, max savings |
|
||||
|
||||
### Where the Truncation Happens
|
||||
|
||||
The truncation threshold affects `lineFilter.ts`:
|
||||
|
||||
```ts
|
||||
// From open-sse/services/compression/engines/rtk/index.ts:329-330
|
||||
config.intensity === "aggressive" ? 16 : 24,
|
||||
config.intensity === "aggressive" ? 16 : 24,
|
||||
```
|
||||
|
||||
Both the **head** and **tail** of each section are preserved; middle content is dropped when truncation kicks in.
|
||||
|
||||
### What Stays vs. What Gets Cut
|
||||
|
||||
| Content | minimal | standard | aggressive |
|
||||
|---------|---------|----------|------------|
|
||||
| Errors / stack traces | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test failures | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Build errors | ✅ preserved | ✅ preserved | ✅ preserved |
|
||||
| Test passes (verbose) | ✅ preserved | 🟡 collapsed | 🟡 collapsed |
|
||||
| Routine output (info logs) | 🟡 collapsed | 🟡 collapsed | ❌ dropped |
|
||||
| Progress bars | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
| Banner / ASCII art | 🟡 collapsed | ❌ dropped | ❌ dropped |
|
||||
|
||||
### Choosing the Right Intensity
|
||||
|
||||
```
|
||||
Is losing context catastrophic?
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
│ │ │
|
||||
YES NO NOT SURE
|
||||
│ │ │
|
||||
▼ │ │
|
||||
minimal │ │
|
||||
│ │ │
|
||||
│ ▼ ▼
|
||||
│ How critical Try `standard` first
|
||||
│ is throughput? (works for 80% of
|
||||
│ │ cases)
|
||||
│ ┌────┴────┐
|
||||
│ │ │
|
||||
│ LOW HIGH
|
||||
│ │ │
|
||||
│ ▼ ▼
|
||||
│ standard aggressive
|
||||
│ │ │
|
||||
└──────┴─────────┘
|
||||
```
|
||||
|
||||
### Configuring Intensity
|
||||
|
||||
**Per-combo** (in combo config):
|
||||
|
||||
```json
|
||||
{
|
||||
"combo": "my-coding-combo",
|
||||
"routing": { /* ... */ },
|
||||
"compression": {
|
||||
"engine": "rtk",
|
||||
"intensity": "aggressive"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Programmatically**:
|
||||
|
||||
`rtkEngine` (`@omniroute/open-sse/services/compression/engines/rtk`) is a
|
||||
`CompressionEngine` and has no `updateConfig` method. Update an engine's config
|
||||
through the registry helper instead:
|
||||
|
||||
```ts
|
||||
import { updateEngineConfig } from "@omniroute/open-sse/services/compression/engines/registry";
|
||||
|
||||
updateEngineConfig("rtk", { intensity: "aggressive" });
|
||||
```
|
||||
|
||||
### Verifying the Effect
|
||||
Use the **Verify Gate** (see below) to confirm your filter is safe at your chosen intensity:
|
||||
```ts
|
||||
import { runRtkFilterTests } from "omniroute/compression/engines/rtk/verify";
|
||||
|
||||
const result = runRtkFilterTests({ intensity: "aggressive" });
|
||||
if (!result.passed) {
|
||||
console.error("Filters failed at aggressive intensity");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Filter Development (v3.8.16+)
|
||||
|
||||
The `engines/rtk/filters/` directory contains **49+ built-in filter JSON files**. You can add your own to compress output from custom tools not covered by the defaults.
|
||||
|
||||
### Filter Schema (Zod)
|
||||
|
||||
```ts
|
||||
{
|
||||
"id": "string", // Required. Filter identifier (kebab-case, e.g., "python-traceback")
|
||||
"label": "string", // Required. Human-readable filter name
|
||||
"description": "string", // Optional (default: ""). Short description of what filter does
|
||||
"category": "git|test|build|shell|docker|package|infra|cloud|generic",
|
||||
"priority": number, // Optional (0-100, default: 50). Execution order (higher = first)
|
||||
"match": {
|
||||
"commands": ["string"], // Command names to match (e.g., "python", "pytest")
|
||||
"patterns": ["string"], // Regex patterns to match output
|
||||
"outputTypes": ["string"] // Detected output classes (e.g., "test-failure")
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": boolean, // Optional (default: false). Strip ANSI color codes
|
||||
"replace": [ // Find-and-replace rules (default: [])
|
||||
{ "pattern": "regex", "replacement": "..." }
|
||||
],
|
||||
"matchOutput": [ // Short-circuit on pattern match (default: [])
|
||||
{
|
||||
"pattern": "regex",
|
||||
"message": "short summary",
|
||||
"unless": "regex" // Skip if this pattern matches
|
||||
}
|
||||
],
|
||||
"includePatterns": ["string"], // Lines to keep (regex patterns, default: [])
|
||||
"dropPatterns": ["string"], // Lines to drop (regex patterns, default: [])
|
||||
"collapsePatterns": ["string"], // Lines to collapse to single occurrence (default: [])
|
||||
"deduplicate": boolean, // Optional (default: false). Remove duplicate lines
|
||||
"truncateLineAt": number, // Optional (default: 0). Truncate lines to max chars
|
||||
"maxLines": number, // Optional (default: 0). Hard cap on total lines
|
||||
"headLines": number, // Optional (default: 20). Keep first N lines of matched output
|
||||
"tailLines": number, // Optional (default: 20). Keep last N lines of matched output
|
||||
"onEmpty": "string", // Optional (default: ""). Fallback message if all lines filtered
|
||||
"filterStderr": boolean // Optional (default: false). Also filter stderr output
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["string"], // Patterns that must always be preserved (default: [])
|
||||
"summaryPatterns": ["string"] // Patterns for final summary line (default: [])
|
||||
},
|
||||
"tests": [ // Inline tests for verification (default: [])
|
||||
{
|
||||
"name": "string", // Required. Test name
|
||||
"input": "sample output", // Required. Sample input text
|
||||
"expected": "expected output", // Required. Expected compressed output
|
||||
"command": "optional command" // Optional. Command context
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Python Traceback Filter
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "python-traceback",
|
||||
"label": "Python Traceback Filter",
|
||||
"description": "Compresses Python tracebacks to essential file/line locations and error type",
|
||||
"category": "test",
|
||||
"priority": 60,
|
||||
"match": {
|
||||
"commands": ["python", "python3", "pytest", "uv", "poetry"],
|
||||
"patterns": ["Traceback \\(most recent call last\\)", "Error", "Exception"],
|
||||
"outputTypes": ["error-traceback"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"Traceback \\(most recent call last\\)",
|
||||
"^\\s*File \".+\", line \\d+",
|
||||
"^\\s*[A-Z][a-zA-Z]+Error:",
|
||||
"^\\s*[A-Z][a-zA-Z]+Exception"
|
||||
],
|
||||
"dropPatterns": [
|
||||
"site-packages/",
|
||||
"^\\s+[a-z_]+\\([^)]*\\)$"
|
||||
],
|
||||
"headLines": 5,
|
||||
"tailLines": 3,
|
||||
"maxLines": 25,
|
||||
"filterStderr": true
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [
|
||||
"Error:",
|
||||
"Exception:",
|
||||
"Traceback"
|
||||
],
|
||||
"summaryPatterns": [
|
||||
"^[A-Z][a-zA-Z]+(?:Error|Exception):"
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "preserves-error-type-and-location",
|
||||
"input": "Traceback (most recent call last):\n File \"app.py\", line 42, in main\n do_thing()\n File \"lib/utils.py\", line 17, in helper\n return 1 / 0\nZeroDivisionError: division by zero",
|
||||
"expected": "Traceback (most recent call last):\n File \"app.py\", line 42, in main\n File \"lib/utils.py\", line 17, in helper\nZeroDivisionError: division by zero",
|
||||
"command": "python app.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Custom Filters
|
||||
|
||||
Place the file in a recognized location:
|
||||
|
||||
```
|
||||
~/.omniroute/rtk/filters/my-filter.json # User-level
|
||||
<project>/.rtk/filters/my-filter.json # Project-level
|
||||
```
|
||||
|
||||
Filters are loaded automatically on startup via `loadRtkFilters()` in `open-sse/services/compression/engines/rtk/filterLoader.ts`. The loader discovers filters from:
|
||||
|
||||
- Built-in catalog: `open-sse/services/compression/engines/rtk/filters/`
|
||||
- User directory: `~/.omniroute/rtk/filters/`
|
||||
- Project directory: `<project>/.rtk/filters/`
|
||||
|
||||
To load filters programmatically:
|
||||
|
||||
```ts
|
||||
import { loadRtkFilters } from "@omniroute/open-sse/services/compression/engines/rtk/filterLoader";
|
||||
|
||||
// Options: customFiltersEnabled (load user/project filters, default on),
|
||||
// trustProjectFilters, refresh.
|
||||
const filters = loadRtkFilters({ customFiltersEnabled: true });
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Filters are validated against the Zod schema on load. A filter with bad structure will fail to load and log an error:
|
||||
|
||||
```
|
||||
RTK_FILTER_LOADER: filter "my-filter" failed validation:
|
||||
- rules.replace.0.pattern: Invalid regex
|
||||
- match.commands: must not be empty
|
||||
```
|
||||
|
||||
To validate all installed filters, call `runRtkFilterTests()` which is exported from `open-sse/services/compression/engines/rtk/verify.ts`.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always include `tests[]`** — they prove your filter works and prevent regressions
|
||||
2. **Use `matchOutput` for short-circuits** — if a single line tells the story, replace the whole block
|
||||
3. **Prefer `keep` over `strip`** — explicit "always preserve" rules are safer than "always remove"
|
||||
4. **Test at all 3 intensity levels** — `minimal` should be a no-op, `aggressive` should still preserve errors
|
||||
5. **Use the `unless` field** — guard short-circuits with "don't trigger if X is present"
|
||||
|
||||
---
|
||||
|
||||
## Raw Output Recovery & Verify Gate
|
||||
|
||||
When RTK compresses output aggressively, you can **recover the original text** for debugging, audit, or replay.
|
||||
|
||||
### How Raw Output Recovery Works
|
||||
|
||||
```
|
||||
Original output (10K tokens)
|
||||
│
|
||||
▼
|
||||
RTK compress (with rawOutput.enabled=true)
|
||||
│
|
||||
├─▶ Compressed output (2K tokens) ──▶ to LLM
|
||||
│
|
||||
└─▶ Original output (10K tokens) ──▶ stored in DB
|
||||
(linked by request_id)
|
||||
```
|
||||
|
||||
### Enabling Raw Output Storage
|
||||
|
||||
**Per-request** (in combo config):
|
||||
|
||||
```json
|
||||
{
|
||||
"compression": {
|
||||
"engine": "rtk",
|
||||
"intensity": "aggressive",
|
||||
"rawOutput": {
|
||||
"enabled": true,
|
||||
"maxBytes": 1048576 // 1MB cap
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default**: `rawOutput.enabled: false` (saves storage).
|
||||
|
||||
### Storage Cost
|
||||
|
||||
| Per-request | 1MB cap | 10MB cap |
|
||||
|-------------|---------|----------|
|
||||
| Average compressed output | ~5KB | ~5KB |
|
||||
| Raw output stored | ~50-500KB | ~500KB-5MB |
|
||||
| With 1000 requests/day | 50-500MB/day | 500MB-5GB/day |
|
||||
|
||||
> **Recommendation**: Only enable raw output for **debugging sessions** or **sampled auditing**, not always-on.
|
||||
|
||||
### Recovering the Original
|
||||
|
||||
```ts
|
||||
import { readRtkRawOutput } from "omniroute/compression/engines/rtk/rawOutput";
|
||||
|
||||
const raw = readRtkRawOutput(pointerId); // pointerId from compression stats
|
||||
if (raw) {
|
||||
console.log("Original output:", raw);
|
||||
}
|
||||
```
|
||||
|
||||
The `pointerId` is returned in `CompressionStats.rtkRawOutputPointers[]` after compression.
|
||||
See `open-sse/services/compression/engines/rtk/rawOutput.ts:102` for the function signature.
|
||||
|
||||
### The Verify Gate
|
||||
|
||||
The **RTK Filter Verification** (`open-sse/services/compression/engines/rtk/verify.ts`) validates all filters against their `tests[]` and ensures behavior is correct at all 3 intensity levels.
|
||||
|
||||
**Call `runRtkFilterTests()`** to run verification:
|
||||
|
||||
```ts
|
||||
import { runRtkFilterTests } from "open-sse/services/compression/engines/rtk/verify";
|
||||
|
||||
const result = runRtkFilterTests();
|
||||
console.log(`Passed: ${result.outcomes.filter(o => o.passed).length}`);
|
||||
console.log(`Failed: ${result.outcomes.filter(o => !o.passed).length}`);
|
||||
if (!result.passed) {
|
||||
console.error("Filters failed verification");
|
||||
result.outcomes.filter(o => !o.passed).forEach(o => {
|
||||
console.error(` - ${o.filterId} / ${o.testName}: expected "${o.expected}", got "${o.actual}"`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**What it validates**:
|
||||
|
||||
1. Every filter loads and passes schema validation
|
||||
2. Every `tests[]` entry produces expected output
|
||||
3. `minimal` intensity is a no-op (preserves original, only applies structural filters)
|
||||
4. `aggressive` intensity preserves errors, test failures, and stack traces
|
||||
5. Compressed output is never larger than original input
|
||||
|
||||
- Source: `open-sse/services/compression/engines/rtk/` (63 files, ~70KB)
|
||||
|
||||
- **Before merging a filter change** — always ensure tests pass
|
||||
- **After upgrading RTK engine** — schema may have changed
|
||||
- **Periodically in monitoring** — protects against drift in test fixtures
|
||||
- **When adding a new tool/command family** — proves the new filter works
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [COMPRESSION_GUIDE.md](./COMPRESSION_GUIDE.md) — Full compression pipeline overview
|
||||
- [COMPRESSION_ENGINES.md](./COMPRESSION_ENGINES.md) — Engine registry and built-in engines
|
||||
- [EXTENDING_COMPRESSION.md](./EXTENDING_COMPRESSION.md) — Custom engines, language packs, stacked pipelines
|
||||
- Source: `open-sse/services/compression/engines/rtk/` (63 files, ~70KB)
|
||||
|
||||
108
docs/fix-opencode-context.md
Normal file
108
docs/fix-opencode-context.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Fix: OpenCode combo context window detection
|
||||
|
||||
## Bug
|
||||
|
||||
OpenCode v1's `opencode.json` requires an explicit `limit.context` (and
|
||||
`limit.output`) for every model. Without these fields, OpenCode's heuristic
|
||||
kicks in and reports a wrong context window.
|
||||
|
||||
The OmniRoute `/v1/models` catalog is the single source of truth for context
|
||||
windows. The bug was that some combos were published to the catalog WITHOUT
|
||||
a computed `context_length`, so any OpenCode client pulling the catalog got
|
||||
no answer for them.
|
||||
|
||||
## Root cause
|
||||
|
||||
The "Opencode FREE Omni" combo references four `opencode/<model>` targets
|
||||
(`big-pickle`, `deepseek-v4-flash-free`, `minimax-m3-free`,
|
||||
`nemotron-3-super-free`). The catalog's `buildComboCatalogMetadata` computes
|
||||
the combo's `context_length` as the **minimum of its targets' known
|
||||
contexts**, but every target's lookup was returning `null`.
|
||||
|
||||
The lookup chain in `getCanonicalModelMetadata`:
|
||||
|
||||
1. `getSyncedCapability("opencode", "big-pickle")` → returns `null` because
|
||||
the DB row is stored under `provider = "opencode-zen"`, not `"opencode"`.
|
||||
2. `getRegistryModel("opencode", "big-pickle")` → returns `null` because
|
||||
`PROVIDER_MODELS["opencode"]` (and `["oc"]`) don't have static entries
|
||||
for these models.
|
||||
3. `getModelSpec("big-pickle")` → returns `null` (no static spec).
|
||||
|
||||
All three lookups fail → metadata is `null` → combo has no context.
|
||||
|
||||
Why was the DB row under `"opencode-zen"` and not `"opencode"`?
|
||||
`MODELS_DEV_PROVIDER_MAP["opencode"]` was `["opencode-zen"]` only — a
|
||||
historical one-way mapping. New syncs continued to write under the alias
|
||||
side of the pair, while the catalog & combo targets use the canonical id
|
||||
side.
|
||||
|
||||
## Fix (3 layers, in this order)
|
||||
|
||||
### 1. `src/lib/modelsDevSync.ts` — symmetric `mapProviderId` mapping
|
||||
|
||||
```ts
|
||||
// Before:
|
||||
opencode: ["opencode-zen"],
|
||||
"opencode-go": ["opencode-go"],
|
||||
|
||||
// After:
|
||||
opencode: ["opencode", "opencode-zen"],
|
||||
"opencode-go": ["opencode-go", "opencode-zen"],
|
||||
```
|
||||
|
||||
Now models.dev data lands under BOTH the canonical id and the historical
|
||||
alias, so any future sync keeps the lookup paths in sync.
|
||||
|
||||
### 2. `src/lib/modelsDevSync.ts` — alias-aware fallback in `getSyncedCapability`
|
||||
|
||||
The runtime fix that takes effect **immediately**, without waiting for a
|
||||
re-sync. Existing DB rows under `"opencode-zen"` are now found when
|
||||
callers pass `"opencode"` (or vice-versa).
|
||||
|
||||
```ts
|
||||
const SYNCED_CAPABILITY_FALLBACK_ALIASES: Record<string, string[]> = {
|
||||
opencode: ["opencode-zen"],
|
||||
"opencode-zen": ["opencode"],
|
||||
"opencode-go": ["opencode-zen"],
|
||||
};
|
||||
```
|
||||
|
||||
### 3. `src/lib/cli-helper/config-generator/opencode.ts` — drop the hardcoded 128K fallback
|
||||
|
||||
The previous band-aid hardcoded `FALLBACK_CONTEXT_LENGTH = 128_000` for
|
||||
models whose context was unknown. That's wrong: combos like "Opencode
|
||||
FREE Omni" should report **200K** (the min of their 200K targets), not
|
||||
the universal 128K default.
|
||||
|
||||
The generator now:
|
||||
|
||||
- Uses the catalog as the **single source of truth** for `limit.context`.
|
||||
- Emits the model **without** `limit.context` if the catalog has no entry
|
||||
— OpenCode's own heuristic applies and the user can fix the upstream.
|
||||
- **Throws** if the catalog fetch fails outright — the CLI catches and
|
||||
surfaces the error. We never silently write a stale opencode.json.
|
||||
|
||||
## Deployment steps (for the operator)
|
||||
|
||||
1. Pull the latest from the branch:
|
||||
`git fetch && git checkout fix/opencode-context-window`
|
||||
2. Rebuild OmniRoute: `npm run build`
|
||||
3. Restart the OmniRoute server (kill the running process and re-run).
|
||||
4. Trigger a models.dev sync from the Settings → Models.dev panel
|
||||
(or POST `/api/settings/models-dev` with `{"action": "sync"}`).
|
||||
5. Re-run the opencode.json generator (the CLI command or the
|
||||
`scripts/regen-opencode-config.ts` script).
|
||||
|
||||
After step 5, `Opencode FREE Omni`'s `limit.context` will be **200000**
|
||||
and every other combo will reflect its targets' min context.
|
||||
|
||||
## Verification
|
||||
|
||||
After the rebuild, hit `GET /v1/models` and inspect the response:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:20128/v1/models \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
| jq '.data[] | select(.id == "Opencode FREE Omni") | .context_length'
|
||||
# → 200000
|
||||
```
|
||||
564
docs/frameworks/ACP.md
Normal file
564
docs/frameworks/ACP.md
Normal file
@@ -0,0 +1,564 @@
|
||||
---
|
||||
title: ACP (Agent Client Protocol)
|
||||
---
|
||||
|
||||
# ACP (Agent Client Protocol)
|
||||
|
||||
> **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex, Gemini CLI) as child processes instead of using HTTP APIs. This gives you "CLI-as-backend" transport.
|
||||
|
||||
---
|
||||
|
||||
## What Is ACP?
|
||||
|
||||
ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. Instead of intercepting HTTP API calls to AI providers, ACP **spawns CLI agents as child processes** and feeds prompts through their native interface.
|
||||
|
||||
### Why Use ACP?
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **No API keys needed** | Uses your existing CLI authentication |
|
||||
| **Native protocol** | Uses each CLI's native input/output format |
|
||||
| **Auto-discovery** | Detects installed CLIs on your system |
|
||||
| **14 built-in agents** | Pre-configured for popular CLI tools |
|
||||
| **Custom agents** | Add your own CLI tools via settings |
|
||||
| **Process management** | Handles lifecycle (spawn, send, kill) |
|
||||
|
||||
---
|
||||
|
||||
## Supported CLI Agents
|
||||
|
||||
ACP supports **14 built-in CLI agents** out of the box:
|
||||
|
||||
| Agent ID | Display Name | Binary | Protocol |
|
||||
|----------|--------------|--------|----------|
|
||||
| `codex` | OpenAI Codex CLI | `codex` | stdio |
|
||||
| `claude` | Claude Code CLI | `claude` | stdio |
|
||||
| `goose` | Goose CLI | `goose` | stdio |
|
||||
| `gemini-cli` | Gemini CLI | `gemini` | stdio |
|
||||
| `openclaw` | OpenClaw | `openclaw` | stdio |
|
||||
| `aider` | Aider | `aider` | stdio |
|
||||
| `opencode` | OpenCode | `opencode` | stdio |
|
||||
| `cline` | Cline | `cline` | stdio |
|
||||
| `qwen-code` | Qwen Code | `qwen` | stdio |
|
||||
| `forge` | ForgeCode | `forge` | stdio |
|
||||
| `amazon-q` | Amazon Q Developer | `q` | stdio |
|
||||
| `interpreter` | Open Interpreter | `interpreter` | stdio |
|
||||
| `cursor-cli` | Cursor CLI | `cursor` | stdio |
|
||||
| `warp` | Warp AI | `warp` | stdio |
|
||||
|
||||
### Custom Agents
|
||||
|
||||
You can add your own CLI agents via settings. Custom agents support the same features as built-in agents.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Install a CLI Agent
|
||||
|
||||
```bash
|
||||
# Example: Install Claude Code CLI
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Verify installation
|
||||
claude --version
|
||||
```
|
||||
|
||||
### Step 2: ACP Auto-Detection
|
||||
|
||||
ACP automatically detects installed CLI agents on your system. No configuration needed!
|
||||
|
||||
### Step 3: Use ACP Transport
|
||||
|
||||
Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available.
|
||||
|
||||
---
|
||||
|
||||
## How ACP Works
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ OmniRoute │
|
||||
│ (HTTP Proxy) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│ spawn()
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Child Process │
|
||||
│ (CLI Agent) │
|
||||
│ │
|
||||
│ stdin ◄──────┤ Send prompt
|
||||
│ stdout ──────►│ Receive response
|
||||
│ stderr ──────►│ Receive errors
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Process Lifecycle
|
||||
|
||||
1. **Spawn** — ACP creates a child process for the CLI agent
|
||||
2. **Send** — ACP writes prompts to the process's stdin
|
||||
3. **Receive** — ACP reads responses from stdout/stderr
|
||||
4. **Idle Detection** — ACP waits 2 seconds of inactivity before considering the response complete
|
||||
5. **Kill** — ACP terminates the process (SIGTERM, then SIGKILL after 5s)
|
||||
|
||||
### Communication Protocol
|
||||
|
||||
ACP uses **stdio** (standard input/output) for communication with CLI agents. The protocol is:
|
||||
|
||||
1. **Send prompt** — Write to stdin with a newline
|
||||
2. **Wait for response** — Read from stdout until idle (2s of no output)
|
||||
3. **Timeout** — Default 120 seconds (configurable)
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Registry Functions
|
||||
|
||||
#### `detectInstalledAgents()`
|
||||
|
||||
Detects all installed CLI agents on the system. Results are cached for 60 seconds.
|
||||
|
||||
```typescript
|
||||
import { detectInstalledAgents } from "@/lib/acp";
|
||||
|
||||
const agents = detectInstalledAgents();
|
||||
// Returns: CliAgentInfo[]
|
||||
|
||||
interface CliAgentInfo {
|
||||
id: string; // e.g., "codex", "claude"
|
||||
name: string; // Display name
|
||||
binary: string; // Binary name to spawn
|
||||
versionCommand: string; // Version detection command
|
||||
version: string | null; // Detected version (null if not installed)
|
||||
installed: boolean; // Whether the agent is installed
|
||||
providerAlias: string; // Provider ID in OmniRoute
|
||||
spawnArgs: string[]; // Arguments to pass when spawning
|
||||
protocol: "stdio" | "http"; // Communication protocol
|
||||
isCustom?: boolean; // Whether this is a user-defined custom agent
|
||||
}
|
||||
```
|
||||
|
||||
#### `getAvailableAgents()`
|
||||
|
||||
Gets only the agents that are installed and available for ACP.
|
||||
|
||||
```typescript
|
||||
import { getAvailableAgents } from "@/lib/acp";
|
||||
|
||||
const available = getAvailableAgents();
|
||||
// Returns: CliAgentInfo[] (only installed agents)
|
||||
```
|
||||
|
||||
#### `getAgentById(id)`
|
||||
|
||||
Gets a specific agent by ID.
|
||||
|
||||
```typescript
|
||||
import { getAgentById } from "@/lib/acp";
|
||||
|
||||
const agent = getAgentById("claude");
|
||||
// Returns: CliAgentInfo | undefined
|
||||
```
|
||||
|
||||
#### `setCustomAgents(agents)`
|
||||
|
||||
Sets custom agent definitions from settings.
|
||||
|
||||
```typescript
|
||||
import { setCustomAgents } from "@/lib/acp";
|
||||
|
||||
setCustomAgents([
|
||||
{
|
||||
id: "my-custom-cli",
|
||||
name: "My Custom CLI",
|
||||
binary: "mycli",
|
||||
versionCommand: "mycli --version",
|
||||
providerAlias: "my-provider",
|
||||
spawnArgs: [],
|
||||
protocol: "stdio",
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### Manager Functions
|
||||
|
||||
#### `acpManager.spawn(agentId, binary, args, env)`
|
||||
|
||||
Spawns a new CLI agent process.
|
||||
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
const session = acpManager.spawn(
|
||||
"claude",
|
||||
"claude",
|
||||
["--print", "--output-format", "json"],
|
||||
{ /* custom env vars */ }
|
||||
);
|
||||
// Returns: AcpSession
|
||||
```
|
||||
|
||||
**Allowed agent IDs**: `["claude", "codex", "gemini", "qwen"]`
|
||||
|
||||
#### `acpManager.sendPrompt(sessionId, prompt, timeoutMs)`
|
||||
|
||||
Sends a prompt to a CLI agent and collects the response.
|
||||
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
const response = await acpManager.sendPrompt(
|
||||
"acp-claude-1234567890-abc123",
|
||||
"What is 2+2?",
|
||||
120000 // 2 minutes timeout
|
||||
);
|
||||
// Returns: Promise<string>
|
||||
```
|
||||
|
||||
#### `acpManager.kill(sessionId)`
|
||||
|
||||
Kills a session and cleans up.
|
||||
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
const killed = acpManager.kill("acp-claude-1234567890-abc123");
|
||||
// Returns: boolean
|
||||
```
|
||||
|
||||
#### `acpManager.getActiveSessions()`
|
||||
|
||||
Gets all active sessions.
|
||||
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
const sessions = acpManager.getActiveSessions();
|
||||
// Returns: AcpSession[]
|
||||
```
|
||||
|
||||
#### `acpManager.killAll()`
|
||||
|
||||
Kills all sessions.
|
||||
|
||||
```typescript
|
||||
import { acpManager } from "@/lib/acp";
|
||||
|
||||
acpManager.killAll();
|
||||
```
|
||||
|
||||
### Session Interface
|
||||
|
||||
```typescript
|
||||
interface AcpSession {
|
||||
id: string; // Unique session ID
|
||||
agentId: string; // Agent ID (e.g., "claude")
|
||||
process: ChildProcess; // Child process handle
|
||||
alive: boolean; // Whether the process is alive
|
||||
stdoutBuffer: string; // Accumulated stdout buffer
|
||||
stderrBuffer: string; // Accumulated stderr buffer
|
||||
createdAt: Date; // Created timestamp
|
||||
}
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
The `AcpManager` extends `EventEmitter` and emits the following events:
|
||||
|
||||
#### `stdout`
|
||||
|
||||
Emitted when the CLI agent writes to stdout.
|
||||
|
||||
```typescript
|
||||
acpManager.on("stdout", ({ sessionId, data }) => {
|
||||
console.log(`[${sessionId}] stdout: ${data}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### `stderr`
|
||||
|
||||
Emitted when the CLI agent writes to stderr.
|
||||
|
||||
```typescript
|
||||
acpManager.on("stderr", ({ sessionId, data }) => {
|
||||
console.error(`[${sessionId}] stderr: ${data}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### `exit`
|
||||
|
||||
Emitted when the CLI agent process exits.
|
||||
|
||||
```typescript
|
||||
acpManager.on("exit", ({ sessionId, code, signal }) => {
|
||||
console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### `error`
|
||||
|
||||
Emitted when the CLI agent process errors.
|
||||
|
||||
```typescript
|
||||
acpManager.on("error", ({ sessionId, error }) => {
|
||||
console.error(`[${sessionId}] error: ${error}`);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
ACP inherits all environment variables from the parent process and can be extended with custom env vars:
|
||||
|
||||
```typescript
|
||||
acpManager.spawn("claude", "claude", [], {
|
||||
ANTHROPIC_API_KEY: "sk-...",
|
||||
DEBUG: "true",
|
||||
});
|
||||
```
|
||||
|
||||
### Spawn Arguments
|
||||
|
||||
Each agent has default spawn arguments defined in the registry. You can override them:
|
||||
|
||||
```typescript
|
||||
acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});
|
||||
```
|
||||
|
||||
### Timeouts
|
||||
|
||||
Default prompt timeout is **120 seconds** (2 minutes). You can override:
|
||||
|
||||
```typescript
|
||||
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
|
||||
```
|
||||
|
||||
### Detection Cache
|
||||
|
||||
Agent detection is cached for **60 seconds** to avoid expensive filesystem scans. Force refresh:
|
||||
|
||||
```typescript
|
||||
import { refreshAgentCache } from "@/lib/acp";
|
||||
|
||||
refreshAgentCache();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Command Injection Prevention
|
||||
|
||||
ACP validates version commands to prevent command injection attacks:
|
||||
|
||||
```typescript
|
||||
const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/;
|
||||
```
|
||||
|
||||
Version commands containing these characters are rejected:
|
||||
|
||||
- `;` — Command separator
|
||||
- `&` — Background process
|
||||
- `|` — Pipe
|
||||
- `<`, `>` — Redirection
|
||||
- `` ` `` — Command substitution
|
||||
- `$` — Variable expansion
|
||||
- `\r`, `\n` — Line breaks
|
||||
|
||||
### Binary Name Validation
|
||||
|
||||
ACP validates that the version command binary matches the expected binary name (unless it's a custom agent).
|
||||
|
||||
### Process Isolation
|
||||
|
||||
Each ACP session runs in its own child process. The process is killed when the session ends or times out.
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Detection Performance
|
||||
|
||||
- **First call**: ~50-200ms (runs `version` command for each agent)
|
||||
- **Cached calls**: <1ms (returns from cache)
|
||||
- **Cache TTL**: 60 seconds
|
||||
|
||||
### Prompt Performance
|
||||
|
||||
- **Spawn**: ~50-100ms
|
||||
- **Send prompt**: ~10-50ms
|
||||
- **Wait for response**: Depends on CLI agent (typically 1-30 seconds)
|
||||
- **Kill**: ~5 seconds (SIGTERM) + immediate (SIGKILL)
|
||||
|
||||
### Resource Usage
|
||||
|
||||
- **Memory per session**: ~10-50MB (depends on CLI agent)
|
||||
- **CPU**: Minimal (I/O bound)
|
||||
- **Disk**: None
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Unknown agent" Error
|
||||
|
||||
**Problem**: `acpManager.spawn()` throws `Unknown agent: <id>`
|
||||
|
||||
**Solution**: Only 4 agents are allowed in `spawn()`:
|
||||
- `claude`
|
||||
- `codex`
|
||||
- `gemini`
|
||||
- `qwen`
|
||||
|
||||
Other agents must be spawned manually or via custom agent definitions.
|
||||
|
||||
### "Session not alive" Error
|
||||
|
||||
**Problem**: `acpManager.sendPrompt()` throws `Session ${sessionId} is not alive`
|
||||
|
||||
**Solution**: The session may have exited or been killed. Check session status:
|
||||
|
||||
```typescript
|
||||
const session = acpManager.getSession(sessionId);
|
||||
if (!session?.alive) {
|
||||
// Re-spawn the session
|
||||
acpManager.spawn("claude", "claude", [], {});
|
||||
}
|
||||
```
|
||||
|
||||
### "ACP timeout" Error
|
||||
|
||||
**Problem**: `acpManager.sendPrompt()` throws `ACP timeout after 120000ms`
|
||||
|
||||
**Solution**: Increase the timeout:
|
||||
|
||||
```typescript
|
||||
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes
|
||||
```
|
||||
|
||||
### CLI Not Detected
|
||||
|
||||
**Problem**: `detectInstalledAgents()` doesn't find your CLI
|
||||
|
||||
**Solutions**:
|
||||
1. **Check PATH**: Ensure the CLI is in your system PATH
|
||||
2. **Check version command**: Run `claude --version` manually
|
||||
3. **Check permissions**: Ensure the CLI is executable
|
||||
4. **Custom agent**: Add a custom agent definition for non-standard CLIs
|
||||
|
||||
### Permission Denied
|
||||
|
||||
**Problem**: ACP can't execute the CLI
|
||||
|
||||
**Solutions**:
|
||||
1. **Check file permissions**: `chmod +x /usr/local/bin/claude`
|
||||
2. **Check ownership**: Ensure OmniRoute has read/execute permissions
|
||||
3. **Check SELinux/AppArmor**: May block process spawning
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Spawn and Use Claude Code
|
||||
|
||||
```typescript
|
||||
import { acpManager, detectInstalledAgents } from "@/lib/acp";
|
||||
|
||||
// Detect installed agents
|
||||
const agents = detectInstalledAgents();
|
||||
const claude = agents.find((a) => a.id === "claude");
|
||||
|
||||
if (claude?.installed) {
|
||||
// Spawn a new session
|
||||
const session = acpManager.spawn(
|
||||
"claude",
|
||||
claude.binary,
|
||||
["--print", "--output-format", "json"]
|
||||
);
|
||||
|
||||
// Send a prompt
|
||||
const response = await acpManager.sendPrompt(
|
||||
session.id,
|
||||
"Explain quantum computing in 100 words"
|
||||
);
|
||||
|
||||
console.log("Claude's response:", response);
|
||||
|
||||
// Clean up
|
||||
acpManager.kill(session.id);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Auto-Discovery with Fallback
|
||||
|
||||
```typescript
|
||||
import { acpManager, getAvailableAgents } from "@/lib/acp";
|
||||
|
||||
const available = getAvailableAgents();
|
||||
|
||||
// Try Claude first, fallback to Codex
|
||||
let agentId = "claude";
|
||||
if (!available.find((a) => a.id === "claude")) {
|
||||
if (available.find((a) => a.id === "codex")) {
|
||||
agentId = "codex";
|
||||
} else {
|
||||
throw new Error("No ACP-compatible CLI agent found");
|
||||
}
|
||||
}
|
||||
|
||||
const agent = available.find((a) => a.id === agentId)!;
|
||||
const session = acpManager.spawn(agentId, agent.binary, agent.spawnArgs);
|
||||
|
||||
const response = await acpManager.sendPrompt(session.id, "Hello!");
|
||||
|
||||
acpManager.kill(session.id);
|
||||
```
|
||||
|
||||
### Example 3: Custom Agent
|
||||
|
||||
```typescript
|
||||
import { setCustomAgents, detectInstalledAgents } from "@/lib/acp";
|
||||
|
||||
// Register a custom CLI agent
|
||||
setCustomAgents([
|
||||
{
|
||||
id: "my-llm-cli",
|
||||
name: "My LLM CLI",
|
||||
binary: "myllm",
|
||||
versionCommand: "myllm --version",
|
||||
providerAlias: "my-llm-provider",
|
||||
spawnArgs: ["--format", "json"],
|
||||
protocol: "stdio",
|
||||
},
|
||||
]);
|
||||
|
||||
// Now detectInstalledAgents() will include "my-llm-cli"
|
||||
const agents = detectInstalledAgents();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[API Reference](../reference/API_REFERENCE.md)** — REST API endpoints
|
||||
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — All 223 providers
|
||||
- **[MCP Server](./MCP-SERVER.md)** — Model Context Protocol integration
|
||||
- **[A2A Server](./A2A-SERVER.md)** — Agent-to-Agent protocol
|
||||
- **[Cloud Agent](./CLOUD_AGENT.md)** — Cloud-based agents
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- [AionUi Project](https://github.com/iOfficeAI/AionUi) — Inspiration for ACP auto-detection
|
||||
- [ACP Source Code](../../src/lib/acp/) — Implementation details
|
||||
- `manager.ts` — Process lifecycle management
|
||||
- `registry.ts` — Agent discovery and registration
|
||||
- `index.ts` — Public API exports
|
||||
@@ -367,7 +367,7 @@ Base path: `/api/tools/agent-bridge/`
|
||||
| GET | `/api/tools/agent-bridge/agents` | List all 9 agents with current state |
|
||||
| GET | `/api/tools/agent-bridge/state` | Global server state (running, port, cert info) |
|
||||
| POST | `/api/tools/agent-bridge/server` | Start/stop/restart server (`action: "start"\|"stop"\|"restart"\|"trust-cert"\|"regenerate-cert"`) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/state` | State of one agent (dns_enabled, cert_trusted, etc.) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}` | State of one agent (dns_enabled, cert_trusted, etc.) |
|
||||
| POST | `/api/tools/agent-bridge/agents/{id}/dns` | Enable/disable DNS for agent (`{enabled: boolean}`) |
|
||||
| GET | `/api/tools/agent-bridge/agents/{id}/mappings` | Model mappings for agent |
|
||||
| PUT | `/api/tools/agent-bridge/agents/{id}/mappings` | Update model mappings |
|
||||
|
||||
@@ -537,7 +537,268 @@ default TTL 5 min).
|
||||
- `src/app/api/memory/reindex/route.ts`
|
||||
- `src/app/api/settings/memory/route.ts`
|
||||
- `src/app/api/settings/qdrant/route.ts` + sub-routes
|
||||
- `src/app/(dashboard)/dashboard/memory/` — Studio UI (page + components +
|
||||
tabs + hooks)
|
||||
- `open-sse/handlers/chatCore.ts` (injection / extraction wiring)
|
||||
- `open-sse/mcp-server/tools/memoryTools.ts`
|
||||
- `src/app/(dashboard)/dashboard/memory/` — Studio UI (page + components +
|
||||
tabs + hooks)
|
||||
- `open-sse/handlers/chatCore.ts` (injection / extraction wiring)
|
||||
- `open-sse/mcp-server/tools/memoryTools.ts`
|
||||
|
||||
---
|
||||
|
||||
## Choosing an Embedding Provider (v3.8.16+)
|
||||
|
||||
OmniRoute's memory engine supports **four embedding sources** (`src/lib/memory/embedding/`). Each has different trade-offs in **latency, cost, model quality, and setup complexity**.
|
||||
|
||||
### The Four Providers
|
||||
|
||||
| Provider | Source | Latency | Cost | Quality | Setup |
|
||||
|----------|--------|---------|------|---------|-------|
|
||||
| `transformers` | Local ONNX model (Xenova/all-MiniLM-L6-v2) | ~50-150ms (CPU) | Free | Good | `npm install` only |
|
||||
| `static` | Pre-computed vectors (cached) | <1ms | Free | N/A (depends on cache hit) | None |
|
||||
| `remote` | OpenAI / Cohere / Voyage API | ~100-300ms | $0.02-0.10/1M tokens | Excellent | API key |
|
||||
| `cache` | In-memory LRU layer over any source | <1ms (hit), full latency (miss) | Free | Same as underlying | None |
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
What's your deployment context?
|
||||
│
|
||||
┌───────────┼───────────┬──────────────┐
|
||||
│ │ │ │
|
||||
DEV/TEST SMALL PROD LARGE PROD EDGE / OFFLINE
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
transformers transformers remote (Qdrant) transformers
|
||||
(free, no API) (best quality) (no internet)
|
||||
│ │ │ │
|
||||
└────────┬──┴───────────┴──────────────┘
|
||||
│
|
||||
▼
|
||||
ALWAYS add `cache` layer on top
|
||||
(LruCache wraps any provider)
|
||||
```
|
||||
|
||||
### Database & API Configuration
|
||||
|
||||
Memory embedding options are configured via the Settings API/UI, not environment variables. The relevant settings database keys under Settings (`normalizeMemorySettings` in `src/lib/memory/settings.ts`) are:
|
||||
|
||||
- `memoryEmbeddingSource`: `"transformers"` (local), `"remote"` (API-based, e.g. OpenAI), `"static"` (external store), or `"auto"`
|
||||
- `memoryEmbeddingProviderModel`: Model identifier for remote/static sources (e.g., `"text-embedding-3-small"`)
|
||||
- `memoryTransformersEnabled`: `true` | `false`
|
||||
- `memoryStaticEnabled`: `true` | `false`
|
||||
- `memoryVectorStore`: `"sqlite-vec"`, `"qdrant"`, or `"auto"`
|
||||
|
||||
#### Local Model (`transformers`)
|
||||
|
||||
Uses transformers.js internally to run local models:
|
||||
|
||||
```bash
|
||||
# Env vars read in code (src/lib/memory/embedding/index.ts):
|
||||
MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2 # HF model repo
|
||||
MEMORY_STATIC_MODEL=minishlab/potion-base-8M # HF static potion model
|
||||
MEMORY_STATIC_CACHE_DIR=<DATA_DIR>/embeddings # Cache directory
|
||||
```
|
||||
|
||||
#### LRU Embedding Cache
|
||||
|
||||
The cache is always on by default and configured via env vars:
|
||||
|
||||
```bash
|
||||
MEMORY_EMBEDDING_CACHE_MAX=1000 # Max cached items
|
||||
MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # TTL (5 min)
|
||||
```
|
||||
### Performance Numbers
|
||||
|
||||
Benchmark on a typical 4-core x86 server (texts ~100 tokens each):
|
||||
|
||||
| Provider | p50 | p95 | p99 | Cost / 1M embeddings |
|
||||
|----------|-----|-----|-----|-----------------------|
|
||||
| `transformers` (CPU) | 80ms | 180ms | 350ms | Free |
|
||||
| `remote` (OpenAI) | 120ms | 220ms | 400ms | ~$0.02 (ada-002) / $0.13 (3-large) |
|
||||
| `static` (Qdrant) | 15ms | 30ms | 60ms | Depends on Qdrant hosting |
|
||||
| `cache` (hit) | <1ms | <1ms | 2ms | Free |
|
||||
|
||||
---
|
||||
|
||||
## Fact Extraction Patterns (v3.8.16+)
|
||||
|
||||
The `extraction.ts` module (`src/lib/memory/extraction.ts`) uses **regex pattern matching** to extract structured facts from conversation messages. Understanding these patterns helps you tune extraction quality for your use case.
|
||||
|
||||
### Default Pattern Categories
|
||||
|
||||
| Category | Example pattern | Captures |
|
||||
|----------|-----------------|----------|
|
||||
| PREFERENCE_PATTERNS | `"I prefer <X>"`, `"I like <X>"`, `"I hate <X>"` | User preferences |
|
||||
| DECISION_PATTERNS | `"I'll use <X>"`, `"I decided to <X>"`, `"I went with <X>"` | User decisions (episodic) |
|
||||
| PATTERN_PATTERNS | `"I usually <X>"`, `"I always <X>"`, `"I never <X>"` | Persistent behavioral patterns |
|
||||
|
||||
### Example Patterns (Simplified)
|
||||
```ts
|
||||
// From src/lib/memory/extraction.ts
|
||||
const PREFERENCE_PATTERNS = [
|
||||
/\bI\s+(?:really\s+)?prefer\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:really\s+)?like\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:hate|dislike|avoid)\s+([^.,\n]+)/gi
|
||||
];
|
||||
const DECISION_PATTERNS = [
|
||||
/\bI'?(?:ll|will)\s+use\s+([^.,\n]+)/gi,
|
||||
/\bI\s+(?:have\s+)?decided\s+(?:to\s+)?([^.,\n]+)/gi
|
||||
];
|
||||
const PATTERN_PATTERNS = [
|
||||
/\bI\s+usually\s+([^.,\n]+)/gi,
|
||||
/\bI\s+always\s+([^.,\n]+)/gi
|
||||
];
|
||||
```
|
||||
|
||||
### What Gets Extracted
|
||||
|
||||
When a user says:
|
||||
> "I prefer TypeScript. I'll use Postgres for this project. I always commit before pushing. I don't like Python."
|
||||
Extraction produces 4 memories:
|
||||
| Key | Category | Type | Content |
|
||||
|-----|----------|------|---------|
|
||||
| `preference:typescript` | preference | factual | "TypeScript" |
|
||||
| `decision:postgres_for_this_project` | decision | episodic | "Postgres for this project" |
|
||||
| `pattern:commit_before_pushing` | pattern | factual | "commit before pushing" |
|
||||
| `preference:python` | preference | factual | "Python" |
|
||||
|
||||
### Extraction Limits
|
||||
To prevent runaway extraction, the following limits apply:
|
||||
|
||||
| Min content length | 3 chars |
|
||||
| Max content length | 500 chars |
|
||||
|
||||
### When to Disable Extraction
|
||||
|
||||
Extraction runs automatically whenever memory is enabled; there is no separate
|
||||
extraction-only toggle. To turn it off, disable memory entirely (`enabled: false`
|
||||
via `PUT /api/settings/memory`). Consider doing so when:
|
||||
- You have high message volume and the extraction cost is non-trivial
|
||||
- Your conversations are mostly transient (chat, debugging) with no long-term value
|
||||
- You're already capturing context via custom plugins
|
||||
|
||||
---
|
||||
|
||||
## Hybrid RRF Tuning (v3.8.16+)
|
||||
|
||||
The **Reciprocal Rank Fusion (RRF)** algorithm combines FTS5 (keyword) and vector (semantic) results. The `k` parameter controls how much weight is given to lower-ranked results.
|
||||
|
||||
### The Formula
|
||||
|
||||
For each candidate memory, the RRF score is:
|
||||
|
||||
```
|
||||
RRF(d) = Σ 1 / (k + rank_i(d))
|
||||
```
|
||||
|
||||
Where:
|
||||
- `k` is the constant (default 60)
|
||||
- `rank_i(d)` is the rank of document `d` in the i-th retrieval system (FTS, vector)
|
||||
- The sum runs over all retrieval systems
|
||||
|
||||
### How `k` Affects Results
|
||||
|
||||
| `k` value | Effect | Best for |
|
||||
|-----------|--------|----------|
|
||||
| `k=0` | Pure rank fusion (no smoothing) | Theoretical baseline |
|
||||
| `k=10-30` | Heavily weights top results, low-rank barely contributes | When top-3 results are usually correct |
|
||||
| **`k=60`** (default) | Balanced — top-10 results all contribute meaningfully | General-purpose retrieval |
|
||||
| `k=100+` | Flatter — even low-rank results can dominate if they appear in multiple systems | When recall > precision is critical |
|
||||
|
||||
### Tuning `k` in Practice
|
||||
|
||||
```bash
|
||||
# Default
|
||||
MEMORY_RRF_K=60
|
||||
|
||||
# Aggressive precision (small memory, few docs)
|
||||
MEMORY_RRF_K=20
|
||||
|
||||
# Maximum recall (large memory, varied queries)
|
||||
MEMORY_RRF_K=120
|
||||
```
|
||||
|
||||
**Example with `k=20`:**
|
||||
- FTS rank 1 → contribution `1/21 = 0.048`
|
||||
- FTS rank 10 → contribution `1/30 = 0.033`
|
||||
- Vector rank 1 → contribution `0.048`
|
||||
- Combined max: `0.096`
|
||||
|
||||
**Example with `k=60`:**
|
||||
- FTS rank 1 → contribution `1/61 = 0.016`
|
||||
- FTS rank 10 → contribution `1/70 = 0.014`
|
||||
- Vector rank 1 → contribution `0.016`
|
||||
- Combined max: `0.033`
|
||||
|
||||
With higher `k`, the **relative difference** between top-1 and rank-10 is smaller, so the algorithm relies more on **consensus across retrieval systems** than on top-rank confidence.
|
||||
|
||||
### When to Change `k`
|
||||
|
||||
| Symptom | Try |
|
||||
|---------|-----|
|
||||
| Top result always wins, but it's wrong | **Lower** k (e.g., 20) — top-rank confidence matters more |
|
||||
| Right answer is in top-5 but not top-1 | **Higher** k (e.g., 100) — flatter scoring rewards consensus |
|
||||
| Recall is high but precision is low | **Lower** k — sharpen the ranking |
|
||||
| Recall is low (missing relevant docs) | **Higher** k — give lower-ranked docs a chance |
|
||||
|
||||
### RRF Weighting
|
||||
|
||||
The reciprocal rank fusion uses equal weights for semantic vector rank and full-text search rank:
|
||||
|
||||
```
|
||||
RRF(d) = 1/(k + rank_vector) + 1/(k + rank_fts)
|
||||
```
|
||||
|
||||
There are no environment variables to adjust individual weights (`MEMORY_RRF_VECTOR_WEIGHT`/`MEMORY_RRF_FTS_WEIGHT` do not exist).
|
||||
|
||||
---
|
||||
|
||||
## Summarization Strategy (v3.8.16+)
|
||||
|
||||
The `summarization.ts` module (`src/lib/memory/summarization.ts`) compresses older memories to keep the active set small while preserving recall.
|
||||
|
||||
### When Summarization Triggers
|
||||
|
||||
| Trigger | Threshold (default) |
|
||||
|---------|---------------------|
|
||||
| Manual trigger via API | n/a |
|
||||
|
||||
### What Gets Summarized
|
||||
|
||||
Two entry points are exported from `summarization.ts`:
|
||||
|
||||
- **`summarizeMemories(apiKeyId, sessionId?, maxTokens = 4000)`** — condenses the
|
||||
memories for a session into a single summary text bounded by a token budget.
|
||||
- **`summarizeMemoriesOlderThan(apiKeyId, days, dryRun)`** — the age-based
|
||||
compaction used by the API: it selects every memory older than `days`, builds
|
||||
one condensed summary memory from them, and (when `dryRun` is `false`) deletes
|
||||
the originals. Pass `dryRun: true` to preview the candidate set and token total
|
||||
without modifying anything.
|
||||
|
||||
There is no tag/key clustering pass or per-memory "core vs summarizable" scoring —
|
||||
selection is purely the age cutoff, and the summary text is a condensed,
|
||||
type-prefixed line per candidate.
|
||||
|
||||
### Triggering Summarization
|
||||
|
||||
Summarization is **manual / opt-in** — the `autoSummarize` setting is `false` by
|
||||
default, so nothing is compacted automatically. Trigger it via the API:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/memory/summarize \
|
||||
-H "Authorization: Bearer $OMNIROUTE_KEY"
|
||||
```
|
||||
|
||||
To leave it off, simply keep `autoSummarize` at its default (`false`).
|
||||
|
||||
### Summarization Quality Tips
|
||||
|
||||
- **Preview first with `dryRun`** — `summarizeMemoriesOlderThan(..., true)` returns
|
||||
the candidate list and total token count so you can confirm what would be merged
|
||||
before deleting the originals.
|
||||
- **Run summarization during low-traffic hours** if you have a large memory corpus — the LLM call is the slow part
|
||||
|
||||
```bash
|
||||
# Cron-style: summarize at 3am daily
|
||||
0 3 * * * curl -X POST http://localhost:20128/api/memory/summarize \
|
||||
-H "Authorization: Bearer $OMNIROUTE_KEY"
|
||||
```
|
||||
|
||||
@@ -300,6 +300,182 @@ See [MCP-SERVER.md](./MCP-SERVER.md) for transport setup and scope assignments.
|
||||
|
||||
---
|
||||
|
||||
## Execution Lifecycle (v3.8.16+)
|
||||
|
||||
The `SkillExecutor` (`src/lib/skills/executor.ts`) is a **singleton** that manages every skill invocation. Understanding its lifecycle is critical for debugging timeouts, retries, and execution state.
|
||||
|
||||
### The 5-Stage Lifecycle
|
||||
|
||||
```
|
||||
execute() called
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ PENDING │ ← queued, not yet started (DB row created)
|
||||
└──────┬──────┘
|
||||
│ start handler
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ RUNNING │ ← handler invoked with timeout
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────┴────┬──────────┬──────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
SUCCESS ERROR TIMEOUT (no other path — killed by parent)
|
||||
│ │ │
|
||||
└────┬────┴──────────┘
|
||||
│
|
||||
▼
|
||||
DB row updated with status, output, durationMs
|
||||
```
|
||||
|
||||
### Default Configuration
|
||||
|
||||
| Setting | Default | Configurable via |
|
||||
|---------|---------|------------------|
|
||||
| `timeout` | `30000` (30s) | `skillExecutor.setTimeout(ms)` |
|
||||
| `maxRetries` | `3` | `skillExecutor.setMaxRetries(count)` |
|
||||
|
||||
> **Important**: The executor is a singleton — calling `setTimeout()` affects all subsequent invocations globally. Per-skill timeouts are not currently supported; if you need different timeouts per skill, submit separate processes or fork the executor.
|
||||
|
||||
### Status Values
|
||||
|
||||
From `src/lib/skills/types.ts`:
|
||||
|
||||
```ts
|
||||
enum SkillStatus {
|
||||
PENDING = "pending", // Queued, not yet started
|
||||
RUNNING = "running", // Handler invoked
|
||||
SUCCESS = "success", // Handler returned valid output
|
||||
ERROR = "error", // Handler threw an exception
|
||||
TIMEOUT = "timeout", // Exceeded the executor's timeout
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The `TIMEOUT` status is defined in the enum but is **not actually written to the DB** by the current executor implementation — timeouts surface as `ERROR` with the message `"Skill execution timed out"`. The status enum is reserved for future use.
|
||||
|
||||
### Inspecting Executions
|
||||
|
||||
```ts
|
||||
import { skillExecutor } from "omniroute/skills/executor";
|
||||
|
||||
// Get a specific execution by ID
|
||||
const exec = skillExecutor.getExecution("exec-uuid-123");
|
||||
if (exec) {
|
||||
console.log(`${exec.skillName}: ${exec.status} in ${exec.durationMs}ms`);
|
||||
}
|
||||
|
||||
// List recent executions for an API key
|
||||
const recent = skillExecutor.listExecutions("api-key-id", 50, 0);
|
||||
for (const e of recent) {
|
||||
console.log(`${e.skillName} → ${e.status} (${e.durationMs}ms)`);
|
||||
}
|
||||
|
||||
// Count total executions
|
||||
const total = skillExecutor.countExecutions("api-key-id");
|
||||
```
|
||||
|
||||
### Retry Behavior
|
||||
|
||||
The `maxRetries` setting is stored but **not currently used** by the executor's `execute()` method — it only performs a single attempt. The `maxRetries` value is exposed for future implementation and for hooks that want to read it.
|
||||
|
||||
For now, retries must be implemented inside the skill handler itself. Built-in
|
||||
skills are registered against the executor (e.g. `registerBuiltinSkills(executor)`
|
||||
/ `registerBrowserSkill(executor)` in `src/lib/skills/builtin/`); whichever handler
|
||||
you register can wrap its own retry loop:
|
||||
|
||||
```ts
|
||||
// inside a skill handler
|
||||
async function handler(input, ctx) {
|
||||
const maxRetries = 3;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await fetchSomething(input);
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SkillMode in Detail
|
||||
|
||||
The `SkillMode` enum (`src/lib/skills/types.ts`) controls **when and how** skills are invoked:
|
||||
|
||||
```ts
|
||||
enum SkillMode {
|
||||
AUTO = "auto", // LLM decides when to call the skill
|
||||
MANUAL = "manual", // Only invoked by explicit user request
|
||||
HYBRID = "hybrid", // AUTO scoring + manual override
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: The codebase defines `SkillMode` (AUTO/MANUAL/HYBRID), while the `Skill.mode` field uses a different shape (`"on" | "off" | "auto"`). They are related but not identical — `SkillMode` is for executor policy, `Skill.mode` is for per-skill enablement.
|
||||
|
||||
### When to Use Each Mode
|
||||
|
||||
| Mode | LLM behavior | Use case |
|
||||
|------|--------------|----------|
|
||||
| `AUTO` | LLM can call the skill when it deems necessary | General-purpose skills (file reads, HTTP requests) |
|
||||
| `MANUAL` | LLM cannot call the skill; only an explicit `executeSkill` API call invokes it | Sensitive operations (database writes, payments) |
|
||||
| `HYBRID` | LLM can suggest the skill; user must confirm | Skills that have side effects but aren't dangerous |
|
||||
|
||||
### AUTO Scoring
|
||||
|
||||
When `AUTO` mode is active, each candidate skill is scored against the request
|
||||
context by `scoreAutoSkill()` in `src/lib/skills/injection.ts` — an additive,
|
||||
integer point system (skill-name match, name/tag/description token overlap,
|
||||
background-reason hints, provider-hint bonus/penalty). The top
|
||||
`AUTO_MAX_SKILLS = 5` skills with `score >= AUTO_MIN_SCORE = 3` are injected as
|
||||
callable tools, ties broken by `installCount` then name. See the full point table
|
||||
in [**Tool Schema Generation → AUTO Scoring**](#auto-scoring) earlier in this
|
||||
document; there is no float `0.6`-style threshold and no `registry.ts` scoring.
|
||||
|
||||
---
|
||||
|
||||
## Built-in Skills Catalog
|
||||
|
||||
OmniRoute ships with a curated set of built-in skills in `src/lib/skills/builtin/`. The most common ones:
|
||||
|
||||
### Browser Automation Skill
|
||||
|
||||
The browser skill (`src/lib/skills/builtin/browser.ts`) provides headless browser automation via Playwright/Puppeteer. **It is implemented but not in the default skills catalog** — to use it, install the browser extension plugin separately.
|
||||
|
||||
```ts
|
||||
// Enable in your config
|
||||
const config: SkillConfig = {
|
||||
enabled: true,
|
||||
mode: SkillMode.MANUAL, // Always require explicit invocation
|
||||
allowedSkills: ["browser"],
|
||||
timeout: 60000, // 60s for page loads
|
||||
maxRetries: 1,
|
||||
};
|
||||
```
|
||||
|
||||
### Other Built-in Categories
|
||||
|
||||
| Category | Skills | Mode |
|
||||
|----------|--------|------|
|
||||
| File I/O | `file_read`, `file_write` | AUTO |
|
||||
| HTTP | `http_request` | AUTO |
|
||||
| Search | `web_search` | AUTO |
|
||||
| Code Exec | `eval_code` (sandboxed JavaScript/Python) | HYBRID |
|
||||
| System | `execute_command` (sandboxed CLI execution) | MANUAL |
|
||||
### Adding a Custom Skill
|
||||
|
||||
See the [Plugin SDK & Skills Integration](../plugins/PLUGIN_SDK.md) for how to add a custom skill via the plugin system.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP-SERVER.md](./MCP-SERVER.md) — MCP tool registration and transports
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"MCP-SERVER",
|
||||
"A2A-SERVER",
|
||||
"AGENT_PROTOCOLS_GUIDE",
|
||||
"ACP",
|
||||
"CLOUD_AGENT",
|
||||
"EVALS",
|
||||
"GAMIFICATION",
|
||||
|
||||
210
docs/getting-started/AUTO-COMBO-GUIDE.md
Normal file
210
docs/getting-started/AUTO-COMBO-GUIDE.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Auto-Combo: Let OmniRoute Pick the Best AI for You
|
||||
|
||||
> **TL;DR**: Set your model to `auto` and OmniRoute automatically picks the best AI provider for each request. No configuration needed.
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
Instead of choosing a specific AI model (like GPT-4o or Claude), you can let OmniRoute **automatically pick the best one** for each request. It considers:
|
||||
|
||||
- **Health** — Is the provider working right now?
|
||||
- **Speed** — How fast is it?
|
||||
- **Cost** — How much does it cost?
|
||||
- **Quality** — Is it good at this type of task?
|
||||
- **Capacity** — Does it have quota remaining?
|
||||
|
||||
OmniRoute scores all your connected providers and picks the best one. If it fails, it automatically tries the next one.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Step 1**: Set your model to `auto` in your IDE or CLI:
|
||||
|
||||
```
|
||||
model: "auto"
|
||||
```
|
||||
|
||||
**Step 2**: That's it! OmniRoute handles the rest.
|
||||
|
||||
**Step 3** (optional): Use a variant for specific tasks:
|
||||
|
||||
```
|
||||
model: "auto/coding" # Best for code
|
||||
model: "auto/fast" # Fastest response
|
||||
model: "auto/cheap" # Cheapest option
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Which "auto" Should I Use?
|
||||
|
||||
| If you want... | Use this | Best for | How it works |
|
||||
|----------------|----------|----------|--------------|
|
||||
| **Best overall** | `auto` | General questions, chat | Balances speed, cost, and quality |
|
||||
| **Best code** | `auto/coding` | Writing code, debugging | Picks models good at coding tasks |
|
||||
| **Fastest response** | `auto/fast` | Quick answers, low latency | Prioritizes speed over everything |
|
||||
| **Cheapest option** | `auto/cheap` | Saving money | Picks the cheapest provider |
|
||||
| **Smartest model** | `auto/smart` | Complex tasks | Quality-first + explores new models |
|
||||
| **Most available** | `auto/offline` | When providers are busy | Picks providers with most capacity |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# General chat — balanced
|
||||
curl http://localhost:20128/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"auto","messages":[{"role":"user","content":"Hello!"}]}'
|
||||
|
||||
# Code generation — quality-first
|
||||
curl http://localhost:20128/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"auto/coding","messages":[{"role":"user","content":"Write a Python function"}]}'
|
||||
|
||||
# Quick answer — speed-first
|
||||
curl http://localhost:20128/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"auto/fast","messages":[{"role":"user","content":"What is 2+2?"}]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works (Simple Version)
|
||||
|
||||
When you send a request with `model: "auto"`, OmniRoute:
|
||||
|
||||
1. **Looks at all your connected providers** — Every provider you've added (OpenAI, Anthropic, Google, etc.)
|
||||
2. **Scores each one** on 5 factors:
|
||||
- Is it working? (health)
|
||||
- Does it have capacity? (quota)
|
||||
- How much does it cost? (price)
|
||||
- How fast is it? (speed)
|
||||
- Is it good at this task? (quality)
|
||||
3. **Picks the best one** — The highest-scoring provider gets your request
|
||||
4. **Auto-recovers** — If it fails, OmniRoute tries the next one automatically
|
||||
|
||||
### The Scoring System
|
||||
|
||||
Each provider gets a score from 0 to 1. The higher the score, the better the fit.
|
||||
|
||||
| Factor | Weight | What it means |
|
||||
|--------|--------|---------------|
|
||||
| Health | 20% | Is the provider working? (circuit breaker state) |
|
||||
| Quota | 15% | Does it have capacity remaining? |
|
||||
| Cost | 15% | How expensive is it? (cheaper = higher score) |
|
||||
| Speed | 12% | How fast is it? (lower latency = higher score) |
|
||||
| Task Fit | 8% | Is it good at this type of task? |
|
||||
| Stability | 5% | Is it consistent? (low error rate) |
|
||||
| Tier | 5% | Account tier (Ultra > Pro > Free) |
|
||||
| Other | 20% | Context affinity, connection density, etc. |
|
||||
|
||||
### How Variants Change the Scoring
|
||||
|
||||
Each variant uses different weights:
|
||||
|
||||
| Variant | Prioritizes | Key Weights |
|
||||
|---------|-------------|-------------|
|
||||
| `auto` | Balanced | health=20%, quota=15%, cost=15% |
|
||||
| `auto/coding` | Quality | taskFit=37%, stability=15% |
|
||||
| `auto/fast` | Speed | latency=32%, health=28% |
|
||||
| `auto/cheap` | Cost | cost=37% |
|
||||
| `auto/smart` | Quality + Explore | taskFit=37%, exploration=10% |
|
||||
| `auto/offline` | Capacity | quota=37%, health=28% |
|
||||
|
||||
---
|
||||
|
||||
## How It Handles Failures
|
||||
|
||||
OmniRoute has **three layers of protection**:
|
||||
|
||||
### 1. Auto-Fallback
|
||||
If the best provider fails, OmniRoute automatically tries the next one. You don't need to do anything.
|
||||
|
||||
### 2. Self-Healing
|
||||
If a provider keeps failing:
|
||||
- **Score < 0.2** → Excluded for 5 minutes
|
||||
- **Circuit breaker open** → Auto-excluded
|
||||
- **More than 50% providers down** → Incident mode (no exploration)
|
||||
|
||||
### 3. Emergency Fallback
|
||||
If all providers fail, OmniRoute routes to stable free providers (like Kiro or Qoder) as a last resort.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Account Support
|
||||
|
||||
If you have multiple accounts for the same provider (e.g., two OpenAI keys), OmniRoute treats each as a **separate candidate**. This means:
|
||||
|
||||
- Account A has quota remaining → use it
|
||||
- Account B is rate-limited → skip it
|
||||
- Account C is cheaper → prefer it
|
||||
|
||||
Each account is scored independently based on its own health, quota, and speed.
|
||||
|
||||
---
|
||||
|
||||
## Bandit Exploration
|
||||
|
||||
OmniRoute occasionally **explores** new providers to discover better options:
|
||||
|
||||
- **Default**: 5% of requests go to random providers
|
||||
- **Auto/smart**: 10% exploration rate
|
||||
- **Disabled** when more than 50% of providers are unhealthy
|
||||
|
||||
This helps OmniRoute learn which providers work best for your usage patterns.
|
||||
|
||||
---
|
||||
|
||||
## Common Questions
|
||||
|
||||
### "Will it always pick the most expensive model?"
|
||||
|
||||
**No.** Cost is only 15% of the score by default. A cheap, fast, healthy provider can beat an expensive one. Use `auto/cheap` if you want to prioritize cost even more.
|
||||
|
||||
### "What if a provider goes down?"
|
||||
|
||||
OmniRoute automatically skips it and tries the next one. If a provider keeps failing, it's excluded temporarily (5-30 minutes). You don't need to do anything.
|
||||
|
||||
### "Can I see which provider was used?"
|
||||
|
||||
Check the response headers — OmniRoute includes the provider and model used in each response.
|
||||
|
||||
### "Does it learn from my usage?"
|
||||
|
||||
Yes! The scoring system uses historical data (latency, error rates, success rates) to make better decisions over time.
|
||||
|
||||
### "What's the difference between `auto` and `auto/smart`?"
|
||||
|
||||
- `auto` — Balanced, 5% exploration
|
||||
- `auto/smart` — Quality-first (same weights as `auto/coding`), 10% exploration
|
||||
|
||||
Use `auto/smart` when you want the best quality and are okay with occasional exploration.
|
||||
|
||||
### "Can I force a specific provider?"
|
||||
|
||||
Yes! Use a combo with `priority` strategy instead of `auto`. See the [Technical Reference](../routing/AUTO-COMBO.md) for details.
|
||||
|
||||
### "How is this different from round-robin?"
|
||||
|
||||
Round-robin cycles through providers in order. Auto-combo **scores each provider** and picks the best one. It's smarter — it considers health, speed, cost, and quality.
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Connect a Provider](./PROVIDERS-GUIDE.md)** — Add your first AI provider
|
||||
- **[Free Tiers Guide](./FREE-TIERS-GUIDE.md)** — Get free AI with no credit card
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** — Fix common issues
|
||||
- **[Technical Reference](../routing/AUTO-COMBO.md)** — Deep dive into the scoring algorithm
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
For developers and contributors, see the [Auto-Combo Technical Reference](../routing/AUTO-COMBO.md) for:
|
||||
- Full 12-factor scoring algorithm
|
||||
- Mode pack weight tables
|
||||
- Implementation file paths
|
||||
- API endpoints
|
||||
- Self-healing algorithm details
|
||||
270
docs/getting-started/FREE-TIERS-GUIDE.md
Normal file
270
docs/getting-started/FREE-TIERS-GUIDE.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Free Tiers Guide: Get Free AI Without a Credit Card
|
||||
|
||||
> **TL;DR**: OmniRoute aggregates free tiers from 50+ providers. Connect multiple free providers for unlimited free AI with automatic fallback.
|
||||
|
||||
---
|
||||
|
||||
## What Are Free Tiers?
|
||||
|
||||
Many AI providers offer **free usage** — no credit card required. Think of it like free samples at a grocery store. You can try the product without paying.
|
||||
|
||||
OmniRoute **aggregates** these free tiers into one endpoint. Instead of signing up for 10 different services, you connect them all to OmniRoute and use `model: "auto"` to automatically pick the best free option for each request.
|
||||
|
||||
---
|
||||
|
||||
## Best Free Providers (No Credit Card)
|
||||
|
||||
### Tier 1: Free Forever (Unlimited)
|
||||
|
||||
These providers are **always free** with no limits:
|
||||
|
||||
| Provider | Models | Quota | How to Connect |
|
||||
|----------|--------|-------|----------------|
|
||||
| **Kiro AI** | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | 50 credits/month | No auth needed |
|
||||
| **OpenCode Free** | GPT-4o, Claude, Gemini | Unlimited | No auth needed |
|
||||
| **Pollinations** | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No key needed | No auth needed |
|
||||
| **LongCat** | LongCat-Flash-Lite | 50M tokens/day | No auth needed |
|
||||
| **Cloudflare AI** | 50+ models | 10K neurons/day | No auth needed |
|
||||
| **Qwen** | Qwen3-coder-plus/flash/next | Unlimited | No auth needed |
|
||||
| **Qoder** | Kimi-K2, DeepSeek-R1, Qwen3-coder | Unlimited | No auth needed |
|
||||
|
||||
### Tier 2: Free with Signup (Generous)
|
||||
|
||||
These providers give you **free credits** when you sign up:
|
||||
|
||||
| Provider | Free Credits | Models | How to Get |
|
||||
|----------|-------------|--------|------------|
|
||||
| **NVIDIA NIM** | ~40 RPM | 129 models | Sign up at build.nvidia.com |
|
||||
| **Cerebras** | 1M tokens/day | Qwen3 235B, GPT-OSS 120B | Sign up at cerebras.ai |
|
||||
| **DeepSeek** | 5M free tokens | DeepSeek V4 | Sign up at platform.deepseek.com |
|
||||
| **Groq** | 30 RPM free | Llama 4, Mixtral | Sign up at console.groq.com |
|
||||
| **OpenAI** | $5 free credits | GPT-5, GPT-4o | Sign up at platform.openai.com |
|
||||
| **Anthropic** | $5 free credits | Claude Opus 4.6, Sonnet 4.6 | Sign up at console.anthropic.com |
|
||||
| **Google** | 1,500 req/day | Gemini 2.5 Pro, Flash | Sign up at aistudio.google.com |
|
||||
|
||||
### Tier 3: Free with Limits (Specific Use Cases)
|
||||
|
||||
These providers have **free tiers** with specific limits:
|
||||
|
||||
| Provider | Free Limit | Models | Best For |
|
||||
|----------|-----------|--------|----------|
|
||||
| **Cerebras** | 1M tokens/day | Qwen3 235B | Fast inference |
|
||||
| **NVIDIA NIM** | ~40 RPM | 129 models | Variety |
|
||||
| **Groq** | 30 RPM | Llama 4, Mixtral | Speed |
|
||||
| **Cloudflare AI** | 10K neurons/day | 50+ models | Variety |
|
||||
|
||||
---
|
||||
|
||||
## How to Stack Free Tiers
|
||||
|
||||
The magic of OmniRoute is **stacking free tiers**. Instead of relying on one provider, you connect multiple free providers and let OmniRoute automatically pick the best one for each request.
|
||||
|
||||
### Example: Unlimited Free AI
|
||||
|
||||
Connect these 4 providers for **unlimited free AI**:
|
||||
|
||||
1. **Kiro AI** — 50 credits/month (Claude models)
|
||||
2. **OpenCode Free** — Unlimited (GPT models)
|
||||
3. **Pollinations** — No key needed (multiple models)
|
||||
4. **LongCat** — 50M tokens/day (backup)
|
||||
|
||||
Then use `model: "auto"` and OmniRoute will:
|
||||
- Try Kiro first (best quality)
|
||||
- If Kiro is busy → try OpenCode Free
|
||||
- If OpenCode Free is slow → try Pollinations
|
||||
- If all fail → use LongCat as backup
|
||||
|
||||
**Result**: Unlimited free AI with automatic fallback!
|
||||
|
||||
---
|
||||
|
||||
## How to Connect Free Providers
|
||||
|
||||
### Step 1: Open the Dashboard
|
||||
|
||||
Go to `http://localhost:20128` in your browser.
|
||||
|
||||
### Step 2: Go to Providers
|
||||
|
||||
Click **Providers** in the sidebar.
|
||||
|
||||
### Step 3: Click Add Provider
|
||||
|
||||
Click the **+ Add Provider** button.
|
||||
|
||||
### Step 4: Select a Free Provider
|
||||
|
||||
Browse the list and select one of these free providers:
|
||||
- **Kiro AI** — Free Claude models
|
||||
- **OpenCode Free** — Free GPT models
|
||||
- **Pollinations** — Free GPT-5, Claude, Gemini
|
||||
- **LongCat** — 50M tokens/day free
|
||||
- **Cloudflare AI** — 50+ models, 10K neurons/day
|
||||
|
||||
### Step 5: Click Connect
|
||||
|
||||
No API key needed — just click **Connect**.
|
||||
|
||||
### Step 6: Repeat
|
||||
|
||||
Connect 3-4 free providers for the best experience.
|
||||
|
||||
---
|
||||
|
||||
## Free Provider Details
|
||||
|
||||
### Kiro AI
|
||||
|
||||
- **Models**: Claude Sonnet 4.5, Haiku 4.5, Opus 4.6
|
||||
- **Quota**: 50 credits/month
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: High-quality Claude models
|
||||
|
||||
### OpenCode Free
|
||||
|
||||
- **Models**: GPT-4o, Claude, Gemini
|
||||
- **Quota**: Unlimited
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: General-purpose AI
|
||||
|
||||
### Pollinations
|
||||
|
||||
- **Models**: GPT-5, Claude, Gemini, DeepSeek, Llama 4
|
||||
- **Quota**: No key needed
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: Variety of models
|
||||
|
||||
### LongCat
|
||||
|
||||
- **Models**: LongCat-Flash-Lite
|
||||
- **Quota**: 50M tokens/day
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: High-volume usage
|
||||
|
||||
### Cloudflare AI
|
||||
|
||||
- **Models**: 50+ models
|
||||
- **Quota**: 10K neurons/day
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: Variety and reliability
|
||||
|
||||
### NVIDIA NIM
|
||||
|
||||
- **Models**: 129 models
|
||||
- **Quota**: ~40 RPM
|
||||
- **Auth**: Sign up at build.nvidia.com
|
||||
- **Best for**: Variety and speed
|
||||
|
||||
### Cerebras
|
||||
|
||||
- **Models**: Qwen3 235B, GPT-OSS 120B
|
||||
- **Quota**: 1M tokens/day
|
||||
- **Auth**: Sign up at cerebras.ai
|
||||
- **Best for**: Fast inference
|
||||
|
||||
### Qwen
|
||||
|
||||
- **Models**: Qwen3-coder-plus/flash/next
|
||||
- **Quota**: Unlimited
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: Coding tasks
|
||||
|
||||
### Qoder
|
||||
|
||||
- **Models**: Kimi-K2, DeepSeek-R1, Qwen3-coder
|
||||
- **Quota**: Unlimited
|
||||
- **Auth**: No auth needed
|
||||
- **Best for**: Coding tasks
|
||||
|
||||
---
|
||||
|
||||
## How OmniRoute Makes Free Tiers Better
|
||||
|
||||
### 1. Automatic Fallback
|
||||
|
||||
If one free provider is busy or down, OmniRoute automatically tries the next one. You don't need to do anything.
|
||||
|
||||
### 2. Smart Routing
|
||||
|
||||
OmniRoute picks the **best free provider** for each request based on:
|
||||
- Speed — Which provider is fastest right now?
|
||||
- Quality — Which provider is best for this task?
|
||||
- Capacity — Which provider has quota remaining?
|
||||
|
||||
### 3. Token Savings
|
||||
|
||||
OmniRoute's **compression** feature saves 15-95% of tokens. This means your free quota lasts **5-20x longer**.
|
||||
|
||||
### 4. Multi-Account Support
|
||||
|
||||
If you have multiple accounts for the same provider, OmniRoute treats each as a separate candidate. This doubles or triples your free quota.
|
||||
|
||||
---
|
||||
|
||||
## Free Tier Math
|
||||
|
||||
Let's calculate how much free AI you can get:
|
||||
|
||||
### Conservative Estimate (3 providers)
|
||||
|
||||
| Provider | Daily Quota | Monthly Quota |
|
||||
|----------|-------------|---------------|
|
||||
| Kiro AI | ~1.7 credits | 50 credits |
|
||||
| OpenCode Free | Unlimited | Unlimited |
|
||||
| Pollinations | Unlimited | Unlimited |
|
||||
|
||||
**Total**: Unlimited free AI
|
||||
|
||||
### Aggressive Estimate (7 providers)
|
||||
|
||||
| Provider | Daily Quota | Monthly Quota |
|
||||
|----------|-------------|---------------|
|
||||
| Kiro AI | ~1.7 credits | 50 credits |
|
||||
| OpenCode Free | Unlimited | Unlimited |
|
||||
| Pollinations | Unlimited | Unlimited |
|
||||
| LongCat | 50M tokens | 1.5B tokens |
|
||||
| Cloudflare AI | 10K neurons | 300K neurons |
|
||||
| NVIDIA NIM | ~40 RPM | ~1.7M requests |
|
||||
| Cerebras | 1M tokens | 30M tokens |
|
||||
|
||||
**Total**: ~1.9B+ free tokens/month (with compression: ~9.5B+ effective tokens)
|
||||
|
||||
---
|
||||
|
||||
## Common Questions
|
||||
|
||||
### "Is this really free?"
|
||||
|
||||
**Yes!** These are official free tiers from the providers. OmniRoute just makes it easier to use them all at once.
|
||||
|
||||
### "Will the free tier run out?"
|
||||
|
||||
Some providers have limits (like Kiro's 50 credits/month), but others are unlimited (like OpenCode Free and Pollinations). By connecting multiple providers, you always have a backup.
|
||||
|
||||
### "Can I use free providers for production?"
|
||||
|
||||
**Yes!** Many free providers are production-ready. However, for critical applications, consider adding a paid provider as a backup.
|
||||
|
||||
### "What's the catch?"
|
||||
|
||||
No catch! Providers offer free tiers to attract users. OmniRoute just makes it easier to use them all at once.
|
||||
|
||||
### "How do I get more free quota?"
|
||||
|
||||
1. Connect more free providers
|
||||
2. Use compression to save tokens (15-95% savings)
|
||||
3. Use `auto/cheap` to prioritize free/cheap providers
|
||||
4. Create multiple accounts for the same provider
|
||||
|
||||
### "Do free providers have worse quality?"
|
||||
|
||||
**Not necessarily!** Many free providers offer the same models as paid providers. For example, Kiro gives you access to Claude Sonnet 4.5 — the same model you'd get with a paid Anthropic subscription.
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Auto-Combo Guide](./AUTO-COMBO-GUIDE.md)** — Let OmniRoute pick the best AI for you
|
||||
- **[Providers Guide](./PROVIDERS-GUIDE.md)** — Connect more providers
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** — Fix common issues
|
||||
- **[Free Tiers Reference](../reference/FREE_TIERS.md)** — Full list of free tiers
|
||||
220
docs/getting-started/PROVIDERS-GUIDE.md
Normal file
220
docs/getting-started/PROVIDERS-GUIDE.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Providers Guide: Connect AI Models to OmniRoute
|
||||
|
||||
> **TL;DR**: A provider is a connection to an AI service (like OpenAI, Anthropic, Google). You need at least one provider to use OmniRoute.
|
||||
|
||||
---
|
||||
|
||||
## What Is a Provider?
|
||||
|
||||
Think of a provider like a **phone carrier**. Just as you need a phone carrier to make calls, you need an AI provider to use AI models. OmniRoute is like a phone that works with **all carriers** — you can switch between them automatically.
|
||||
|
||||
### Types of Providers
|
||||
|
||||
| Type | What It Is | Examples | Cost |
|
||||
|------|-----------|----------|------|
|
||||
| **Free** | No payment required | Kiro, OpenCode Free, Pollinations | $0 |
|
||||
| **API Key** | You need an API key | OpenAI, Anthropic, Google | Pay per use |
|
||||
| **OAuth** | Login with your account | Claude Code, GitHub Copilot | Subscription |
|
||||
| **Web Cookie** | Uses your browser session | ChatGPT Web, Gemini Web | $0 (uses your account) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start: Connect Your First Provider
|
||||
|
||||
### Option A: Free Provider (No Credit Card)
|
||||
|
||||
1. Open the dashboard at `http://localhost:20128`
|
||||
2. Go to **Providers** → **Add Provider**
|
||||
3. Select one of these free providers:
|
||||
- **Kiro AI** — Free Claude models (no auth needed)
|
||||
- **OpenCode Free** — Free GPT models (no auth needed)
|
||||
- **Pollinations** — Free GPT-5, Claude, Gemini (no key needed)
|
||||
- **LongCat** — 50M tokens/day free
|
||||
- **Cloudflare AI** — 50+ models, 10K neurons/day
|
||||
4. Click **Connect**
|
||||
5. Done! You now have free AI access.
|
||||
|
||||
### Option B: API Key Provider (Paid)
|
||||
|
||||
1. Get an API key from the provider's website:
|
||||
- **OpenAI**: https://platform.openai.com/api-keys
|
||||
- **Anthropic**: https://console.anthropic.com/
|
||||
- **Google**: https://aistudio.google.com/apikey
|
||||
- **DeepSeek**: https://platform.deepseek.com/
|
||||
- **Groq**: https://console.groq.com/
|
||||
2. Open the dashboard at `http://localhost:20128`
|
||||
3. Go to **Providers** → **Add Provider**
|
||||
4. Select your provider
|
||||
5. Paste your API key
|
||||
6. Click **Connect**
|
||||
7. Done! You now have access to that provider's models.
|
||||
|
||||
### Option C: OAuth Provider (Subscription)
|
||||
|
||||
1. Open the dashboard at `http://localhost:20128`
|
||||
2. Go to **Providers** → **Add Provider**
|
||||
3. Select your provider (e.g., Claude Code, GitHub Copilot)
|
||||
4. Click **Connect with OAuth**
|
||||
5. Login with your account
|
||||
6. Done! You now have access to your subscription models.
|
||||
|
||||
---
|
||||
|
||||
## Best Free Providers
|
||||
|
||||
These providers offer **free access** with no credit card:
|
||||
|
||||
| Provider | Free Quota | Models | How to Connect |
|
||||
|----------|-----------|--------|----------------|
|
||||
| **Kiro AI** | 50 credits/month | Claude Sonnet 4.5, Haiku 4.5, Opus 4.6 | No auth needed |
|
||||
| **OpenCode Free** | Unlimited | GPT-4o, Claude, Gemini | No auth needed |
|
||||
| **Pollinations** | No key needed | GPT-5, Claude, Gemini, DeepSeek, Llama 4 | No auth needed |
|
||||
| **LongCat** | 50M tokens/day | LongCat-Flash-Lite | No auth needed |
|
||||
| **Cloudflare AI** | 10K neurons/day | 50+ models | No auth needed |
|
||||
| **NVIDIA NIM** | ~40 RPM | 129 models | API key needed |
|
||||
| **Cerebras** | 1M tokens/day | Qwen3 235B, GPT-OSS 120B | API key needed |
|
||||
| **Qwen** | Unlimited | Qwen3-coder-plus/flash/next | No auth needed |
|
||||
| **Qoder** | Unlimited | Kimi-K2, DeepSeek-R1, Qwen3-coder | No auth needed |
|
||||
|
||||
**Tip**: Connect multiple free providers for **unlimited free AI** with automatic fallback!
|
||||
|
||||
---
|
||||
|
||||
## Best Paid Providers
|
||||
|
||||
These providers offer **high-quality models** with API keys:
|
||||
|
||||
| Provider | Best Models | Cost | Free Tier |
|
||||
|----------|------------|------|-----------|
|
||||
| **OpenAI** | GPT-5, GPT-4o | $2.50-$10/1M tokens | $5 free credits |
|
||||
| **Anthropic** | Claude Opus 4.6, Sonnet 4.6 | $3-$15/1M tokens | $5 free credits |
|
||||
| **Google** | Gemini 2.5 Pro, Flash | $0.075-$1.25/1M tokens | 1,500 req/day free |
|
||||
| **DeepSeek** | DeepSeek V4 | $0.14-$0.28/1M tokens | 5M free tokens |
|
||||
| **Groq** | Llama 4, Mixtral | $0.05-$0.27/1M tokens | 30 RPM free |
|
||||
| **xAI** | Grok 3 | $0.30-$0.60/1M tokens | — |
|
||||
|
||||
---
|
||||
|
||||
## How to Connect a Provider (Step-by-Step)
|
||||
|
||||
### Step 1: Open the Dashboard
|
||||
|
||||
Go to `http://localhost:20128` in your browser.
|
||||
|
||||
### Step 2: Go to Providers
|
||||
|
||||
Click **Providers** in the sidebar.
|
||||
|
||||
### Step 3: Click Add Provider
|
||||
|
||||
Click the **+ Add Provider** button.
|
||||
|
||||
### Step 4: Select Your Provider
|
||||
|
||||
Browse the list or search for your provider. Click on it.
|
||||
|
||||
### Step 5: Enter Credentials
|
||||
|
||||
- **Free providers**: No credentials needed — just click **Connect**
|
||||
- **API key providers**: Paste your API key
|
||||
- **OAuth providers**: Click **Connect with OAuth** and login
|
||||
|
||||
### Step 6: Test the Connection
|
||||
|
||||
Click **Test Connection** to verify it works.
|
||||
|
||||
### Step 7: Done!
|
||||
|
||||
Your provider is now connected. You can use it with `model: "auto"` or specify the provider directly.
|
||||
|
||||
---
|
||||
|
||||
## Using Multiple Providers
|
||||
|
||||
OmniRoute works best with **multiple providers**. This gives you:
|
||||
|
||||
- **Automatic fallback** — If one provider fails, OmniRoute tries the next
|
||||
- **Cost optimization** — OmniRoute picks the cheapest provider for each request
|
||||
- **Speed optimization** — OmniRoute picks the fastest provider for each request
|
||||
- **Quality optimization** — OmniRoute picks the best provider for each task
|
||||
|
||||
### Recommended Setup
|
||||
|
||||
Connect at least **3 providers** for the best experience:
|
||||
|
||||
1. **One free provider** (Kiro, OpenCode Free, or Pollinations) — Always available
|
||||
2. **One fast provider** (Groq, Cerebras) — For quick responses
|
||||
3. **One quality provider** (OpenAI, Anthropic, Google) — For complex tasks
|
||||
|
||||
Then use `model: "auto"` and OmniRoute will automatically pick the best one for each request.
|
||||
|
||||
---
|
||||
|
||||
## Provider-Specific Setup
|
||||
|
||||
### OpenAI
|
||||
|
||||
1. Get API key: https://platform.openai.com/api-keys
|
||||
2. In OmniRoute: Providers → Add Provider → OpenAI
|
||||
3. Paste API key → Connect
|
||||
|
||||
### Anthropic
|
||||
|
||||
1. Get API key: https://console.anthropic.com/
|
||||
2. In OmniRoute: Providers → Add Provider → Anthropic
|
||||
3. Paste API key → Connect
|
||||
|
||||
### Google (Gemini)
|
||||
|
||||
1. Get API key: https://aistudio.google.com/apikey
|
||||
2. In OmniRoute: Providers → Add Provider → Gemini
|
||||
3. Paste API key → Connect
|
||||
|
||||
### DeepSeek
|
||||
|
||||
1. Get API key: https://platform.deepseek.com/
|
||||
2. In OmniRoute: Providers → Add Provider → DeepSeek
|
||||
3. Paste API key → Connect
|
||||
|
||||
### Groq
|
||||
|
||||
1. Get API key: https://console.groq.com/
|
||||
2. In OmniRoute: Providers → Add Provider → Groq
|
||||
3. Paste API key → Connect
|
||||
|
||||
---
|
||||
|
||||
## Common Questions
|
||||
|
||||
### "Do I need to pay to use OmniRoute?"
|
||||
|
||||
**No!** OmniRoute is free and open-source. You can use free providers (Kiro, OpenCode Free, Pollinations) without paying anything. You only pay if you choose to use paid providers.
|
||||
|
||||
### "Which provider should I start with?"
|
||||
|
||||
Start with **Kiro AI** — it's free, requires no API key, and gives you access to Claude models. Then add more providers as needed.
|
||||
|
||||
### "Can I use multiple providers at once?"
|
||||
|
||||
**Yes!** That's the whole point of OmniRoute. Connect multiple providers and use `model: "auto"` to let OmniRoute pick the best one for each request.
|
||||
|
||||
### "What if a provider goes down?"
|
||||
|
||||
OmniRoute automatically skips failed providers and tries the next one. You don't need to do anything.
|
||||
|
||||
### "How do I disconnect a provider?"
|
||||
|
||||
Go to Providers → click on the provider → click **Disconnect**.
|
||||
|
||||
### "Can I use my existing API keys?"
|
||||
|
||||
**Yes!** If you already have API keys for OpenAI, Anthropic, Google, etc., you can use them in OmniRoute. Just paste them when connecting the provider.
|
||||
|
||||
---
|
||||
|
||||
## What's Next?
|
||||
|
||||
- **[Auto-Combo Guide](./AUTO-COMBO-GUIDE.md)** — Let OmniRoute pick the best AI for you
|
||||
- **[Free Tiers Guide](./FREE-TIERS-GUIDE.md)** — Get free AI with no credit card
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** — Fix common issues
|
||||
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — Full list of 177 providers
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user