mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-09 16:53:13 +03:00
Compare commits
2 Commits
compressio
...
feat/tinyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f04073e9 | ||
|
|
d98737578e |
197
.cbmignore
197
.cbmignore
@@ -1,197 +0,0 @@
|
||||
# codebase-memory-mcp ignore list
|
||||
#
|
||||
# Padrão gitignore-style. Linhas começando com `#` são comentários.
|
||||
# Barra final (`/`) = só diretório. Sem barra = casa arquivo OU diretório.
|
||||
#
|
||||
# O CBM também lê `.gitignore` automaticamente — esta lista deixa explícito o que
|
||||
# os hooks do CBM vão pular. Se uma regra entrar em conflito entre os dois arquivos,
|
||||
# vale a união. Editar este arquivo é mais barato do que confiar na herança implícita.
|
||||
#
|
||||
# Última reconciliação: 2026-07-31, status `ready` (513k nodes / 689k edges),
|
||||
# `auto_index_limit=50000`, total indexável medido ≈11.546 arquivos (folga 4,3×).
|
||||
#
|
||||
# Fontes cruzadas:
|
||||
# - `codebase-memory-mcp cli index_status --project home-diegosouzapw-dev-proxys-OmniRoute`
|
||||
# → `not_indexed.dirs` (27) + `not_indexed.files` (336), todos `BY DESIGN`.
|
||||
# - `.gitignore` deste repo (5.691 B) — fonte canônica secundária.
|
||||
#
|
||||
# Como auditar mudanças: depois de editar este arquivo, rodar `index_repository`
|
||||
# (ou esperar `auto_watch` re-indexar) e re-checar `cli index_status` → comparar
|
||||
# contagens em `not_indexed.dirs_count` e `not_indexed.files_count`.
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 1. Diretorios de runtime / pacote — nao sao codigo-fonte
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
node_modules/
|
||||
node_modules
|
||||
|
||||
# Builds e artefatos reproduziveis (Layer 1 Next.js / Electron)
|
||||
.build/
|
||||
dist/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# Electron especifico
|
||||
electron/dist-electron/
|
||||
electron/node_modules/
|
||||
icon.iconset/
|
||||
|
||||
# Workspaces internos que tem proprio node_modules
|
||||
@omniroute/opencode-plugin/dist/
|
||||
@omniroute/opencode-plugin/node_modules/
|
||||
@omniroute/opencode-provider/dist/
|
||||
@omniroute/opencode-provider/node_modules/
|
||||
|
||||
# Recursos nativos compilados (C/JNI/wasm)
|
||||
src/mitm/tproxy/native/build/
|
||||
|
||||
# Artefatos locais do Stryker / Playwright / coverage
|
||||
.stryker-tmp/
|
||||
reports/mutation/
|
||||
stryker-output-*.json
|
||||
.playwright-mcp/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
|
||||
# Analise / linters / caches
|
||||
.analysis/
|
||||
.sisyphus/
|
||||
.plans/
|
||||
.gitnexus
|
||||
.worktrees
|
||||
.codegraph/
|
||||
|
||||
# Quality artifacts (gerados por npm run lint --cache etc)
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
|
||||
# Claude Code local state
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/scheduled_tasks/
|
||||
.claude/sessions/
|
||||
.claude/state.json
|
||||
.claude/settings.local.json
|
||||
|
||||
# Serena / Antigravity / outras tools locais
|
||||
.serena/
|
||||
.antigravitycli/
|
||||
.gemini/
|
||||
.config/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 2. Diretorios com prefixo `_` — locais / privados (regra global do .gitignore)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
_*/
|
||||
_artifacts/
|
||||
_cache/
|
||||
_mono_repo/
|
||||
_references/
|
||||
_tasks/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 3. Diretorios de tooling IA (state local, nao codigo)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
.agents/
|
||||
.claude/
|
||||
.vscode/
|
||||
.idea/
|
||||
.junie/
|
||||
.omc/
|
||||
.data/
|
||||
.data-dev/
|
||||
.local-data/
|
||||
.logs/
|
||||
.artifacts/
|
||||
.source/
|
||||
.superpowers/
|
||||
.claude-flow/
|
||||
.omnivscodeagent/
|
||||
omnirouteCloud/
|
||||
omnirouteSite/
|
||||
.omniroute/
|
||||
.stent/
|
||||
|
||||
# Subpaths especificos do Claude Code que nao estao em .claude/ (criados sob repo)
|
||||
.claude/worktrees/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 4. Diretorios de dados / runtime locais (storage, env, secrets, scratch)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
data/
|
||||
src/lib/env/
|
||||
src/app/api/agent-skills/coverage/
|
||||
src/app/api/cloud/
|
||||
src/app/api/sync/cloud/
|
||||
src/app/api/system/env/
|
||||
tests/golden-set/data/
|
||||
|
||||
# Logs e saida de teste
|
||||
logs/*
|
||||
test_output.log
|
||||
home-diegosouzapw-dev-automacoes-*.txt
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 5. Diretorios do monorepo por subprojeto (nao fazem parte do app principal)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
security-analysis/
|
||||
vscode-extension/
|
||||
obsidian-plugin/node_modules/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 6. Diretorios de documentacao interna / workflow
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
docs/superpowers/
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 7. Arquivos especificos (nao diretorios inteiros)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Segredos e env — NUNCA indexar
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.homolog.example
|
||||
|
||||
# TypeScript build info e next env declaration
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
typescript
|
||||
|
||||
# SQLite transient files (WAL/SHM/journal)
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
*.sqlite-journal
|
||||
|
||||
# Mapas e source maps
|
||||
*.map
|
||||
|
||||
# Bun / npm lockfiles ruidosos
|
||||
bun.lock
|
||||
|
||||
# `cheaper-inference-gateway.svg` e arquivos de midia na raiz/asset ja cobertos
|
||||
# pelos `ignored-suffix` do indexador (svg/png/jpg/ico/etc >50kB ou >500linhas);
|
||||
# manter a regra explicita aqui ajuda a auditar:
|
||||
cheaper-inference-gateway.svg
|
||||
cheaper-inference-gateway-*.svg
|
||||
|
||||
# Husky internals
|
||||
.husky/_/
|
||||
|
||||
# CI / quality metric artifacts
|
||||
config/quality/quality-metrics.json
|
||||
config/quality/test-impact-map.json
|
||||
audit-report.json
|
||||
.gh-discussions.json
|
||||
|
||||
# i18n audit (gerado por npm run scripts)
|
||||
scripts/i18n/_audit.json
|
||||
scripts/i18n/_pending-keys.json
|
||||
|
||||
# Cli binario local (scratch)
|
||||
bin/omniroute.mjs
|
||||
|
||||
# Deploy / docker backups
|
||||
deploy.sh
|
||||
docker-compose.yml.bak
|
||||
docker-compose.minimal.yml
|
||||
@@ -18,7 +18,6 @@ coverage
|
||||
# Runtime data and logs
|
||||
data
|
||||
logs
|
||||
.sandbox
|
||||
|
||||
# Local env files (inject at runtime via --env-file or -e)
|
||||
.env
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
ENABLE_LIVE_DEVIN_TESTS=0
|
||||
DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
|
||||
DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
|
||||
51
.env.example
51
.env.example
@@ -84,12 +84,7 @@ PORT=20128
|
||||
# endpoint display (useDisplayBaseUrl) shows https://host/omniroute/v1 instead of
|
||||
# https://host/v1. Rebuild after changing this value (Next basePath is build-time).
|
||||
# Default: "" (served at the domain root). Example: /omniroute to serve under https://host/omniroute
|
||||
# Docker: baked at image build time via build-arg; root-path images can also apply this at
|
||||
# container start (see docs/guides/DOCKER_GUIDE.md).
|
||||
# OMNIROUTE_BASE_PATH=
|
||||
# Client fetch/EventSource under this path are rewritten via installBasePathFetch
|
||||
# (src/shared/utils/basePathFetch.ts) so absolute `/api/*` and `/v1/*` hits work
|
||||
# without a reverse-proxy rewrite. Rebuild after changing (Next basePath is build-time).
|
||||
#
|
||||
# Browser-visible mirror of OMNIROUTE_BASE_PATH, inlined at build time so the
|
||||
# dashboard endpoint display can read it client-side. Set it to the same value
|
||||
@@ -240,9 +235,8 @@ NODE_ENV=production
|
||||
|
||||
# Container runtime — controls startup script behavior (permissions, advice).
|
||||
# Values: docker | podman | Default: docker
|
||||
# Set to "podman" for any Podman topology. The entrypoint cannot determine
|
||||
# whether the engine is local or reached through Podman Machine, so it prints
|
||||
# topology-neutral guidance and links contrib/podman/README.md.
|
||||
# Set to "podman" when running under rootless Podman so the entrypoint
|
||||
# gives the correct fix instructions (podman unshare chown vs sudo chown).
|
||||
CONTAINER_HOST=docker
|
||||
|
||||
# Container runtime override for skill sandboxing.
|
||||
@@ -811,15 +805,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Default: <repo>/src/lib/db/migrations.
|
||||
#OMNIROUTE_MIGRATIONS_DIR=
|
||||
|
||||
# Additional migration directories, as `namespace=dir` entries separated by the
|
||||
# platform path delimiter (`:` on POSIX, `;` on Windows). Files found there are
|
||||
# recorded as `<namespace>-<number>` (e.g. `ee-134`), a version space that cannot
|
||||
# collide with the upstream numeric slots — so a distribution shipping its own
|
||||
# migrations never silently loses one to a number the upstream set also claimed.
|
||||
# A malformed entry, an invalid namespace or a missing directory aborts startup
|
||||
# rather than skipping the schema. Unset = no extra directories (the default).
|
||||
#OMNIROUTE_EXTRA_MIGRATIONS_DIRS=ee=/opt/app/enterprise/db/migrations
|
||||
|
||||
# Mass-pending-migrations safety threshold (#3416). If more than this many
|
||||
# migrations are pending on an existing DB, startup aborts (a wiped tracking
|
||||
# table could cause data loss). Raise it to restore an older backup; set to 0
|
||||
@@ -1414,6 +1399,10 @@ APP_LOG_TO_FILE=true
|
||||
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
|
||||
# OMNIROUTE_PLUGIN_PATH=
|
||||
|
||||
# Allow plugins to request the 'exec' permission (spawn child processes from the
|
||||
# plugin worker sandbox). Disabled by default; set to 1 to enable (local operator only).
|
||||
# OMNIROUTE_PLUGINS_ALLOW_EXEC=0
|
||||
|
||||
# ── Prompt cache (system prompt deduplication) ──
|
||||
# Used by: open-sse/services — caches identical system prompts across requests.
|
||||
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)
|
||||
@@ -1826,10 +1815,6 @@ APP_LOG_TO_FILE=true
|
||||
# Tokens reserved for completion output when computing prompt budgets.
|
||||
# Used by: open-sse/services/contextManager.ts. Default: 1024.
|
||||
# CONTEXT_RESERVE_TOKENS=1024
|
||||
# How many of the newest inline images to keep when pruning older ones to fit
|
||||
# the context window (#8560). Used by: open-sse/services/contextManager.ts.
|
||||
# Default: 2.
|
||||
# CONTEXT_KEEP_LATEST_IMAGES=2
|
||||
|
||||
# ── Model alias rewriting (legacy compatibility) ──
|
||||
# Toggle the legacy model-alias compatibility layer used by older clients.
|
||||
@@ -1839,18 +1824,6 @@ APP_LOG_TO_FILE=true
|
||||
# ── Devin CLI binary path ──
|
||||
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
|
||||
# CLI_DEVIN_BIN=devin
|
||||
# Agentic bridge-only binary override. The bridge still executes ACP stdio only.
|
||||
# CLI_DEVIN_AGENTIC_BIN=devin
|
||||
# Required isolated HOME for the agentic Devin child process.
|
||||
# DEVIN_AGENTIC_HOME=/home/bridge
|
||||
# Bounded ACP turn timeout in milliseconds. Default: 120000.
|
||||
# DEVIN_AGENTIC_ACP_TIMEOUT_MS=120000
|
||||
# Agentic bridge model aliases. Values must keep the devin-cli-agentic/ prefix.
|
||||
# DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
|
||||
# DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
|
||||
# DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
|
||||
# DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
|
||||
# DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
|
||||
|
||||
# ── Command Code (custom CLI) callback ──
|
||||
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
||||
@@ -2312,18 +2285,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ChatGPT Web (Codex) headless browser and outbound tool tunnel
|
||||
# Used by: open-sse/executors/chatgpt-web-codex.ts
|
||||
# Connection values entered in the dashboard override these global defaults.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
|
||||
# CHROME_PATH=/usr/bin/chromium
|
||||
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
|
||||
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
|
||||
# Containerized Chromium+VNC used for interactive browser-login credential
|
||||
|
||||
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
@@ -9,14 +9,11 @@
|
||||
|
||||
## Validation
|
||||
|
||||
Choose the change type and focused loop from the
|
||||
[Contribution Golden Path](../docs/dev/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
|
||||
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
|
||||
Run only the focused loop for what you changed — the full unit suite, Vitest, the
|
||||
60% coverage gate, and the production build all run in CI on this PR (#8329):
|
||||
|
||||
- [ ] Change type: provider / routing / UI / i18n / CLI / DB / build-deploy / other
|
||||
- [ ] Focused tests and category gates from the golden path
|
||||
- [ ] Focused tests for the change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
|
||||
- [ ] `npm run lint`
|
||||
- [ ] Reconciled with the current active release base; focused checks rerun afterward
|
||||
- [ ] Production-code changes include a new or updated automated test in this PR
|
||||
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
|
||||
|
||||
@@ -32,4 +29,4 @@ Vitest, the 60% coverage gate, and the production build all run in CI on this PR
|
||||
|
||||
## Reviewer Notes
|
||||
|
||||
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
|
||||
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
|
||||
71
.github/workflows/build-fork.yml
vendored
Normal file
71
.github/workflows/build-fork.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Publish Fork Image to GHCR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
# Least-privilege default: read-only at the top level; the build job that pushes to
|
||||
# GHCR grants packages: write itself (Scorecard TokenPermissions).
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Fork Image
|
||||
if: github.repository == 'kang-heewon/OmniRoute'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=sha,prefix=sha-
|
||||
type=ref,event=tag
|
||||
labels: |
|
||||
org.opencontainers.image.title=omniroute
|
||||
org.opencontainers.image.description=Unified AI proxy/router — fork image
|
||||
org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute
|
||||
org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: runner-base
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
252
.github/workflows/ci.yml
vendored
252
.github/workflows/ci.yml
vendored
@@ -27,7 +27,7 @@ env:
|
||||
jobs:
|
||||
changes:
|
||||
name: Change Classification
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code: ${{ steps.classify.outputs.code }}
|
||||
docs: ${{ steps.classify.outputs.docs }}
|
||||
@@ -35,22 +35,13 @@ jobs:
|
||||
workflow: ${{ steps.classify.outputs.workflow }}
|
||||
testsOnly: ${{ steps.classify.outputs.testsOnly }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
# Refuse a PR that targets its own head branch before spending anything on it. #8912 has
|
||||
# head == base == release/v3.8.50: no diff, can never merge, and it sits in the queue with
|
||||
# a full check board attached on every push to that branch. One field comparison.
|
||||
- name: Reject a PR that targets its own branch
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: node scripts/check/check-pr-self-target.mjs
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- id: classify
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
@@ -74,7 +65,7 @@ jobs:
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
@@ -88,9 +79,13 @@ jobs:
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run audit:deps
|
||||
@@ -130,17 +125,6 @@ jobs:
|
||||
- run: npm run check:route-guard-membership
|
||||
- run: npm run check:test-discovery
|
||||
- run: npm run check:tracked-artifacts
|
||||
# (gap 30) Also lives in quality.yml's PR-only "Merge integrity" job — because the
|
||||
# CHANGELOG half of that job needs a base to diff against. This half does NOT: the
|
||||
# generator either reproduces the committed SKILL.md files or it does not.
|
||||
#
|
||||
# Keeping it PR-only left a real hole. This cycle's merge trains landed in batches with
|
||||
# `--admin`, which bypasses required checks, so three SKILL.md files drifted from the route
|
||||
# catalog, rode the release squash into `main`, and the next cycle's sync-back turned them
|
||||
# into a base-red that blocked EVERY PR into release/v3.8.50 until #8954. Running it here
|
||||
# means a push to `main` catches the drift at the source instead of the next cycle
|
||||
# inheriting it.
|
||||
- run: npm run check:agent-skills-sync
|
||||
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
|
||||
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
|
||||
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
|
||||
@@ -164,7 +148,7 @@ jobs:
|
||||
|
||||
quality-gate:
|
||||
name: Quality Ratchet
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# needs lint so eslint-results artifact is available (same inventory as the
|
||||
# blocking lint step). Allow lint failure so other ratchets still run.
|
||||
needs: [changes, test-coverage, lint]
|
||||
@@ -184,9 +168,13 @@ jobs:
|
||||
contents: read
|
||||
security-events: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
@@ -278,7 +266,7 @@ jobs:
|
||||
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
|
||||
quality-extended:
|
||||
name: Quality Gates (Extended)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
@@ -288,10 +276,14 @@ jobs:
|
||||
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
|
||||
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
|
||||
# would lack the base ref and the gate would self-skip (base-unresolved).
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
# Dead-code, cognitive-complexity, type-coverage foram promovidos ao job
|
||||
# quality-gate (bloqueante) na Fase 7 INT — não rodam aqui para evitar duplo custo.
|
||||
@@ -338,16 +330,8 @@ jobs:
|
||||
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
|
||||
# actionlint — official download script
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
|
||||
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin.
|
||||
# PINNED on purpose. Unpinned, the runner installed whatever PyPI served that day and
|
||||
# measured 1 finding MORE than the devbox on the identical commit (190 vs 189) during
|
||||
# the v3.8.49 cycle — which cost a second rebaseline push per release, chasing a
|
||||
# number that was never the code's. The ratchet compares counts across machines, so
|
||||
# the auditor version has to be the same on both. Bump this deliberately, and
|
||||
# rebaseline in the same commit: check-workflows.mjs now prints `zizmorVersion=` next
|
||||
# to the count so the new number is traceable to the tool that produced it.
|
||||
ZIZMOR_VERSION=1.25.2
|
||||
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
|
||||
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin
|
||||
pipx install zizmor || pip install --user zizmor
|
||||
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
|
||||
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
|
||||
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
|
||||
@@ -394,16 +378,20 @@ jobs:
|
||||
|
||||
docs-sync-strict:
|
||||
name: Docs Sync (Strict)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:docs-all
|
||||
# Previously-orphaned contract gates (existed as files, never wired anywhere).
|
||||
@@ -423,7 +411,7 @@ jobs:
|
||||
|
||||
docs-lint:
|
||||
name: Docs Lint (prose — advisory)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
@@ -433,9 +421,13 @@ jobs:
|
||||
# existing doc corpus is brought up to style. Promote to blocking once it converges.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- name: markdownlint (docs + root, advisory)
|
||||
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
|
||||
- name: Vale prose lint (Microsoft style, advisory)
|
||||
@@ -450,42 +442,39 @@ jobs:
|
||||
|
||||
i18n-ui-coverage:
|
||||
name: i18n UI Coverage
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# UI keys move with dashboard code OR message catalogs.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
steps:
|
||||
# fetch-depth: 0 — the value-drift gate diffs en.json against the merge base to
|
||||
# find rewritten English strings. On a shallow clone the base ref is missing and
|
||||
# the gate self-skips (base-unresolved), so it would never actually run.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
||||
# #8463: a rewritten English value used to leave its 39 translations behind
|
||||
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
|
||||
# Key parity above cannot see it — a stale translation counts as covered.
|
||||
- name: i18n UI value drift (stale translations)
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: node scripts/i18n/check-ui-value-drift.mjs
|
||||
|
||||
# #8038: cheap single-locale glossary/protected-terms consistency gate —
|
||||
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
|
||||
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
|
||||
i18n-glossary-zhcn:
|
||||
name: i18n Glossary (zh-CN)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN
|
||||
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
|
||||
@@ -497,7 +486,7 @@ jobs:
|
||||
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
|
||||
i18n:
|
||||
name: i18n Validation (all languages)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
@@ -505,7 +494,7 @@ jobs:
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
@@ -540,12 +529,15 @@ jobs:
|
||||
pr-test-policy:
|
||||
name: PR Test Policy
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- name: Fetch base branch
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
|
||||
- name: Validate source changes include tests
|
||||
@@ -572,17 +564,17 @@ jobs:
|
||||
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
|
||||
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
|
||||
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
|
||||
# back to ubuntu-26.04 unless the PR head repo is this repository (push /
|
||||
# back to ubuntu-latest unless the PR head repo is this repository (push /
|
||||
# dispatch events are own-origin by definition). Any failure path (VM down,
|
||||
# var unset/false) also falls back to ubuntu-26.04.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
|
||||
# var unset/false) also falls back to ubuntu-latest.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
@@ -622,14 +614,18 @@ jobs:
|
||||
|
||||
package-artifact:
|
||||
name: Package Artifact
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- name: Download Next.js build artifact
|
||||
@@ -665,15 +661,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-26.04, windows-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
@@ -712,14 +708,8 @@ jobs:
|
||||
|
||||
test-unit:
|
||||
name: Unit Tests (${{ matrix.shard }}/8)
|
||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
|
||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
||||
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
|
||||
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
|
||||
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
|
||||
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
|
||||
# self-hosted is strictly worse here and there is nothing to configure.
|
||||
runs-on: ubuntu-26.04
|
||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 25
|
||||
# needs: changes (not build) — this job never downloads the next-build artifact;
|
||||
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
|
||||
@@ -737,9 +727,13 @@ jobs:
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
|
||||
@@ -769,27 +763,25 @@ jobs:
|
||||
|
||||
test-bun-sqlite:
|
||||
name: Bun SQLite Compatibility
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run test:bun:db
|
||||
|
||||
test-vitest:
|
||||
name: Vitest (MCP / autoCombo / UI components)
|
||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
|
||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
||||
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
|
||||
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
|
||||
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
|
||||
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
|
||||
# self-hosted is strictly worse here and there is nothing to configure.
|
||||
runs-on: ubuntu-26.04
|
||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
timeout-minutes: 15
|
||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||
needs: changes
|
||||
@@ -799,9 +791,13 @@ jobs:
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
# 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).
|
||||
@@ -829,7 +825,7 @@ jobs:
|
||||
# the release gate can still exercise them via workflow_dispatch when needed).
|
||||
test-coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
||||
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
|
||||
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
||||
@@ -840,9 +836,13 @@ jobs:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- name: Download all shard coverage
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -926,14 +926,14 @@ jobs:
|
||||
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -978,7 +978,7 @@ jobs:
|
||||
|
||||
coverage-pr-comment:
|
||||
name: PR Coverage Comment
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }}
|
||||
needs:
|
||||
- changes
|
||||
@@ -1057,7 +1057,7 @@ jobs:
|
||||
|
||||
test-e2e:
|
||||
name: E2E Tests (${{ matrix.shard }}/9)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# 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).
|
||||
@@ -1079,9 +1079,13 @@ jobs:
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- name: Cache Playwright browsers
|
||||
@@ -1111,8 +1115,7 @@ jobs:
|
||||
if FILES=$(node scripts/quality/balance-e2e-shards.mjs "$SHARD" 9); then
|
||||
if [ -z "$FILES" ]; then echo "[e2e-balance] shard $SHARD has no files"; exit 0; fi
|
||||
echo "[e2e-balance] shard $SHARD runs:"; echo "$FILES"
|
||||
# FILES is our own newline-separated path list, so word-splitting is intended
|
||||
# shellcheck disable=SC2086,SC2046
|
||||
# shellcheck disable=SC2086 — FILES is our own newline-separated path list
|
||||
npx playwright test $(echo "$FILES" | tr '\n' ' ') --reporter=line,junit
|
||||
else
|
||||
echo "[e2e-balance] balancer unavailable — plain --shard fallback"
|
||||
@@ -1130,7 +1133,7 @@ jobs:
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests (${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||
needs: changes
|
||||
@@ -1146,9 +1149,13 @@ jobs:
|
||||
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
|
||||
@@ -1156,7 +1163,7 @@ jobs:
|
||||
|
||||
test-security:
|
||||
name: Security Tests
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||
@@ -1165,16 +1172,20 @@ jobs:
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run test:security
|
||||
|
||||
ci-summary:
|
||||
name: CI Dashboard
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() }}
|
||||
needs:
|
||||
- changes
|
||||
@@ -1206,8 +1217,6 @@ jobs:
|
||||
- name: Generate dashboard
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
# Workflow-controlled data (job results), not user input — safe to read here.
|
||||
NEEDS_JSON: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
status() {
|
||||
case "$1" in
|
||||
@@ -1222,29 +1231,6 @@ jobs:
|
||||
echo "# 🚀 CI Dashboard" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# (gap 12) A cancelled job never reported a verdict, and in a long table that reads the
|
||||
# same as a green one. `cancel-in-progress` plus incremental fixing cancels jobs on every
|
||||
# push, and this cycle the Vitest job was cancelled in rounds 1, 2 and 3 — it only ran to
|
||||
# completion in round 4, where it revealed a suite that had been broken the whole cycle
|
||||
# plus two production bugs. A gate that never finishes is indistinguishable from one that
|
||||
# passes, so name them at the TOP instead of leaving them to be spotted mid-table.
|
||||
CANCELLED_JOBS=$(printf '%s' "$NEEDS_JSON" \
|
||||
| jq -r 'to_entries | map(select(.value.result == "cancelled")) | .[].key' 2>/dev/null \
|
||||
| sort | paste -sd", " -) || CANCELLED_JOBS=""
|
||||
if [ -n "$CANCELLED_JOBS" ]; then
|
||||
{
|
||||
echo "> ### ⚫ Cancelled — no verdict was reported"
|
||||
echo ">"
|
||||
echo "> \`$CANCELLED_JOBS\`"
|
||||
echo ">"
|
||||
echo "> These did not fail; they never finished, so nothing was checked. Treat this"
|
||||
echo "> run as INCOMPLETE for those gates. If the cancellation came from"
|
||||
echo "> \`cancel-in-progress\` on a newer push, the newer run covers it — otherwise"
|
||||
echo "> re-run them before reading this dashboard as green."
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
echo "## 🧱 Core Checks" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
4
.github/workflows/claude.yml
vendored
4
.github/workflows/claude.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 1
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -13,13 +13,13 @@ permissions:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (javascript-typescript)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
|
||||
14
.github/workflows/dast-smoke.yml
vendored
14
.github/workflows/dast-smoke.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
dast-smoke:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
|
||||
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
|
||||
continue-on-error: true
|
||||
@@ -30,9 +30,13 @@ jobs:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Build CLI bundle
|
||||
env:
|
||||
@@ -55,14 +59,8 @@ jobs:
|
||||
- run: pip install schemathesis
|
||||
- name: Schemathesis smoke (high-risk endpoints, blocking)
|
||||
run: |
|
||||
# /api/auth/oidc/* is a BROWSER redirect flow (302 to the IdP, 302 back to
|
||||
# /login?oidc_error=... on every failure), not a REST endpoint: Schemathesis reads
|
||||
# those 302s as "the API accepted a schema-violating request" and the configured-off
|
||||
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
|
||||
# still right (operators need it); fuzzing it is not what this smoke is for.
|
||||
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
|
||||
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
|
||||
--exclude-path-regex '^/api/auth/oidc/' \
|
||||
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
|
||||
--request-timeout 20 --suppress-health-check all --no-color
|
||||
- name: promptfoo injection-guard (blocking)
|
||||
|
||||
2
.github/workflows/deploy-vps.yml
vendored
2
.github/workflows/deploy-vps.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
(github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
|
||||
&& vars.DEPLOY_ENABLED == 'true'
|
||||
name: Deploy OmniRoute to VPS
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check VPS SSH reachability from runner
|
||||
id: reach
|
||||
|
||||
10
.github/workflows/docker-publish.yml
vendored
10
.github/workflows/docker-publish.yml
vendored
@@ -33,7 +33,7 @@ permissions:
|
||||
jobs:
|
||||
prepare:
|
||||
name: Resolve Docker release metadata
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
promote_latest: ${{ steps.version.outputs.promote_latest }}
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
IMAGE_NAME: diegosouzapw/omniroute
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
- prepare
|
||||
- build
|
||||
if: needs.prepare.outputs.skip != 'true'
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||
|
||||
103
.github/workflows/electron-release.yml
vendored
103
.github/workflows/electron-release.yml
vendored
@@ -20,14 +20,14 @@ permissions:
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate version
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
version: ${{ steps.validate.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -78,17 +78,17 @@ jobs:
|
||||
target: mac-arm64
|
||||
ext: -arm64.dmg
|
||||
- platform: linux
|
||||
runner: ubuntu-26.04
|
||||
runner: ubuntu-latest
|
||||
target: linux
|
||||
ext: .AppImage
|
||||
deb_ext: .deb
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
@@ -120,18 +120,6 @@ jobs:
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
NODE_OPTIONS: "--max_old_space_size=6144"
|
||||
# Linux builds with webpack, not Turbopack. Turbopack's production build
|
||||
# allocates natively (Rust, off the V8 heap), so --max_old_space_size does
|
||||
# not bound it, and on this module graph it peaks above what the hosted
|
||||
# runner can give — the VM is reclaimed mid-compile with "The runner has
|
||||
# received a shutdown signal", no exit code. That is what silently took the
|
||||
# whole desktop channel out of v3.8.49: the linux leg died, `release` was
|
||||
# skipped, and the release shipped with ZERO assets. Measured on a 32 GB
|
||||
# box the same build passes and peaks past 14 GB. The webpack fallback is
|
||||
# the project's documented escape hatch for RAM-constrained machines
|
||||
# (docs/reference/ENVIRONMENT.md, #6409) and is the same remedy already
|
||||
# applied to nightly-compat's Node 26 build (#8090).
|
||||
OMNIROUTE_USE_TURBOPACK: ${{ matrix.platform == 'linux' && '0' || '1' }}
|
||||
run: npm run build
|
||||
|
||||
- name: Sync version in electron/package.json
|
||||
@@ -229,53 +217,21 @@ jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
needs: [validate, build]
|
||||
# Fail-partial, not fail-closed. `build` is a 4-leg matrix with `fail-fast: false`,
|
||||
# so the legs that succeed still upload their artifacts — but a default `needs:`
|
||||
# gate skips this job the moment ANY leg fails, discarding all of them. That is
|
||||
# exactly what happened to v3.8.49: the linux leg died and the release shipped with
|
||||
# ZERO assets, throwing away 1.7 GB of good Windows/macOS installers **and** the
|
||||
# source archives + SBOM, which do not depend on a build at all. The result was
|
||||
# indistinguishable from "this version has no desktop channel".
|
||||
# Now: attach everything that did build, then fail the job loudly (see the last
|
||||
# step) so an incomplete channel is visible instead of silent.
|
||||
if: ${{ !cancelled() && needs.validate.result == 'success' }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # softprops/action-gh-release creates the GitHub Release
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
# `merge-multiple` is deliberately OFF. It resolves same-name collisions by ARRIVAL
|
||||
# ORDER, and the two macOS jobs each emit their own `latest-mac.yml` listing only their
|
||||
# own dmg (measured: 338 and 350 bytes, different content, identical name). One silently
|
||||
# overwrote the other — arm64 won in the published v3.8.48, and since the Intel dmg
|
||||
# carries no arch suffix in its name, electron-updater's
|
||||
# `files.find(url includes process.arch) ?? files.shift()` sends every Intel Mac to the
|
||||
# ARM dmg. Downloading into per-artifact subdirectories keeps both, so they can be
|
||||
# merged on purpose instead of by luck.
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
# Writes release-assets/latest-mac.yml with BOTH dmgs, un-suffixed entry first (that is
|
||||
# the one electron-updater can only reach through its fallback). Refuses to write when the
|
||||
# inputs disagree on version — a manifest stitched from two builds is worse than none.
|
||||
- name: Merge the per-arch macOS updater manifests
|
||||
run: node scripts/release/merge-mac-update-manifest.mjs artifacts release-assets
|
||||
|
||||
# Everything else moves across as-is. The partial latest-mac.yml files are excluded so
|
||||
# they cannot clobber the merged one; -n is a second belt on the same braces.
|
||||
- name: Collect the remaining artifacts
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
find artifacts -type f ! -name latest-mac.yml -exec cp -n {} release-assets/ \;
|
||||
echo "release-assets:"
|
||||
ls -la release-assets/
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create source archives
|
||||
env:
|
||||
@@ -319,47 +275,6 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
verify-desktop-assets:
|
||||
name: Verify desktop assets landed
|
||||
needs: [validate, release]
|
||||
# Deliberately a SEPARATE job, not a final step of `release`: failing inside
|
||||
# `release` would cascade into `publish-npm` (which gates on `needs: release`) and
|
||||
# block the npm channel over a desktop-only gap. Here the assets are attached, npm
|
||||
# still publishes, and an incomplete desktop channel shows up as a red job instead
|
||||
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
|
||||
# green, because nothing ever asserted the release HAS binaries.
|
||||
if: ${{ !cancelled() && needs.release.result == 'success' }}
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Assert every platform is present on the release
|
||||
env:
|
||||
# Regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in the `validate` job, and
|
||||
# passed via env rather than interpolated into the script body.
|
||||
VERSION: ${{ needs.validate.outputs.version }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
names=$(gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" \
|
||||
--json assets --jq '.assets[].name')
|
||||
echo "Assets on $VERSION:"
|
||||
echo "$names" | sed 's/^/ /'
|
||||
|
||||
missing=""
|
||||
# `[ ... ] && missing=...` as the last command in a branch returns 1 and
|
||||
# would abort the whole script under Actions' default `set -e`. Use if/fi.
|
||||
for want in '\.exe$' '\.dmg$' '\.AppImage$' '\.deb$' '^latest.*\.yml$' '\.source\.tar\.gz$'; do
|
||||
if ! echo "$names" | grep -qE "$want"; then
|
||||
missing="$missing $want"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error::Desktop channel incomplete on $VERSION — no asset matching:$missing"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ every platform present on $VERSION"
|
||||
|
||||
publish-npm:
|
||||
name: Publish to npm
|
||||
needs: [validate, release]
|
||||
|
||||
4
.github/workflows/lock-released-branch.yml
vendored
4
.github/workflows/lock-released-branch.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
lock-branch:
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Lock release/<tag> branch
|
||||
env:
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
guard-no-push-after-release:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Reject push if matching release tag exists
|
||||
env:
|
||||
|
||||
8
.github/workflows/mutation-redundancy.yml
vendored
8
.github/workflows/mutation-redundancy.yml
vendored
@@ -22,7 +22,7 @@ permissions:
|
||||
jobs:
|
||||
stryker-nobail:
|
||||
name: Stryker disableBail (batch ${{ matrix.batch.name }})
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -41,9 +41,13 @@ jobs:
|
||||
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
|
||||
timeout-minutes: 300
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Run Stryker (disableBail)
|
||||
env:
|
||||
|
||||
31
.github/workflows/nightly-compat.yml
vendored
31
.github/workflows/nightly-compat.yml
vendored
@@ -28,11 +28,11 @@ concurrency:
|
||||
jobs:
|
||||
resolve-branch:
|
||||
name: Resolve active release branch
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
target: ${{ steps.branch.outputs.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -58,37 +58,24 @@ jobs:
|
||||
|
||||
compat-build-26:
|
||||
name: Node 26 Compatibility Build
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: resolve-branch
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.resolve-branch.outputs.target }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "26"
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
# #8090 — this is the ONLY Node 26 build in the whole CI matrix (ci.yml pins
|
||||
# CI_NODE_VERSION=24). It failed every nightly with the runner-reclaimed
|
||||
# signature ("The runner has received a shutdown signal" / "The operation was
|
||||
# canceled", no exit code) always at the same Turbopack compile phase — a
|
||||
# classic OOM kill on the memory-constrained 16 GB hosted runner. Turbopack's
|
||||
# native (Rust, off-V8-heap) allocation is NOT bounded by --max-old-space-size
|
||||
# and peaks far higher than webpack on this large module graph (#6409), and is
|
||||
# heavier still under Node 26. Use the documented webpack fallback here: it still
|
||||
# validates that the app *builds* on Node 26 (the point of this compat job) at a
|
||||
# much lower memory peak. Turbopack-on-Node-24 stays covered by ci.yml's build
|
||||
# job. See docs/reference/ENVIRONMENT.md (OMNIROUTE_USE_TURBOPACK) and #6409.
|
||||
- run: npm run build
|
||||
env:
|
||||
OMNIROUTE_USE_TURBOPACK: "0"
|
||||
|
||||
compat-tests:
|
||||
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: resolve-branch
|
||||
strategy:
|
||||
@@ -102,11 +89,11 @@ jobs:
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
TEST_SHARD: ${{ matrix.shard }}/4
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.resolve-branch.outputs.target }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
@@ -116,7 +103,7 @@ jobs:
|
||||
|
||||
report:
|
||||
name: Open / update tracking issue on failure
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
|
||||
needs: [resolve-branch, compat-build-26, compat-tests]
|
||||
permissions:
|
||||
|
||||
13
.github/workflows/nightly-llm-security.yml
vendored
13
.github/workflows/nightly-llm-security.yml
vendored
@@ -10,11 +10,13 @@ permissions:
|
||||
jobs:
|
||||
promptfoo-guard:
|
||||
name: promptfoo — injection guard (block mode, no secret)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with: { node-version: "24", cache: npm }
|
||||
- run: npm ci
|
||||
- name: Build CLI bundle
|
||||
env:
|
||||
@@ -44,7 +46,7 @@ jobs:
|
||||
|
||||
garak:
|
||||
name: garak probes (skip without provider secret)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# NOTE: the `secrets` context is NOT available in a job-level `if:` — referencing
|
||||
# it there makes GitHub reject the file on push (startup_failure on every push).
|
||||
# Map the secret into a job-level env and gate each step on a presence check, so
|
||||
@@ -61,10 +63,13 @@ jobs:
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
|
||||
fi
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
if: steps.gate.outputs.run == 'true'
|
||||
- uses: actions/setup-node@v7
|
||||
if: steps.gate.outputs.run == 'true'
|
||||
with: { node-version: "24", cache: npm }
|
||||
- run: npm ci
|
||||
if: steps.gate.outputs.run == 'true'
|
||||
- name: Build CLI bundle
|
||||
|
||||
15
.github/workflows/nightly-mutation.yml
vendored
15
.github/workflows/nightly-mutation.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
||||
jobs:
|
||||
stryker:
|
||||
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# Mutation testing is expensive. History of the budget:
|
||||
# - Full 8-module set TIMED OUT at the 180min cap (run 27705123780 = exactly 180min).
|
||||
# The two god-files chatCore.ts/combo.ts dominated ~2/3 of the mutants and were
|
||||
@@ -104,9 +104,13 @@ jobs:
|
||||
# scripts/quality/mutation-radiography.mjs both merge per file).
|
||||
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore Stryker incremental cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -141,12 +145,15 @@ jobs:
|
||||
name: Mutation score ratchet (blocking)
|
||||
needs: stryker
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: Download all mutation reports
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
|
||||
8
.github/workflows/nightly-property.yml
vendored
8
.github/workflows/nightly-property.yml
vendored
@@ -8,11 +8,15 @@ permissions:
|
||||
issues: write
|
||||
jobs:
|
||||
property-random-seed:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: fast-check random seed (high runs)
|
||||
id: prop
|
||||
|
||||
155
.github/workflows/nightly-release-green.yml
vendored
155
.github/workflows/nightly-release-green.yml
vendored
@@ -68,13 +68,13 @@ jobs:
|
||||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
||||
# no local noauth CLIs => zero machine-specific false positives) and no contention.
|
||||
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
git checkout "$TARGET"
|
||||
git log -1 --oneline
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
@@ -143,8 +143,7 @@ jobs:
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# MODE is an intentional flag list, so word-splitting is wanted here
|
||||
# shellcheck disable=SC2086
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
1> release-green.json 2> release-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
@@ -217,19 +216,19 @@ jobs:
|
||||
# On a push, only run for a push to main — a push to release/* is handled by
|
||||
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
@@ -249,8 +248,7 @@ jobs:
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# MODE is an intentional flag list, so word-splitting is wanted here
|
||||
# shellcheck disable=SC2086
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
1> main-green.json 2> main-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
@@ -303,140 +301,3 @@ jobs:
|
||||
main-green.json
|
||||
main-green.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ── Banking lane (#8584) ──────────────────────────────────────────────────
|
||||
# The ratchet is asymmetric: RAISING a cap is a ten-second manual JSON edit made
|
||||
# under merge pressure, LOWERING one requires someone to run `--update` and commit
|
||||
# — which no workflow does. Grep `.github/workflows/` for `--update`: only
|
||||
# wiki-sync.yml (unrelated) and ci.yml's check-quality-ratchet.mjs --require-tighten
|
||||
# (a different script, a different metric). So a cap outlives the code that earned
|
||||
# it and every completed decomposition silently becomes a growth allowance for
|
||||
# whoever touches the file next. Measured on 2026-07-25: 18 frozen files already at
|
||||
# or under the 800-line new-file cap, up to 132x (schemas.ts, 19 lines / 2523 cap),
|
||||
# and 31 unfulfilled "tighten via --update next cycle" notes honoured exactly once
|
||||
# (-1 unit) in six weeks.
|
||||
#
|
||||
# This job closes that loop by making the DOWNWARD direction as automatic as the
|
||||
# upward one is easy. It measures the active release branch, runs the shrink-only
|
||||
# `--update` paths, and opens ONE always-current PR with the result. It never
|
||||
# pushes to release/* — a human still merges, so a bad measurement cannot land
|
||||
# unreviewed. verify-ratchet-bank.mjs is the hard guarantee that the automation can
|
||||
# only ever write in the shrink direction; if anything was raised, added, or a
|
||||
# rebaseline note was touched, the job aborts and opens nothing.
|
||||
#
|
||||
# Schedule/dispatch only, deliberately NOT on push: banking has no latency
|
||||
# requirement (a shrink banked within 8h is fine) and a per-merge run would rebuild
|
||||
# the PR branch repeatedly during merge campaigns while paying for a full ESLint
|
||||
# walk each time. Detection stays on push (release-green above); banking is batched.
|
||||
bank-ratchet-shrinks:
|
||||
name: Bank ratchet shrinks
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve active release branch
|
||||
id: branch
|
||||
env:
|
||||
INPUT_BRANCH: ${{ github.event.inputs.branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||||
TARGET="$INPUT_BRANCH"
|
||||
else
|
||||
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
|
||||
| sed 's#origin/##' \
|
||||
| sort -t/ -k2 -V \
|
||||
| tail -1)
|
||||
fi
|
||||
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
|
||||
# Same strict guard as the validation job — blocks ref/command injection
|
||||
# through the workflow_dispatch input.
|
||||
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "Refusing non-canonical branch name: $TARGET"; exit 1
|
||||
fi
|
||||
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
|
||||
echo "bank_branch=chore/bank-ratchet-${TARGET#release/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout the release branch
|
||||
env:
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git checkout "$TARGET"
|
||||
git log -1 --oneline
|
||||
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
|
||||
- name: Ratchet the baselines down
|
||||
run: |
|
||||
# Both --update paths are shrink-only by construction (file-size writes only
|
||||
# on `improvements`, complexity-ratchets only when `.improved`), and both exit
|
||||
# non-zero while the branch is over baseline — which is exactly when there is
|
||||
# nothing to bank. Their exit code is not the signal; the verifier below is.
|
||||
set +e
|
||||
node scripts/check/check-file-size.mjs --update
|
||||
node scripts/check/check-complexity-ratchets.mjs --update
|
||||
exit 0
|
||||
|
||||
- name: Verify the write only went downward
|
||||
id: verify
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Exits 1 if ANYTHING was raised/added or a rebaseline note was touched.
|
||||
# `set -e` then aborts the job before a commit exists — no PR is opened.
|
||||
node scripts/quality/verify-ratchet-bank.mjs > bank-summary.md
|
||||
if [ -n "$(git status --porcelain config/quality/)" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Nothing to bank — baselines already match the code."
|
||||
fi
|
||||
|
||||
- name: Open / update the banking PR
|
||||
if: steps.verify.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
BANK_BRANCH: ${{ steps.branch.outputs.bank_branch }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -B "$BANK_BRANCH"
|
||||
git add config/quality/
|
||||
git commit -m "chore(quality): bank ratchet shrinks measured on ${TARGET}"
|
||||
git push --force origin "$BANK_BRANCH"
|
||||
|
||||
{
|
||||
echo "Automated banking of quality-ratchet **shrinks** already present in"
|
||||
echo "\`${TARGET}\` — the downward half of the ratchet, which nothing else runs (#8584)."
|
||||
echo ""
|
||||
echo "Produced by \`check:file-size --update\` + \`check:complexity-ratchets --update\`,"
|
||||
echo "then verified by \`scripts/quality/verify-ratchet-bank.mjs\`: **nothing was raised,"
|
||||
echo "nothing was added, no rebaseline note was touched** — the job aborts without"
|
||||
echo "opening a PR if any of those is violated."
|
||||
echo ""
|
||||
echo "No product code changes. Merging retires growth allowances that the code no"
|
||||
echo "longer needs; not merging leaves them available to whoever edits those files next."
|
||||
echo ""
|
||||
cat bank-summary.md
|
||||
echo ""
|
||||
echo "**Run:** ${RUN_URL}"
|
||||
} > pr-body.md
|
||||
|
||||
EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BANK_BRANCH" \
|
||||
--state open --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING" ]; then
|
||||
gh pr edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file pr-body.md
|
||||
echo "Updated existing PR #$EXISTING"
|
||||
else
|
||||
gh pr create --repo "$GITHUB_REPOSITORY" --base "$TARGET" --head "$BANK_BRANCH" \
|
||||
--title "chore(quality): bank ratchet shrinks (${TARGET})" --body-file pr-body.md
|
||||
fi
|
||||
|
||||
32
.github/workflows/nightly-resilience.yml
vendored
32
.github/workflows/nightly-resilience.yml
vendored
@@ -10,31 +10,43 @@ permissions:
|
||||
jobs:
|
||||
heap:
|
||||
name: Heap-growth gate
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:heap
|
||||
|
||||
chaos:
|
||||
name: Resilience chaos (fault injection)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:chaos
|
||||
|
||||
k6-soak:
|
||||
name: k6 load/soak
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Build CLI bundle
|
||||
env:
|
||||
@@ -66,7 +78,7 @@ jobs:
|
||||
|
||||
a11y:
|
||||
name: A11y axe (nightly, freeze-and-alert)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
# The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and
|
||||
# boots the standalone server itself (waits on /api/monitoring/health, 15min webServer
|
||||
# timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact,
|
||||
@@ -80,9 +92,13 @@ jobs:
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
REQUIRE_AXE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v6.1.0
|
||||
|
||||
6
.github/workflows/nightly-schemathesis.yml
vendored
6
.github/workflows/nightly-schemathesis.yml
vendored
@@ -10,12 +10,14 @@ permissions:
|
||||
jobs:
|
||||
schemathesis:
|
||||
name: Schemathesis — OpenAPI contract fuzz (advisory)
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with: { node-version: "24", cache: npm }
|
||||
- run: npm ci
|
||||
- name: Build CLI bundle
|
||||
env:
|
||||
|
||||
99
.github/workflows/npm-publish.yml
vendored
99
.github/workflows/npm-publish.yml
vendored
@@ -56,21 +56,14 @@ env:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Same dynamic-runner rule as ci.yml's `build`/`test-unit`: `build:cli` falls back to a
|
||||
# full `next build`, whose working set outgrew the 16 GB hosted runner during the
|
||||
# v3.8.49 cycle — the publish died with "The runner has received a shutdown signal"
|
||||
# mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min.
|
||||
# This job never runs on `pull_request`, so the fork-safety clause is always true here;
|
||||
# it is kept verbatim so the expression stays greppable against ci.yml.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read # find + download the CI run's next-build artifact for this SHA
|
||||
contents: write # gh release upload (attach SBOM to the GitHub Release)
|
||||
id-token: write # npm provenance
|
||||
packages: write # publish to npm.pkg.github.com
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Need full tag history to compare against highest semver when
|
||||
@@ -78,7 +71,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
@@ -152,74 +145,6 @@ jobs:
|
||||
run: |
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
|
||||
# Fast path: CI already built the standalone tree for THIS commit and uploaded it as
|
||||
# `next-build`. `build:cli` (scripts/build/prepublish.ts) only shells out to a full
|
||||
# `next build` when `.build/next/standalone/server.js` is missing — restoring the
|
||||
# artifact turns the heaviest step of the publish into a download. Matching on
|
||||
# `head_sha` is the tree-equality guarantee: same commit, same tree.
|
||||
# Best-effort by design (retention is 1 day): every miss falls through to the build
|
||||
# step below, which is why the dynamic runner above matters as the backstop.
|
||||
#
|
||||
# The `head_repository.full_name == env.REPO` clause is a supply-chain guard, not a
|
||||
# filter refinement. This artifact becomes the published npm tarball. `pull_request`
|
||||
# runs from forks execute in THIS repository's context and upload their own
|
||||
# `next-build` built from fork-controlled source, and the runs API returns them for a
|
||||
# matching `head_sha` — 57 such runs exist in this repo today. Without the clause,
|
||||
# anything that made a fork's head commit coincide with the publish commit could put
|
||||
# attacker-built bytes on npm. Requiring the run to originate from this repository
|
||||
# excludes every fork run while keeping the fast path intact (verified: the same
|
||||
# single run is selected either way for the current tip).
|
||||
# CodeQL: actions/artifact-poisoning/critical.
|
||||
- name: Reuse CI's next-build artifact (skips the heavy rebuild)
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
# The question is "which run HAS the artifact", not "which run passed" (gap 16).
|
||||
# Requiring `conclusion == "success"` on the whole run discarded a perfectly good tree
|
||||
# whenever any unrelated shard went red — one flaky test then pushed the publish into
|
||||
# the 40-minute build this step exists to avoid. The artifact is only uploaded if the
|
||||
# Build job itself succeeded, so its PRESENCE is the accurate signal; the run's overall
|
||||
# conclusion is noise from jobs that have nothing to do with the tree.
|
||||
#
|
||||
# `head_repository.full_name == env.REPO` stays, and it is not a filter refinement:
|
||||
# this tree becomes the published npm tarball, and fork `pull_request` runs execute in
|
||||
# THIS repository's context uploading their own next-build. That clause is the
|
||||
# supply-chain guard (CodeQL actions/artifact-poisoning).
|
||||
CANDIDATES=$(gh api "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&per_page=100" \
|
||||
--jq '[.workflow_runs[]
|
||||
| select(.name == "CI"
|
||||
and .head_repository.full_name == env.REPO)]
|
||||
| sort_by(.run_started_at) | reverse | .[0:5] | .[].id') || CANDIDATES=""
|
||||
if [ -z "$CANDIDATES" ]; then
|
||||
echo "::notice::no CI run from this repository for $HEAD_SHA — falling back to a full build"
|
||||
exit 0
|
||||
fi
|
||||
RUN=""
|
||||
for candidate in $CANDIDATES; do
|
||||
if gh run download "$candidate" --repo "$REPO" --name next-build --dir /tmp/next-build 2>/dev/null; then
|
||||
RUN="$candidate"
|
||||
break
|
||||
fi
|
||||
echo " run $candidate carries no usable next-build — trying the next"
|
||||
done
|
||||
if [ -z "$RUN" ]; then
|
||||
echo "::notice::none of the candidate runs still carries next-build (1-day retention) — falling back to a full build"
|
||||
exit 0
|
||||
fi
|
||||
tar -xzf /tmp/next-build/e2e-build.tar.gz -C .
|
||||
rm -rf /tmp/next-build
|
||||
if [ -f .build/next/standalone/server.js ]; then
|
||||
echo "✅ standalone tree restored from CI run $RUN — build:cli will skip next build"
|
||||
else
|
||||
echo "::notice::extract did not yield .build/next/standalone — falling back to a full build"
|
||||
rm -rf .build
|
||||
fi
|
||||
|
||||
- name: Build CLI bundle (standalone app)
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
env:
|
||||
@@ -256,18 +181,6 @@ jobs:
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: npm run check:pack-boot
|
||||
|
||||
# The boot-smoke above proves a CLEAN install boots. It does not prove the path that
|
||||
# actually broke us: installing over an existing version, where ~110 SQLite migrations
|
||||
# run against a populated database. v3.8.48 shipped as a hotfix because the published
|
||||
# 3.8.47 crashed on boot, and the v3.8.49 upgrade path was first exercised end-to-end
|
||||
# by hand on a real 3.8.48 box (VPS .16) — after publishing, which is exactly backwards.
|
||||
# Runs BEFORE `npm stage publish` so a broken upgrade never reaches the registry at all;
|
||||
# a staged package that is never approved simply expires, with no `npm deprecate` needed.
|
||||
- name: Prove clean-install AND upgrade-over-previous both boot
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
timeout-minutes: 30
|
||||
run: npm run check:install-upgrade
|
||||
|
||||
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
|
||||
# parks the exact bytes on the registry WITHOUT making them installable; the
|
||||
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
|
||||
@@ -339,20 +252,20 @@ jobs:
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
|
||||
publish-opencode-plugin:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # npm provenance
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
# Full history needed for auto-bump: git diff against previous release tag
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
12
.github/workflows/opencode-plugin-ci.yml
vendored
12
.github/workflows/opencode-plugin-ci.yml
vendored
@@ -26,16 +26,16 @@ defaults:
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["22", "24"]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
@@ -46,13 +46,13 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
|
||||
12
.github/workflows/opencode-provider-ci.yml
vendored
12
.github/workflows/opencode-provider-ci.yml
vendored
@@ -26,16 +26,16 @@ defaults:
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ["20", "22", "24"]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
@@ -45,13 +45,13 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
|
||||
233
.github/workflows/quality.yml
vendored
233
.github/workflows/quality.yml
vendored
@@ -25,17 +25,20 @@ jobs:
|
||||
# path filters share existence reasons: code / docs / i18n / workflow.
|
||||
changes:
|
||||
name: Change Classification
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code: ${{ steps.classify.outputs.code }}
|
||||
docs: ${{ steps.classify.outputs.docs }}
|
||||
i18n: ${{ steps.classify.outputs.i18n }}
|
||||
workflow: ${{ steps.classify.outputs.workflow }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- id: classify
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
@@ -54,39 +57,21 @@ jobs:
|
||||
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build (advisory)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Fork-safe fallback uses Ubuntu 26.04's bundled Node 24 without setup-node.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
|
||||
# #7307: advisory for the first week of release-PR runs; remove
|
||||
# continue-on-error after the production-build signal is stable.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: node -e 'if (process.versions.node.split(".")[0] !== process.env.CI_NODE_VERSION) throw new Error("Expected Node " + process.env.CI_NODE_VERSION)'
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run build
|
||||
env:
|
||||
OMNIROUTE_USE_TURBOPACK: "1"
|
||||
# No artifact upload here: the PR-to-release quality workflow has no
|
||||
# downstream package/e2e jobs that consume the Next.js build output.
|
||||
|
||||
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
|
||||
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
|
||||
docs-gates:
|
||||
name: Docs Gates (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
|
||||
- run: npm run check:api-docs-refs
|
||||
@@ -101,21 +86,8 @@ jobs:
|
||||
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
|
||||
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
|
||||
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
|
||||
# or a fork PR falls back to ubuntu-26.04, so this is inert until the flag flips.
|
||||
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
|
||||
# expression was DEAD CONFIGURATION: across 160 quality.yml runs the job never once landed on
|
||||
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
|
||||
# not at fault: in the same window ci.yml's Build demonstrably ran on omniroute-113-7 and
|
||||
# omniroute-113-6, so self-hosted runs are visible when they happen.
|
||||
#
|
||||
# And if it ever HAD fired it would have inherited the measured penalty, because this job's
|
||||
# first two steps are exactly the bottleneck: actions/setup-node + npm ci took 20m06s on .113
|
||||
# with 4 concurrent runners versus 16s hosted (npm cache restore saturating the link). Median
|
||||
# here is 5.6 min hosted across 72 successful runs.
|
||||
#
|
||||
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
|
||||
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
|
||||
runs-on: ubuntu-26.04
|
||||
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || '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:
|
||||
@@ -123,10 +95,14 @@ jobs:
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
@@ -165,81 +141,6 @@ jobs:
|
||||
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
|
||||
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
|
||||
- run: npm run check:complexity-ratchets
|
||||
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
|
||||
# The god-file refactor happens in PRs→release/**; without these, the release
|
||||
# rail never sees a new import cycle, dead code, duplication or a security
|
||||
# regression until the release PR to main. Deliberately NOT brought here:
|
||||
# bundle-size (self-skips without a build — this rail's build job is advisory
|
||||
# and uploads nothing, so it would be dead configuration) and the coverage
|
||||
# run (fast-unit already runs the full suite; the coverage ratchet stays on
|
||||
# the main rail via --allow-missing in lint-guard).
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:lockfile
|
||||
- name: Duplication ratchet
|
||||
run: npm run check:duplication
|
||||
- name: Dead-code ratchet (knip)
|
||||
run: npm run check:dead-code
|
||||
- name: Type coverage ratchet
|
||||
run: npm run check:type-coverage
|
||||
- name: Compression budget ratchet
|
||||
run: npm run check:compression-budget
|
||||
# Security scanners — same hardened install as ci.yml quality-extended
|
||||
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
|
||||
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
|
||||
# gates below SKIP (exit 0) when their binary is absent — only a measured
|
||||
# regression vs config/quality/quality-baseline.json blocks.
|
||||
- name: Install security scanners (gitleaks/osv/actionlint/zizmor/oasdiff)
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set +e
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
# Ratchets compare scanner COUNTS across runs. Pin every auditor: a rule-set
|
||||
# update must be an explicit PR that re-measures/rebaselines, never a random
|
||||
# red (or green) caused by whatever "latest" served that morning.
|
||||
GITLEAKS_VERSION=v8.30.1
|
||||
OSV_SCANNER_VERSION=v2.3.8
|
||||
ACTIONLINT_VERSION=v1.7.12
|
||||
ZIZMOR_VERSION=1.25.2
|
||||
OASDIFF_VERSION=v1.19.1
|
||||
# gitleaks — pinned linux x64 tarball via gh (authed), extract binary
|
||||
rm -rf /tmp/gl && mkdir -p /tmp/gl
|
||||
gh release download "$GITLEAKS_VERSION" --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
|
||||
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
|
||||
# osv-scanner — pinned linux amd64 bare binary via gh (authed)
|
||||
rm -rf /tmp/osv && mkdir -p /tmp/osv
|
||||
gh release download "$OSV_SCANNER_VERSION" --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
|
||||
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
|
||||
# actionlint — official installer from a pinned release tag (never main)
|
||||
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" "$HOME/.local/bin"
|
||||
# zizmor — pinned PyPI package (same version as ci.yml quality-extended)
|
||||
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
|
||||
# oasdiff — pinned linux amd64 tarball via gh (authed), extract binary
|
||||
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
|
||||
gh release download "$OASDIFF_VERSION" --repo Tufin/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
|
||||
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
|
||||
# ALWAYS export the bin dir (even if any step above failed)
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/gitleaks" version || true
|
||||
"$HOME/.local/bin/actionlint" -version || true
|
||||
"$HOME/.local/bin/osv-scanner" --version || true
|
||||
"$HOME/.local/bin/oasdiff" --version || true
|
||||
zizmor --version || true
|
||||
- name: Secret scan (gitleaks, ratchet, blocking)
|
||||
run: npm run check:secrets -- --ratchet
|
||||
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
|
||||
run: npm run check:vuln-ratchet -- --ratchet
|
||||
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
|
||||
run: npm run check:workflows -- --ratchet
|
||||
# BASE_REF is read by the script from the env (never interpolated into a
|
||||
# shell body) — workflow-injection-safe. actions/checkout fetches remote
|
||||
# refs, not a local branch named github.base_ref, so prefix origin/ or this
|
||||
# gate self-skips every PR with reason=base-unresolved.
|
||||
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: npm run check:openapi-breaking -- --ratchet
|
||||
- name: Typecheck (core)
|
||||
run: npm run typecheck:core
|
||||
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
|
||||
@@ -265,7 +166,7 @@ jobs:
|
||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
||||
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
|
||||
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
|
||||
# doubled wall time (~16 min extra on the hosted runner) without extra coverage.
|
||||
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
|
||||
#
|
||||
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
|
||||
# coverage remains required via `Unit Tests fast-path` (fast-unit).
|
||||
@@ -329,15 +230,30 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
# Share fast-gates' checkout + npm ci instead of spending ~80s preparing a
|
||||
# separate runner for a ~13s Vitest invocation. !cancelled() preserves the
|
||||
# independent test signal when an earlier fast gate fails.
|
||||
- name: Vitest
|
||||
if: ${{ !cancelled() }}
|
||||
run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
|
||||
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast path runs on every PR.
|
||||
# Advisory upload, own-origin only.
|
||||
- name: Upload Vitest results to Trunk (advisory)
|
||||
fast-vitest:
|
||||
name: Vitest (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
|
||||
# which is where flaky-detection volume actually comes from (ci.yml's heavy
|
||||
# jobs only run on the release PR). Advisory upload, own-origin only.
|
||||
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
@@ -350,18 +266,12 @@ jobs:
|
||||
name: Unit Tests fast-path (${{ matrix.shard }}/4)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-26.04).
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
|
||||
# critical path again (~8.5min → ~4.5min hosted; ~2min on the 8-slot
|
||||
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
|
||||
# runner box). Node's native --test-shard=N/total takes any denominator — only
|
||||
# this matrix and the TEST_SHARD env below encode the shard count.
|
||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
||||
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
|
||||
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
|
||||
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
|
||||
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
|
||||
# self-hosted is strictly worse here and there is nothing to configure.
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -371,9 +281,13 @@ jobs:
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
|
||||
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
|
||||
@@ -398,18 +312,16 @@ jobs:
|
||||
name: No new ESLint warnings
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
|
||||
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
|
||||
# quality-gate job). contents: read keeps checkout working.
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
@@ -423,29 +335,6 @@ jobs:
|
||||
- name: ESLint (baseline congelado — warning novo = vermelho)
|
||||
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
|
||||
run: npm run lint:json -- --max-warnings 0
|
||||
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
|
||||
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
|
||||
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
|
||||
# inventory, two consumers; same reason ci.yml chains lint → quality-gate).
|
||||
# The coverage-report artifact does not exist on this rail, so both ratchet
|
||||
# invocations run --allow-missing: coverage.* metrics skip gracefully while
|
||||
# the deterministic ones (eslint / openapi-coverage / i18n-ui) stay BLOCKING.
|
||||
# Coverage authority remains on the main rail (ci.yml test-coverage → quality-gate).
|
||||
- run: npm run quality:collect
|
||||
- name: Ratchet check (blocking)
|
||||
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
|
||||
- name: Require-tighten (blocking)
|
||||
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
|
||||
# CodeQL alerts ratchet — same semantics as ci.yml quality-gate: exits 1 ONLY
|
||||
# on a real regression (open alerts > baseline in quality-baseline.json);
|
||||
# a measurement failure (gh/auth/api) self-skips with exit 0.
|
||||
- name: CodeQL alerts ratchet (blocking)
|
||||
run: npm run check:codeql-ratchet
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Append ratchet summary
|
||||
if: always()
|
||||
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" || true
|
||||
|
||||
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
|
||||
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
|
||||
@@ -462,17 +351,21 @@ jobs:
|
||||
name: Merge integrity (changelog + generated skills)
|
||||
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
|
||||
run: npm run check:changelog-integrity
|
||||
|
||||
4
.github/workflows/scorecard.yml
vendored
4
.github/workflows/scorecard.yml
vendored
@@ -11,7 +11,7 @@ permissions: read-all
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# security-events: write removed — Scorecard findings are advisory and no longer
|
||||
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/semgrep.yml
vendored
4
.github/workflows/semgrep.yml
vendored
@@ -14,11 +14,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
semgrep:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run semgrep (advisory)
|
||||
|
||||
9
.github/workflows/wiki-sync.yml
vendored
9
.github/workflows/wiki-sync.yml
vendored
@@ -34,10 +34,15 @@ concurrency:
|
||||
jobs:
|
||||
sync-wiki:
|
||||
name: Sync wiki with docs
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Clone wiki
|
||||
env:
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -72,7 +72,6 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.devin-bridge.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
@@ -172,6 +171,7 @@ config/quality/test-impact-map.json
|
||||
# GitNexus local index
|
||||
.gitnexus
|
||||
.worktrees
|
||||
bin/omniroute.mjs
|
||||
|
||||
# Consistent with .dockerignore / .npmignore
|
||||
.omc/
|
||||
@@ -201,17 +201,12 @@ scripts/i18n/_pending-keys.json
|
||||
.codegraph/
|
||||
|
||||
# Fumadocs generated source
|
||||
/.source/
|
||||
|
||||
# Temporary local worktrees used to build unpublished npm tarballs
|
||||
/.deploy-build-*/
|
||||
.source/
|
||||
|
||||
# AI agent local settings and configs
|
||||
.agents/
|
||||
.antigravitycli/
|
||||
.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
@@ -248,8 +243,6 @@ _artifacts/ # release-green artifacts
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
.artifacts/
|
||||
# Isolated Devin bridge workspaces, evidence, and test databases
|
||||
.sandbox/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
@@ -257,4 +250,3 @@ tests/homolog/.auth/
|
||||
tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
docker-compose.yml.bak
|
||||
.playwright-cli/
|
||||
|
||||
@@ -86,10 +86,4 @@
|
||||
regexes = [
|
||||
'''latencyP\d{2}Ms''',
|
||||
'''interleaved-thinking-2025-05-14''',
|
||||
# v3.8.49 pre-flight (2026-07-28). Nenhum dos dois e credencial:
|
||||
# - chave de localStorage do banner de patrocinio (#8723), so um identificador de UI;
|
||||
# - x-api-key PUBLICO do Firefly web (documentado em open-sse/utils/publicCreds.ts:207);
|
||||
# as duas ocorrencias sinalizadas estao em COMENTARIOS JSDoc, o runtime le de resolvePublicCred().
|
||||
'''omniroute-kimi-sponsor-banner-dismissed-v1''',
|
||||
'''SunbreakWebUI1''',
|
||||
]
|
||||
|
||||
8
.source/dynamic.ts
Normal file
8
.source/dynamic.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
|
||||
import * as Config from '../source.config';
|
||||
|
||||
const create = await dynamic<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
|
||||
DocData: {
|
||||
}
|
||||
}>(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}});
|
||||
22
.source/source.config.mjs
Normal file
22
.source/source.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
// source.config.ts
|
||||
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
|
||||
var docs = defineDocs({
|
||||
dir: "docs",
|
||||
docs: {
|
||||
files: [
|
||||
"./architecture/**/*.md",
|
||||
"./guides/**/*.md",
|
||||
"./reference/**/*.md",
|
||||
"./frameworks/**/*.md",
|
||||
"./routing/**/*.md",
|
||||
"./security/**/*.md",
|
||||
"./compression/**/*.md",
|
||||
"./ops/**/*.md"
|
||||
]
|
||||
}
|
||||
});
|
||||
var source_config_default = defineConfig();
|
||||
export {
|
||||
source_config_default as default,
|
||||
docs
|
||||
};
|
||||
4
@omniroute/opencode-plugin/package-lock.json
generated
4
@omniroute/opencode-plugin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@omniroute/opencode-plugin",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@omniroute/opencode-plugin",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts",
|
||||
"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 tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -216,59 +216,6 @@ const optionsSchema = z
|
||||
*/
|
||||
export type OmniRoutePluginOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
/**
|
||||
* Explicit default state for every boolean `features.*` toggle.
|
||||
*
|
||||
* #7624: `featuresSchema` marks every flag `.optional()` with no default, and
|
||||
* the effective value is applied implicitly at each read site (default-ON flags
|
||||
* use the `features.X !== false` convention, default-OFF flags use
|
||||
* `features.X === true`). That implicit convention is scattered across the file,
|
||||
* so an operator who omits the `features` block cannot tell whether
|
||||
* combos / autoCombos / enrichment are enabled — they think features are
|
||||
* disabled when they are actually on. Centralising the declared defaults here
|
||||
* (mirroring the read-site conventions exactly, so runtime behaviour is
|
||||
* unchanged) makes the effective flags introspectable and self-documenting.
|
||||
*/
|
||||
export const OMNIROUTE_FEATURE_DEFAULTS = {
|
||||
// default-ON (read sites use `features.X !== false`)
|
||||
combos: true,
|
||||
autoCombos: true,
|
||||
enrichment: true,
|
||||
diskCache: true,
|
||||
providerTag: true,
|
||||
fetchInterceptor: true,
|
||||
geminiSanitization: true,
|
||||
// default-OFF (read sites use `features.X === true`)
|
||||
compressionMetadata: false,
|
||||
usableOnly: false,
|
||||
mcpAutoEmit: false,
|
||||
debugLog: false,
|
||||
startupDebug: false,
|
||||
} as const;
|
||||
|
||||
/** Union of the boolean feature-flag keys declared in `OMNIROUTE_FEATURE_DEFAULTS`. */
|
||||
export type OmniRouteFeatureFlag = keyof typeof OMNIROUTE_FEATURE_DEFAULTS;
|
||||
|
||||
/**
|
||||
* Resolve the EFFECTIVE boolean state of every feature toggle, applying the
|
||||
* declared default for any flag the operator omitted. A missing `features`
|
||||
* block (or an empty one) yields the full default set — so callers and the
|
||||
* startup diagnostics can surface exactly which features are active instead of
|
||||
* relying on the implicit `!== false` / `=== true` conventions. Purely
|
||||
* derived: it does not mutate options nor change any read-site behaviour.
|
||||
*/
|
||||
export function resolveEffectiveFeatureFlags(
|
||||
features?: OmniRoutePluginOptions["features"]
|
||||
): Record<OmniRouteFeatureFlag, boolean> {
|
||||
const f: Partial<Record<OmniRouteFeatureFlag, boolean>> = features ?? {};
|
||||
const out = {} as Record<OmniRouteFeatureFlag, boolean>;
|
||||
for (const key of Object.keys(OMNIROUTE_FEATURE_DEFAULTS) as OmniRouteFeatureFlag[]) {
|
||||
const val = f[key];
|
||||
out[key] = typeof val === "boolean" ? val : OMNIROUTE_FEATURE_DEFAULTS[key];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export const OMNIROUTE_PROVIDER_KEY = "omniroute" as const;
|
||||
|
||||
/** Deployed plugin version (injected at build time by tsup define). */
|
||||
@@ -2053,7 +2000,6 @@ async function writeStartupDiagnostics(params: {
|
||||
autoComboCount: number;
|
||||
enrichment: OmniRouteEnrichmentMap;
|
||||
autoCombos: OmniRouteRawAutoCombo[];
|
||||
features?: OmniRoutePluginOptions["features"];
|
||||
}): Promise<void> {
|
||||
const {
|
||||
providerId,
|
||||
@@ -2064,7 +2010,6 @@ async function writeStartupDiagnostics(params: {
|
||||
autoComboCount,
|
||||
enrichment,
|
||||
autoCombos,
|
||||
features,
|
||||
} = params;
|
||||
const enriched = [...enrichment.entries()];
|
||||
const withName = enriched.filter(([, e]) => e.name);
|
||||
@@ -2077,16 +2022,6 @@ async function writeStartupDiagnostics(params: {
|
||||
lines.push(
|
||||
`models=${modelCount} combos=${comboCount} enrichment=${enrichmentSize} autoCombos=${autoComboCount}`
|
||||
);
|
||||
// #7624: surface the EFFECTIVE feature flags so an operator who omitted the
|
||||
// `features` block can see combos/autoCombos/enrichment are on (the counts
|
||||
// above can read 0 for reasons unrelated to the flags — e.g. missing auth).
|
||||
const effectiveFlags = resolveEffectiveFeatureFlags(features);
|
||||
lines.push(
|
||||
`features(effective): ` +
|
||||
(Object.keys(effectiveFlags) as OmniRouteFeatureFlag[])
|
||||
.map((k) => `${k}=${effectiveFlags[k] ? "on" : "off"}`)
|
||||
.join(" ")
|
||||
);
|
||||
lines.push(
|
||||
`enrichment: ${withName.length} with name, ${withPricing.length} with pricing, ${withFree.length} free`
|
||||
);
|
||||
@@ -3195,7 +3130,6 @@ export function createOmniRouteProviderHook(
|
||||
autoComboCount: rawAutoCombos.length,
|
||||
enrichment: rawEnrichment,
|
||||
autoCombos: rawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5274,7 +5208,6 @@ export function createOmniRouteConfigHook(
|
||||
autoComboCount: rawAutoCombos.length,
|
||||
enrichment: rawEnrichment,
|
||||
autoCombos: rawAutoCombos,
|
||||
features: resolved.features,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* #7624 — explicit feature-flag defaults.
|
||||
*
|
||||
* The `features` block in opencode.json marks every toggle `.optional()` with
|
||||
* no default. The effective value was previously only knowable by tracing the
|
||||
* implicit `features.X !== false` (default-ON) / `features.X === true`
|
||||
* (default-OFF) convention scattered across each read site, which left
|
||||
* operators unsure whether combos / autoCombos / enrichment were enabled when
|
||||
* they omitted the block.
|
||||
*
|
||||
* `OMNIROUTE_FEATURE_DEFAULTS` declares those defaults explicitly and
|
||||
* `resolveEffectiveFeatureFlags(features)` derives the effective state for any
|
||||
* (possibly-undefined) features object, mirroring the read-site conventions
|
||||
* exactly. These are purely derived — runtime routing behaviour is unchanged.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
OMNIROUTE_FEATURE_DEFAULTS,
|
||||
resolveEffectiveFeatureFlags,
|
||||
} from "../src/index.js";
|
||||
|
||||
const DEFAULT_ON = [
|
||||
"combos",
|
||||
"autoCombos",
|
||||
"enrichment",
|
||||
"diskCache",
|
||||
"providerTag",
|
||||
"fetchInterceptor",
|
||||
"geminiSanitization",
|
||||
] as const;
|
||||
|
||||
const DEFAULT_OFF = [
|
||||
"compressionMetadata",
|
||||
"usableOnly",
|
||||
"mcpAutoEmit",
|
||||
"debugLog",
|
||||
"startupDebug",
|
||||
] as const;
|
||||
|
||||
test("OMNIROUTE_FEATURE_DEFAULTS: declares each flag with its documented default", () => {
|
||||
for (const key of DEFAULT_ON) {
|
||||
assert.equal(OMNIROUTE_FEATURE_DEFAULTS[key], true, `${key} defaults ON`);
|
||||
}
|
||||
for (const key of DEFAULT_OFF) {
|
||||
assert.equal(OMNIROUTE_FEATURE_DEFAULTS[key], false, `${key} defaults OFF`);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveEffectiveFeatureFlags: undefined features → full declared default set", () => {
|
||||
const flags = resolveEffectiveFeatureFlags(undefined);
|
||||
for (const key of DEFAULT_ON) {
|
||||
assert.equal(flags[key], true, `${key} effective ON when features omitted`);
|
||||
}
|
||||
for (const key of DEFAULT_OFF) {
|
||||
assert.equal(flags[key], false, `${key} effective OFF when features omitted`);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveEffectiveFeatureFlags: empty features object → same as omitted", () => {
|
||||
assert.deepEqual(
|
||||
resolveEffectiveFeatureFlags({}),
|
||||
resolveEffectiveFeatureFlags(undefined)
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveEffectiveFeatureFlags: explicit false disables a default-ON flag", () => {
|
||||
const flags = resolveEffectiveFeatureFlags({ autoCombos: false });
|
||||
assert.equal(flags.autoCombos, false, "explicit autoCombos:false honoured");
|
||||
// Untouched flags keep their declared defaults.
|
||||
assert.equal(flags.combos, true);
|
||||
assert.equal(flags.enrichment, true);
|
||||
});
|
||||
|
||||
test("resolveEffectiveFeatureFlags: explicit true enables a default-OFF flag", () => {
|
||||
const flags = resolveEffectiveFeatureFlags({ compressionMetadata: true });
|
||||
assert.equal(flags.compressionMetadata, true, "explicit compressionMetadata:true honoured");
|
||||
assert.equal(flags.usableOnly, false, "other opt-in flags stay OFF");
|
||||
});
|
||||
|
||||
test("resolveEffectiveFeatureFlags: non-boolean sibling keys do not leak into flags", () => {
|
||||
// features may also carry mcpToken/logLevel/apiFormat — the resolver must
|
||||
// only ever return the boolean toggle keys.
|
||||
const flags = resolveEffectiveFeatureFlags({
|
||||
mcpAutoEmit: true,
|
||||
mcpToken: "sk-mcp-token-abc",
|
||||
logLevel: "debug",
|
||||
});
|
||||
assert.equal(flags.mcpAutoEmit, true);
|
||||
assert.equal(Object.keys(flags).length, Object.keys(OMNIROUTE_FEATURE_DEFAULTS).length);
|
||||
assert.equal("mcpToken" in flags, false);
|
||||
assert.equal("logLevel" in flags, false);
|
||||
});
|
||||
5
@omniroute/openhands-plugin/.gitignore
vendored
5
@omniroute/openhands-plugin/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OmniRoute contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,118 +0,0 @@
|
||||
# @omniroute/openhands-plugin
|
||||
|
||||
OpenHands integration for the **OmniRoute AI Gateway**. Generates the OpenHands
|
||||
environment and Docker config that wires an OpenHands agent-server to a running
|
||||
OmniRoute instance — with the integration gotchas already handled.
|
||||
|
||||
## Why
|
||||
|
||||
Running OpenHands against OmniRoute directly hits several wall:
|
||||
|
||||
1. **Model name mismatch** — OpenHands sends `model: "deepseek-chat"`, OmniRoute
|
||||
uses provider-prefixed IDs (`ds/deepseek-v4-flash`) or combos.
|
||||
2. **Python 3.13 sandbox** — `socket.socketpair()` fails under Docker's default
|
||||
seccomp profile; the agent-server needs `privileged: true`.
|
||||
3. **Host reachability** — the sandbox can't resolve `localhost` to the OmniRoute
|
||||
host; needs `host.docker.internal:host-gateway`.
|
||||
4. **Lost state** — conversations die with the container unless
|
||||
`OH_PERSISTENCE_DIR` is a host volume.
|
||||
5. **CORS** — the dashboard origin can't reach agent-server unless
|
||||
`PERMITTED_CORS_ORIGINS` allows it.
|
||||
|
||||
This plugin encodes all of that into one command.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install -g @omniroute/openhands-plugin
|
||||
# or: npx @omniroute/openhands-plugin ...
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
Generate the OpenHands `.env`:
|
||||
|
||||
```bash
|
||||
omniroute-openhands env \
|
||||
--api-key sk-... \
|
||||
--model deepseek-chat \
|
||||
--url http://192.168.3.106:20128
|
||||
```
|
||||
|
||||
Generate a `docker-compose.yml` service:
|
||||
|
||||
```bash
|
||||
omniroute-openhands compose \
|
||||
--api-key sk-... \
|
||||
--model glm-5.2 \
|
||||
--persistence-dir /Users/me/.openhands-state \
|
||||
--cors-origins http://100.73.44.17:3000
|
||||
```
|
||||
|
||||
Or a plain `docker run`:
|
||||
|
||||
```bash
|
||||
omniroute-openhands docker-run \
|
||||
--api-key sk-... \
|
||||
--model vivanta-core \
|
||||
--persistence-dir /Users/me/.openhands-state
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `env` | Print OpenHands `.env` contents |
|
||||
| `compose` | Print a Docker Compose service block |
|
||||
| `docker-run` | Print a full `docker run` command |
|
||||
| `models` | Print the default OpenHands → OmniRoute model map |
|
||||
|
||||
### Common options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--api-key` | OmniRoute API key (`sk-...`) | — |
|
||||
| `--model` | OpenHands model name or OmniRoute combo | — |
|
||||
| `--url` | OmniRoute base URL | `http://localhost:20128` |
|
||||
| `--persistence-dir` | Host dir for conversation state | `.openhands-state` |
|
||||
| `--cors-origins` | Comma-separated allowed origins | `localhost:3000,3001` |
|
||||
| `--sandbox-image` | OpenHands sandbox base image | — |
|
||||
|
||||
## Model mapping
|
||||
|
||||
OpenHands-friendly names are mapped to OmniRoute IDs/combo names:
|
||||
|
||||
| OpenHands sends | OmniRoute resolves to |
|
||||
|-----------------|----------------------|
|
||||
| `deepseek-chat` | `ds/deepseek-v4-flash` |
|
||||
| `deepseek-reasoner` | `ds/deepseek-v4-pro` |
|
||||
| `glm-5.2` | `nvidia/z-ai/glm-5.2` |
|
||||
| `gpt-4o` | `openai/gpt-4o` |
|
||||
| `claude-sonnet-4.5` | `anthropic/claude-sonnet-4.5` |
|
||||
| ... | ... |
|
||||
|
||||
Or just pass an OmniRoute combo name (e.g. `--model vivanta-core`) — the Model
|
||||
Alias Resolver and combo router accept it directly.
|
||||
|
||||
## Library usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildOpenHandsEnv,
|
||||
serializeOpenHandsEnv,
|
||||
buildOpenHandsCompose,
|
||||
resolveOpenHandsModel,
|
||||
} from "@omniroute/openhands-plugin";
|
||||
|
||||
const env = buildOpenHandsEnv({
|
||||
apiKey: "sk-...",
|
||||
model: resolveOpenHandsModel("deepseek-chat"),
|
||||
omnirouteUrl: "http://localhost:20128",
|
||||
persistenceDir: "/Users/me/.openhands-state",
|
||||
});
|
||||
console.log(serializeOpenHandsEnv(env));
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT — same as OmniRoute.
|
||||
2033
@omniroute/openhands-plugin/package-lock.json
generated
2033
@omniroute/openhands-plugin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"name": "@omniroute/openhands-plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenHands integration for the OmniRoute AI Gateway. Generates OpenHands env + Docker Compose config (model mapping, sandbox, CORS, persistence) so OpenHands agents talk to OmniRoute out of the box.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"omniroute-openhands": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./env": {
|
||||
"types": "./dist/env.d.ts",
|
||||
"import": "./dist/env.js"
|
||||
},
|
||||
"./docker": {
|
||||
"types": "./dist/docker.d.ts",
|
||||
"import": "./dist/docker.js"
|
||||
},
|
||||
"./model-map": {
|
||||
"types": "./dist/model-map.d.ts",
|
||||
"import": "./dist/model-map.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/env.test.ts tests/model-map.test.ts tests/docker.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
"omniroute",
|
||||
"openhands",
|
||||
"open-hands",
|
||||
"openhands-plugin",
|
||||
"openai-compatible",
|
||||
"docker",
|
||||
"agent"
|
||||
],
|
||||
"author": "OmniRoute contributors",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute.git",
|
||||
"directory": "@omniroute/openhands-plugin"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
|
||||
},
|
||||
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/openhands-plugin#readme",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@omniroute/open-sse": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@omniroute/open-sse": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.19.19",
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.22.3"
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @omniroute/openhands-plugin CLI — generate OpenHands .env / Docker config
|
||||
* for a running OmniRoute instance.
|
||||
*
|
||||
* Usage:
|
||||
* omniroute-openhands env --api-key sk-... --model deepseek-chat [--url http://localhost:20128]
|
||||
* omniroute-openhands compose --api-key sk-... --model deepseek-chat [--persistence-dir /path]
|
||||
* omniroute-openhands docker-run --api-key sk-... --model deepseek-chat
|
||||
* omniroute-openhands models (print the default model map)
|
||||
*/
|
||||
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
|
||||
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
|
||||
import { DEFAULT_OPENHANDS_MODEL_MAP } from "./model-map.ts";
|
||||
|
||||
function parseArgs(argv: string[]): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith("--")) continue;
|
||||
const key = arg.slice(2);
|
||||
const next = argv[i + 1];
|
||||
if (next !== undefined && !next.startsWith("--")) {
|
||||
out[key] = next;
|
||||
i++;
|
||||
} else {
|
||||
out[key] = "true";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function requireArgs(args: Record<string, string>, names: string[]): void {
|
||||
for (const name of names) {
|
||||
if (!args[name]) {
|
||||
console.error(`Missing required --${name}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [cmd, ...rest] = process.argv.slice(2);
|
||||
const args = parseArgs(rest);
|
||||
|
||||
switch (cmd) {
|
||||
case "env": {
|
||||
requireArgs(args, ["api-key", "model"]);
|
||||
const env = buildOpenHandsEnv({
|
||||
apiKey: args["api-key"],
|
||||
model: args.model,
|
||||
omnirouteUrl: args.url,
|
||||
persistenceDir: args["persistence-dir"],
|
||||
corsOrigins: args["cors-origins"]?.split(","),
|
||||
});
|
||||
process.stdout.write(serializeOpenHandsEnv(env));
|
||||
break;
|
||||
}
|
||||
case "compose": {
|
||||
requireArgs(args, ["api-key", "model"]);
|
||||
process.stdout.write(
|
||||
buildOpenHandsCompose({
|
||||
apiKey: args["api-key"],
|
||||
model: args.model,
|
||||
omnirouteUrl: args.url,
|
||||
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
|
||||
corsOrigins: args["cors-origins"]?.split(","),
|
||||
sandboxBaseImage: args["sandbox-image"],
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "docker-run": {
|
||||
requireArgs(args, ["api-key", "model"]);
|
||||
process.stdout.write(
|
||||
buildOpenHandsDockerRun({
|
||||
apiKey: args["api-key"],
|
||||
model: args.model,
|
||||
omnirouteUrl: args.url,
|
||||
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
|
||||
corsOrigins: args["cors-origins"]?.split(","),
|
||||
sandboxBaseImage: args["sandbox-image"],
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "models": {
|
||||
for (const [name, target] of Object.entries(DEFAULT_OPENHANDS_MODEL_MAP)) {
|
||||
process.stdout.write(`${name}\t->\t${target}\n`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(
|
||||
"Usage: omniroute-openhands <env|compose|docker-run|models> [options]\n" +
|
||||
"Options:\n" +
|
||||
" --api-key <sk-...> OmniRoute API key (required for env/compose/docker-run)\n" +
|
||||
" --model <name> OpenHands model name or OmniRoute combo\n" +
|
||||
" --url <base> OmniRoute URL (default http://localhost:20128)\n" +
|
||||
" --persistence-dir <path> Host dir for conversation state\n" +
|
||||
" --cors-origins <a,b,...> Allowed CORS origins\n" +
|
||||
" --sandbox-image <image> OpenHands sandbox base image"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* OpenHands agent-server Docker Compose generator for OmniRoute.
|
||||
*
|
||||
* Bakes in the integration fixes that were needed to run OpenHands against
|
||||
* OmniRoute reliably:
|
||||
* - `privileged: true` — Python 3.13 socket.socketpair() needs it under
|
||||
* Docker's default seccomp profile
|
||||
* - `extra_hosts` — host.docker.internal → host-gateway so the
|
||||
* sandbox can reach OmniRoute on the host
|
||||
* - host volume for OH_PERSISTENCE_DIR so conversations survive `docker rm`
|
||||
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to hit agent-server
|
||||
*/
|
||||
|
||||
export interface OpenHandsDockerOptions {
|
||||
/** Agent-server image (default: the official OpenHands runtime image). */
|
||||
image?: string;
|
||||
/** Container name (default: openhands-agent). */
|
||||
containerName?: string;
|
||||
/** Model name to pass via LLM_MODEL. */
|
||||
model: string;
|
||||
/** OmniRoute API key. */
|
||||
apiKey: string;
|
||||
/** OmniRoute base URL reachable from the sandbox (default http://localhost:20128). */
|
||||
omnirouteUrl?: string;
|
||||
/** Host directory for OH_PERSISTENCE_DIR (must match env.ts persistenceDir). */
|
||||
persistenceDir: string;
|
||||
/** CORS origins to permit. */
|
||||
corsOrigins?: string[];
|
||||
/** Sandbox base image (defaults to OpenHands default). */
|
||||
sandboxBaseImage?: string;
|
||||
/** Set true to use host networking instead of extra_hosts. */
|
||||
hostNetwork?: boolean;
|
||||
}
|
||||
|
||||
export function buildOpenHandsCompose(opts: OpenHandsDockerOptions): string {
|
||||
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
|
||||
const containerName = opts.containerName ?? "openhands-agent";
|
||||
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
|
||||
const cors =
|
||||
opts.corsOrigins && opts.corsOrigins.length > 0
|
||||
? opts.corsOrigins
|
||||
: ["http://localhost:3000", "http://localhost:3001"];
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`services:`);
|
||||
lines.push(` openhands:`);
|
||||
lines.push(` image: ${image}`);
|
||||
lines.push(` container_name: ${containerName}`);
|
||||
lines.push(` privileged: true`);
|
||||
lines.push(` environment:`);
|
||||
lines.push(` LLM_MODEL: "${opts.model}"`);
|
||||
lines.push(` LLM_BASE_URL: "${omnirouteHost}/v1"`);
|
||||
lines.push(` LLM_API_KEY: "${opts.apiKey}"`);
|
||||
lines.push(` OH_PERSISTENCE_DIR: "/opt/.openhands-state"`);
|
||||
lines.push(` PERMITTED_CORS_ORIGINS: "${cors.join(",")}"`);
|
||||
if (opts.sandboxBaseImage) {
|
||||
lines.push(` SANDBOX_BASE_IMAGE: "${opts.sandboxBaseImage}"`);
|
||||
}
|
||||
lines.push(` volumes:`);
|
||||
lines.push(` - ${opts.persistenceDir}:/opt/.openhands-state`);
|
||||
lines.push(` extra_hosts:`);
|
||||
lines.push(` - "host.docker.internal:host-gateway"`);
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* docker run equivalent of {@link buildOpenHandsCompose} — returns the full
|
||||
* `docker run` command line.
|
||||
*/
|
||||
export function buildOpenHandsDockerRun(opts: OpenHandsDockerOptions): string {
|
||||
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
|
||||
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
|
||||
const cors =
|
||||
opts.corsOrigins && opts.corsOrigins.length > 0
|
||||
? opts.corsOrigins
|
||||
: ["http://localhost:3000", "http://localhost:3001"];
|
||||
|
||||
const parts = [
|
||||
"docker run",
|
||||
"--privileged",
|
||||
"--add-host host.docker.internal:host-gateway",
|
||||
`-e LLM_MODEL="${opts.model}"`,
|
||||
`-e LLM_BASE_URL="${omnirouteHost}/v1"`,
|
||||
`-e LLM_API_KEY="${opts.apiKey}"`,
|
||||
`-e OH_PERSISTENCE_DIR=/opt/.openhands-state`,
|
||||
`-e PERMITTED_CORS_ORIGINS="${cors.join(",")}"`,
|
||||
`-v "${opts.persistenceDir}:/opt/.openhands-state"`,
|
||||
image,
|
||||
];
|
||||
return parts.join(" ") + "\n";
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* OpenHands `.env` generator for the OmniRoute AI Gateway.
|
||||
*
|
||||
* Produces the OpenHands environment that points an OpenHands agent-server at
|
||||
* a running OmniRoute instance and fixes the integration gotchas found in the
|
||||
* field:
|
||||
* - LLM_MODEL — OpenHands-friendly model name → OmniRoute model/combo
|
||||
* - LLM_BASE_URL — OmniRoute OpenAI-compatible endpoint
|
||||
* - LLM_API_KEY — OmniRoute key (sk-...)
|
||||
* - OH_PERSISTENCE_DIR — host-mounted SQLite/conversation persistence
|
||||
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to reach agent-server
|
||||
*/
|
||||
|
||||
export interface OpenHandsEnvOptions {
|
||||
/** OmniRoute base URL as seen from the agent-server (default localhost:20128). */
|
||||
omnirouteUrl?: string;
|
||||
/** OmniRoute API key (sk-...). */
|
||||
apiKey: string;
|
||||
/** OpenHands model name (e.g. "deepseek-chat") or OmniRoute combo/model. */
|
||||
model: string;
|
||||
/** Host directory for OH_PERSISTENCE_DIR (default: current dir + .openhands-state). */
|
||||
persistenceDir?: string;
|
||||
/** CORS origins that must reach the agent-server (default dashboard origin + localhost). */
|
||||
corsOrigins?: string[];
|
||||
/** Optional OpenHands sandbox base image. */
|
||||
sandboxBaseImage?: string;
|
||||
}
|
||||
|
||||
export function buildOpenHandsEnv(opts: OpenHandsEnvOptions): Record<string, string> {
|
||||
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
|
||||
const persistence = opts.persistenceDir ?? `${process.cwd()}/.openhands-state`;
|
||||
const cors =
|
||||
opts.corsOrigins && opts.corsOrigins.length > 0
|
||||
? opts.corsOrigins
|
||||
: ["http://localhost:3000", "http://localhost:3001"];
|
||||
|
||||
const env: Record<string, string> = {
|
||||
LLM_MODEL: opts.model,
|
||||
LLM_BASE_URL: `${omnirouteHost}/v1`,
|
||||
LLM_API_KEY: opts.apiKey,
|
||||
OH_PERSISTENCE_DIR: persistence,
|
||||
PERMITTED_CORS_ORIGINS: cors.join(","),
|
||||
};
|
||||
|
||||
if (opts.sandboxBaseImage) {
|
||||
env.SANDBOX_BASE_IMAGE = opts.sandboxBaseImage;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the env record to `.env` file content (KEY=VALUE lines).
|
||||
* Values are not quoted unless they contain whitespace or `#`.
|
||||
*/
|
||||
export function serializeOpenHandsEnv(env: Record<string, string>): string {
|
||||
const lines: string[] = [];
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
const needsQuotes = /[\s#]/.test(value);
|
||||
lines.push(needsQuotes ? `${key}="${value}"` : `${key}=${value}`);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @omniroute/openhands-plugin — OpenHands integration for the OmniRoute AI Gateway.
|
||||
*
|
||||
* Generates the OpenHands environment and Docker Compose / docker run config
|
||||
* that wires an OpenHands agent-server to a running OmniRoute instance:
|
||||
* model mapping, sandbox privileges, host-gateway networking, persistent
|
||||
* conversation state and CORS.
|
||||
*/
|
||||
export { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
|
||||
export type { OpenHandsEnvOptions } from "./env.ts";
|
||||
export { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
|
||||
export type { OpenHandsDockerOptions } from "./docker.ts";
|
||||
export {
|
||||
DEFAULT_OPENHANDS_MODEL_MAP,
|
||||
resolveOpenHandsModel,
|
||||
buildOpenHandsModel,
|
||||
} from "./model-map.ts";
|
||||
export type { OpenHandsModelMap } from "./model-map.ts";
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* OpenHands → OmniRoute model mapping.
|
||||
*
|
||||
* OpenHands sends `model: "<LLM_MODEL>"` and expects the OpenAI-compatible
|
||||
* endpoint to accept that exact string. OmniRoute uses provider-prefixed
|
||||
* model IDs (`ds/deepseek-v4-flash`) and combo names. This module maps
|
||||
* common OpenHands-friendly names to the OmniRoute model/combo they should
|
||||
* resolve to, and back-fills the `LLM_MODEL` value for OpenHands.
|
||||
*/
|
||||
|
||||
export interface OpenHandsModelMap {
|
||||
/** OpenHands-friendly model name (e.g. "deepseek-chat") */
|
||||
[openHandsName: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default mapping for the model names OpenHands and the broader ecosystem
|
||||
* commonly send. Values are OmniRoute model IDs or combo names. Extend or
|
||||
* override via {@link resolveOpenHandsModel}.
|
||||
*/
|
||||
export const DEFAULT_OPENHANDS_MODEL_MAP: OpenHandsModelMap = Object.freeze({
|
||||
// DeepSeek
|
||||
"deepseek-chat": "ds/deepseek-v4-flash",
|
||||
"deepseek-reasoner": "ds/deepseek-v4-pro",
|
||||
// Claude / Anthropic
|
||||
"claude-sonnet-4.5": "anthropic/claude-sonnet-4.5",
|
||||
"claude-opus-4.1": "anthropic/claude-opus-4.1",
|
||||
"claude-haiku-4.5": "anthropic/claude-haiku-4.5",
|
||||
// GPT / OpenAI
|
||||
"gpt-4o": "openai/gpt-4o",
|
||||
"gpt-4o-mini": "openai/gpt-4o-mini",
|
||||
"gpt-5": "openai/gpt-5",
|
||||
// Gemini
|
||||
"gemini-2.5-flash": "gemini/gemini-2.5-flash",
|
||||
"gemini-2.5-pro": "gemini/gemini-2.5-pro",
|
||||
// GLM / Z.AI (NVIDIA NIM free endpoint)
|
||||
"glm-5.2": "nvidia/z-ai/glm-5.2",
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve the OmniRoute model ID for an OpenHands-friendly model name.
|
||||
* Returns the input unchanged when no mapping exists (OmniRoute will try to
|
||||
* resolve it as a literal model/combo).
|
||||
*/
|
||||
export function resolveOpenHandsModel(
|
||||
openHandsModel: string,
|
||||
map: OpenHandsModelMap = DEFAULT_OPENHANDS_MODEL_MAP
|
||||
): string {
|
||||
if (!openHandsModel) return openHandsModel;
|
||||
const mapped = map[openHandsModel];
|
||||
return mapped ?? openHandsModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `LLM_MODEL` value for OpenHands from an OmniRoute model ID/combo.
|
||||
*
|
||||
* OpenHands only surfaces the literal `LLM_MODEL` string in its UI, so for
|
||||
* OmniRoute combos (e.g. "vivanta-core") that's already the right value.
|
||||
* For provider-prefixed IDs, we return them as-is — the OmniRoute Model
|
||||
* Alias Resolver accepts both the raw ID and aliases on the `/v1` endpoint.
|
||||
*/
|
||||
export function buildOpenHandsModel(omnirouteModelOrCombo: string): string {
|
||||
return omnirouteModelOrCombo;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "../src/env.ts";
|
||||
import { resolveOpenHandsModel, buildOpenHandsModel } from "../src/model-map.ts";
|
||||
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "../src/docker.ts";
|
||||
|
||||
test("buildOpenHandsEnv produces LLM vars pointing at OmniRoute", () => {
|
||||
const env = buildOpenHandsEnv({
|
||||
apiKey: "sk-test-123",
|
||||
model: "deepseek-chat",
|
||||
omnirouteUrl: "http://192.168.3.106:20128",
|
||||
persistenceDir: "/opt/state",
|
||||
corsOrigins: ["http://100.73.44.17:3000"],
|
||||
});
|
||||
assert.equal(env.LLM_MODEL, "deepseek-chat");
|
||||
assert.equal(env.LLM_BASE_URL, "http://192.168.3.106:20128/v1");
|
||||
assert.equal(env.LLM_API_KEY, "sk-test-123");
|
||||
assert.equal(env.OH_PERSISTENCE_DIR, "/opt/state");
|
||||
assert.equal(env.PERMITTED_CORS_ORIGINS, "http://100.73.44.17:3000");
|
||||
});
|
||||
|
||||
test("serializeOpenHandsEnv quotes values with whitespace/#", () => {
|
||||
const out = serializeOpenHandsEnv({ LLM_MODEL: "deepseek-chat", LLM_BASE_URL: "http://localhost:20128/v1" });
|
||||
const lines = out.trim().split("\n");
|
||||
assert.ok(lines.some((l) => l.startsWith("LLM_MODEL=deepseek-chat")));
|
||||
assert.ok(lines.some((l) => l.startsWith("LLM_BASE_URL=http://localhost:20128/v1")));
|
||||
});
|
||||
|
||||
test("resolveOpenHandsModel maps known names to OmniRoute IDs", () => {
|
||||
assert.equal(resolveOpenHandsModel("deepseek-chat"), "ds/deepseek-v4-flash");
|
||||
assert.equal(resolveOpenHandsModel("glm-5.2"), "nvidia/z-ai/glm-5.2");
|
||||
assert.equal(resolveOpenHandsModel("gpt-4o"), "openai/gpt-4o");
|
||||
});
|
||||
|
||||
test("resolveOpenHandsModel passes unknown names through unchanged", () => {
|
||||
assert.equal(resolveOpenHandsModel("vivanta-core"), "vivanta-core");
|
||||
assert.equal(resolveOpenHandsModel(""), "");
|
||||
});
|
||||
|
||||
test("resolveOpenHandsModel accepts custom map overrides", () => {
|
||||
const custom = { "my-alias": "nvidia/z-ai/glm-5.2" };
|
||||
assert.equal(resolveOpenHandsModel("my-alias", custom), "nvidia/z-ai/glm-5.2");
|
||||
assert.equal(resolveOpenHandsModel("deepseek-chat", custom), "deepseek-chat");
|
||||
});
|
||||
|
||||
test("buildOpenHandsModel passes combo names through", () => {
|
||||
assert.equal(buildOpenHandsModel("vivanta-core"), "vivanta-core");
|
||||
assert.equal(buildOpenHandsModel("ds/deepseek-v4-flash"), "ds/deepseek-v4-flash");
|
||||
});
|
||||
|
||||
test("buildOpenHandsCompose includes privileged, extra_hosts, volume, CORS", () => {
|
||||
const compose = buildOpenHandsCompose({
|
||||
apiKey: "sk-x",
|
||||
model: "deepseek-chat",
|
||||
persistenceDir: "/Users/me/.openhands-state",
|
||||
corsOrigins: ["http://localhost:3000"],
|
||||
});
|
||||
assert.ok(compose.includes("privileged: true"), "privileged present");
|
||||
assert.ok(compose.includes("host.docker.internal:host-gateway"), "host-gateway present");
|
||||
assert.ok(compose.includes("/Users/me/.openhands-state"), "persistence volume present");
|
||||
assert.ok(compose.includes("LLM_BASE_URL: \"http://localhost:20128/v1\""), "base url present");
|
||||
assert.ok(compose.includes("PERMITTED_CORS_ORIGINS: \"http://localhost:3000\""), "cors present");
|
||||
});
|
||||
|
||||
test("buildOpenHandsDockerRun produces a runnable docker command", () => {
|
||||
const run = buildOpenHandsDockerRun({
|
||||
apiKey: "sk-x",
|
||||
model: "glm-5.2",
|
||||
persistenceDir: "/opt/state",
|
||||
});
|
||||
assert.ok(run.startsWith("docker run"));
|
||||
assert.ok(run.includes("--privileged"));
|
||||
assert.ok(run.includes("--add-host host.docker.internal:host-gateway"));
|
||||
assert.ok(run.includes("LLM_MODEL=\"glm-5.2\""));
|
||||
assert.ok(run.includes("LLM_BASE_URL=\"http://localhost:20128/v1\""));
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"ignoreDeprecations": "6.0",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"declaration": true,
|
||||
"isolatedModules": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules", "tests"]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts", "src/cli.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
splitting: false,
|
||||
treeshake: false,
|
||||
target: "node18",
|
||||
outDir: "dist",
|
||||
minify: false,
|
||||
cjsInterop: false,
|
||||
});
|
||||
982
CHANGELOG.md
982
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
18
CLAUDE.md
18
CLAUDE.md
@@ -45,7 +45,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
|
||||
| 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 (130 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (95 files, 110 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
@@ -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`): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 13-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
|
||||
---
|
||||
|
||||
@@ -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 (13-factor scoring, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
|
||||
| Auto-Combo (12-factor scoring, 18 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` |
|
||||
@@ -461,18 +461,10 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
|
||||
git fetch origin "$BASE_BRANCH"
|
||||
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
|
||||
cd ".claude/worktrees/${TASK##*/}"
|
||||
# Reuse the main checkout's node_modules to skip a per-worktree npm install.
|
||||
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
|
||||
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
|
||||
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
|
||||
# symlink node_modules from the main checkout to skip a per-worktree npm install:
|
||||
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
|
||||
```
|
||||
|
||||
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
|
||||
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
|
||||
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
|
||||
runners all keep passing — the error names "filesystem root", not the worktree, so it
|
||||
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
|
||||
|
||||
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
|
||||
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
|
||||
with its `path`.
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
Thank you for your interest in contributing! This guide covers everything you need to get started.
|
||||
|
||||
For the official per-change workflow, start with the
|
||||
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
|
||||
UI/UX, i18n, CLI, database, and build/deploy changes to their contracts, focused tests, CI
|
||||
coverage, and reconciliation steps.
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
@@ -203,11 +198,10 @@ Coverage notes:
|
||||
|
||||
### Pull Request Requirements
|
||||
|
||||
Before opening a PR, use the
|
||||
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
|
||||
what you changed. The full unit suite (4 CI shards), Vitest, the **60%+** coverage gate, and
|
||||
the production build are CI's responsibility — running them locally adds no signal the PR
|
||||
checks will not already give you, and on smaller machines it can saturate the host (#8084):
|
||||
Before opening a PR, run the focused loop for what you changed. The full unit suite
|
||||
(4 CI shards), Vitest, the **60%+** coverage gate, and the production build are CI's
|
||||
responsibility — running them locally adds no signal the PR checks will not already
|
||||
give you, and on smaller machines it can saturate the host (#8084):
|
||||
|
||||
- Run the test files that cover your change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
|
||||
- Run `npm run lint`
|
||||
@@ -277,7 +271,7 @@ src/ # TypeScript (.ts / .tsx)
|
||||
│ ├── a2a/ # Agent-to-Agent v0.3 protocol server
|
||||
│ ├── acp/ # Agent Communication Protocol registry
|
||||
│ ├── compliance/ # Compliance policy engine
|
||||
│ ├── db/ # SQLite domain modules + 130 migrations
|
||||
│ ├── db/ # SQLite database layer (21 modules + 16 migrations)
|
||||
│ ├── memory/ # Persistent conversational memory
|
||||
│ ├── oauth/ # OAuth providers, services, and utilities
|
||||
│ ├── skills/ # Extensible skill framework
|
||||
@@ -287,7 +281,7 @@ src/ # TypeScript (.ts / .tsx)
|
||||
├── mitm/ # MITM proxy (cert, DNS, target routing)
|
||||
├── shared/
|
||||
│ ├── components/ # React components (.tsx)
|
||||
│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies
|
||||
│ ├── constants/ # Provider definitions (177), MCP scopes, 14 routing strategies
|
||||
│ ├── utils/ # Circuit breaker, sanitizer, auth helpers
|
||||
│ └── validation/ # Zod v4 schemas
|
||||
└── sse/ # SSE proxy pipeline
|
||||
@@ -295,7 +289,7 @@ src/ # TypeScript (.ts / .tsx)
|
||||
open-sse/ # @omniroute/open-sse workspace
|
||||
├── executors/ # 14 provider-specific request executors
|
||||
├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.)
|
||||
├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes)
|
||||
├── mcp-server/ # MCP server (25 tools, 3 transports, 10 scopes)
|
||||
├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.)
|
||||
├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama)
|
||||
├── transformer/ # Responses API transformer
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# ── Common base with runtime deps ──────────────────────────────────────────
|
||||
FROM node:26-trixie-slim AS base
|
||||
FROM node:24-trixie-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
# `apt-get upgrade` pulls the security-patched versions of the Debian (trixie)
|
||||
@@ -8,8 +8,8 @@ WORKDIR /app
|
||||
# that already have a fix published in trixie. CVEs without an upstream fix yet
|
||||
# (local-only TOCTOU, etc.) remain until the distro patches them and the image
|
||||
# is rebuilt; none are reachable from the proxy's request surface at runtime.
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
|
||||
apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
|
||||
@@ -29,8 +29,8 @@ FROM base AS builder
|
||||
|
||||
# Build tools for native module compilation
|
||||
# apt-get update needed here because base's rm -rf clears the shared cache
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -95,11 +95,6 @@ RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
|
||||
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
|
||||
ENV OMNIROUTE_USE_TURBOPACK=1
|
||||
|
||||
# Next.js basePath is fixed at build time; pass OMNIROUTE_BASE_PATH here when the
|
||||
# image should serve under a reverse-proxy subpath without a runtime patch.
|
||||
ARG OMNIROUTE_BASE_PATH=""
|
||||
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
||||
|
||||
# Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert
|
||||
# access), so keep @/mitm/manager on the graceful stub (#3390). This flag is
|
||||
# Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344).
|
||||
@@ -236,11 +231,6 @@ FROM runner-base AS runner-cli
|
||||
# runner-base runs.
|
||||
USER root
|
||||
|
||||
# The CLI image can use the internal ChatGPT Web (Codex) Chromium sidecar over
|
||||
# CDP without installing a second browser in this container.
|
||||
COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core
|
||||
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
|
||||
|
||||
# Install system dependencies required by openclaw (git+ssh references).
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
|
||||
159
README.md
159
README.md
@@ -67,46 +67,32 @@
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="right"><b>🚀 Start</b></td>
|
||||
<td align="center"><a href="#-quick-start">🚀 Quick Start</a></td>
|
||||
<td align="center"><a href="#-more-install-methods--docker-source-pnpm-arch">📦 Install</a></td>
|
||||
<td align="center"><a href="#-works-the-second-you-install-it--no-keys-no-config">🆓 Zero-config</a></td>
|
||||
<td align="center"><a href="#-quick-start"><b>🚀 Quick Start</b></a></td>
|
||||
<td align="center"><a href="#-combos--the-flagship"><b>🎯 Combos</b></a></td>
|
||||
<td align="center"><a href="#-290-ai-providers--90-free"><b>🌐 Providers</b></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><b>💡 Learn</b></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp"><b>🔌 CLI & MCP</b></a></td>
|
||||
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically"><b>🗜️ Compression</b></a></td>
|
||||
<td align="center"><a href="https://omniroute.online"><b>🌍 Website</b></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="#-the-promise">💥 The Promise</a></td>
|
||||
<td align="center"><a href="#-why-omniroute">🤔 Why OmniRoute</a></td>
|
||||
<td align="center"><a href="#-why-omniroute">🤔 Why</a></td>
|
||||
<td align="center"><a href="#-what-sets-omniroute-apart">🏆 What Sets Apart</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><b>⚙️ Features</b></td>
|
||||
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
|
||||
<td align="center"><a href="#-290-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI & MCP</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"></td>
|
||||
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically">🗜️ Compression</a></td>
|
||||
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
|
||||
<td align="center"><a href="#%EF%B8%8F-where-omniroute-runs--anywhere">🖥️ Where It Runs</a></td>
|
||||
<td align="center"><a href="#-private--local-first">🔒 Private</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><b>👀 See it</b></td>
|
||||
<td align="center"><a href="#-omniroute-in-action">🎬 In Action</a></td>
|
||||
<td align="center"><a href="#-whats-new">✨ What's New</a></td>
|
||||
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><b>💚 Support</b></td>
|
||||
<td align="center"><a href="#-support-omniroute">💚 Support / Donate</a></td>
|
||||
<td align="center"><a href="#-community--help">💬 Community</a></td>
|
||||
<td align="center"><a href="#-sponsors">💖 Sponsors</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right"><b>📦 Project</b></td>
|
||||
<td align="center"><a href="#%EF%B8%8F-tech-stack">🛠️ Tech Stack</a></td>
|
||||
<td align="center"><a href="#-documentation">📖 Docs</a></td>
|
||||
<td align="center"><a href="#-500-contributors">👥 Contributors</a></td>
|
||||
<td align="center"><a href="#-dashboard-screenshots">📸 Screenshots</a></td>
|
||||
<td align="center"><a href="#-support--community">📧 Support</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -241,53 +227,12 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
<b>What Kimi's support powers:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="150">
|
||||
<a href="https://cheaperinference.com/?utm_source=omniroute">
|
||||
<img src="public/providers/cheaperinference.svg" width="64" alt="Cheaper Inference"/>
|
||||
</a>
|
||||
<br/><b>Cheaper Inference</b><br/><sub>cheaperinference.com</sub><br/><br/>
|
||||
<img src="https://img.shields.io/badge/Open_Source_Friend-31f889?style=flat-square&labelColor=04170d" alt="Open Source Friend"/>
|
||||
</td>
|
||||
<td>
|
||||
Thanks to <b>Cheaper Inference</b>, an OmniRoute Open Source Friend, for backing this project! Cheaper Inference is a cost-ranked gateway that resells 42 frontier models — Claude, GPT-5.x, Gemini, Kimi K3, GLM, DeepSeek, Grok and MiniMax — behind one OpenAI-compatible endpoint, routing each request to the cheapest eligible provider without ever charging above the model maker's list price.
|
||||
<br/><br/>
|
||||
<b>First-class support in OmniRoute:</b> Chat Completions, the native <code>/v1/responses</code> endpoint, vision, tool calling and 3 image models (<code>grok-imagine</code>, <code>nano-banana-pro</code>, <code>nano-banana-2</code>, reachable as <code>cheaperinference/<model></code>). <a href="https://cheaperinference.com/?utm_source=omniroute"><b>Get an API key →</b></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Links tagged <code>aff=omniroute</code> are partner links. They fund the project at no extra cost to you.</sub>
|
||||
|
||||
<br/>
|
||||
|
||||
<details open>
|
||||
<summary><sub><b>🎟️ Affiliates Promo</b> — free signup coupons from providers we don't sponsor (click to expand)</sub></summary>
|
||||
|
||||
<sub><i>This section is for referral/coupon codes only. Sponsored partnerships live in <b>🤝 Supported by our Open Source Friends</b> above. OmniRoute has no sponsorship or partnership with the providers listed here — these are public coupons anyone can use.</i></sub>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="120">
|
||||
<a href="https://agentrouter.org/register?aff=70LM">
|
||||
<img src="public/providers/agentrouter.png" width="32" alt="AgentRouter"/>
|
||||
</a>
|
||||
<br/><sub><b>AgentRouter</b></sub><br/><sub>agentrouter.org</sub>
|
||||
</td>
|
||||
<td>
|
||||
<sub><b><a href="https://agentrouter.org/register?aff=70LM">AgentRouter</a></b> — affiliate signup · <b>$100 free credits</b> on signup (free server, expect higher latency — best for testing, not production). First-class support in OmniRoute since <b>v3.8.50</b>: Chat Completions, the Anthropic-compatible wire format and the OpenAI-compatible path. Available models include <code>claude-opus-4-8</code>, <code>claude-opus-5</code>, <code>gpt-5.6-sol</code> and more. <b><a href="https://agentrouter.org/register?aff=70LM">Grab your $100 →</a></b></sub>
|
||||
<br/><br/>
|
||||
<sub>⚠️ <i>Affiliate link — OmniRoute has no sponsorship or partnership with this provider.</i></sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Know another provider with a generous free signup coupon that benefits OmniRoute users? Open an issue and we'll add it here.</sub>
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🎯 Combos — The Flagship
|
||||
@@ -445,50 +390,13 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
<br/>
|
||||
|
||||
## 💚 Support OmniRoute
|
||||
## ❤️ Support
|
||||
|
||||
OmniRoute is MIT-licensed and maintained in the open. If it saves you time or money, here's how to keep it independent — pick whatever fits you. Sponsorship never affects routing priority; it buys visibility, not ranking.
|
||||
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:
|
||||
|
||||
<table>
|
||||
<tr><td nowrap>⭐ <b>Star the repo</b></td><td>Free — genuinely helps visibility</td><td><a href="https://github.com/diegosouzapw/OmniRoute">Star OmniRoute</a></td></tr>
|
||||
<tr><td nowrap>🐙 <b>GitHub Sponsors</b></td><td>One-off or monthly · zero platform fee</td><td><a href="https://github.com/sponsors/diegosouzapw">github.com/sponsors/diegosouzapw</a></td></tr>
|
||||
<tr><td nowrap>🏢 <b>Open Collective</b></td><td><b>Companies</b> — issues an invoice/receipt · transparent books</td><td><a href="https://opencollective.com/omniroute">opencollective.com/omniroute</a></td></tr>
|
||||
<tr><td nowrap>☕ <b>Ko-fi</b></td><td>Quick one-off tip, no signup for the donor</td><td><a href="https://ko-fi.com/diegosouzapw">ko-fi.com/diegosouzapw</a></td></tr>
|
||||
<tr><td nowrap>🧋 <b>Buy Me a Coffee</b></td><td>Small, informal gesture</td><td><a href="https://www.buymeacoffee.com/diegosouzapw">buymeacoffee.com/diegosouzapw</a></td></tr>
|
||||
<tr><td nowrap>🖐 <b>Liberapay</b></td><td>Recurring · non-profit · open source</td><td><a href="https://liberapay.com/diegosouzapw">liberapay.com/diegosouzapw</a></td></tr>
|
||||
<tr><td nowrap>🇧🇷 <b>PIX</b> (Brazil)</td><td>Instant, no fees</td><td>key & QR below</td></tr>
|
||||
<tr><td nowrap>₿ <b>Crypto</b></td><td>BTC · ETH · USDT-TRC20 · USDC-Solana</td><td>addresses below</td></tr>
|
||||
</table>
|
||||
|
||||
**🇧🇷 PIX** — instant, no fees (Brazil)
|
||||
|
||||
<img src="docs/assets/pix-qr.png" width="140" align="right" alt="OmniRoute PIX QR code"/>
|
||||
|
||||
Key (random): `5d865059-bc44-483a-962d-43ceb80126eb`
|
||||
|
||||
Pix copia-e-cola:
|
||||
|
||||
```
|
||||
00020101021126580014br.gov.bcb.pix01365d865059-bc44-483a-962d-43ceb80126eb5204000053039865802BR5922OMNIROUTE CONTRIBUICAO6006BRASIL62070503***630475DD
|
||||
```
|
||||
|
||||
<br clear="right"/>
|
||||
|
||||
<details>
|
||||
<summary><b>₿ Crypto</b> — BTC · ETH · USDT-TRC20 · USDC-Solana (click to expand)</summary>
|
||||
|
||||
<table>
|
||||
<tr><td nowrap><b>₿ BTC</b></td><td nowrap>Bitcoin (SegWit)</td><td><code>bc1qh00smz004sy85wyl28v77tenkt3ckl6eaep7fd</code></td></tr>
|
||||
<tr><td nowrap><b>Ξ ETH</b></td><td nowrap>Ethereum (ERC20)</td><td><code>0x64Cf6B68A6Ff34288e89172950a2d00102337a84</code></td></tr>
|
||||
<tr><td nowrap><b>₮ USDT</b></td><td nowrap>Tron (TRC20)</td><td><code>TKAF41JpuQrHbKTnsQa9svJE2T192Hvsc2</code></td></tr>
|
||||
<tr><td nowrap><b>$ USDC</b></td><td nowrap>Solana</td><td><code>2emNNZzVVWQc3FQ2wk9M6qXUQmW8AKdjjL174fXR28Tu</code></td></tr>
|
||||
</table>
|
||||
|
||||
<sub>⚠️ Send each coin only on the network shown — sending on the wrong network can lose the funds.</sub>
|
||||
|
||||
</details>
|
||||
|
||||
🐛 Found a bug or have feedback? Open a [Discussion](https://github.com/diegosouzapw/OmniRoute/discussions).
|
||||
- ⭐ **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)
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -510,12 +418,11 @@ Pix copia-e-cola:
|
||||
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture CLIs that ignore proxy env vars, with a per-SNI CA + trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
|
||||
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint, cache-HIT savings header, per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite), opt-in credential-masking guardrail (redacts leaked API keys/secrets in both directions), free DuckDuckGo last-resort web search, and an optional OIDC login gate for the dashboard (password login always stays available). → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite) + free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Google Imagen, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **290-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI), Ollama first-class card, Claude Sonnet 5, Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **290-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
|
||||
<br/>
|
||||
@@ -926,22 +833,18 @@ devbox run npm run dev
|
||||
**🦭 Podman**
|
||||
|
||||
```bash
|
||||
# 1. Prepare the bind-mounted data directory
|
||||
mkdir -p data
|
||||
# 1. Build the image
|
||||
podman build --target runner-base -t omniroute:base .
|
||||
|
||||
# 2. Linux + local rootless Podman only (never a remote Podman Machine client):
|
||||
podman unshare chown 1000:1000 ./data
|
||||
# 2. Fix data directory permissions for rootless Podman
|
||||
mkdir -p data && podman unshare chown 1000:1000 ./data
|
||||
|
||||
# 3. Set the runtime hint, build the local Compose image, and start
|
||||
# 3. Set runtime in .env, then run (see contrib/podman/ for Quadlet)
|
||||
echo "CONTAINER_HOST=podman" >> .env
|
||||
podman compose --profile base up -d --build
|
||||
podman compose --profile base up -d
|
||||
```
|
||||
|
||||
On macOS or Windows, Podman uses a remote Podman Machine: skip `podman unshare` and
|
||||
follow the [topology-specific data directory guidance](contrib/podman/README.md#data-directory-permissions-by-topology).
|
||||
|
||||
📖 [Podman Guide](contrib/podman/README.md) — Compose builds, Podman Machine, and
|
||||
Linux/systemd Quadlet setup.
|
||||
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
|
||||
|
||||
**⚡ Faster / leaner install (skip the native build)**
|
||||
|
||||
@@ -997,7 +900,7 @@ same process on one port, so there is no separate CLI-only package today.
|
||||
|
||||
<div align="center">
|
||||
|
||||
# 📧 Community & Help
|
||||
# 📧 Support & Community
|
||||
|
||||
> Everything in one place — follow the maintainer, chat with the community, or open an issue.
|
||||
|
||||
@@ -1013,7 +916,7 @@ same process on one port, so there is no separate CLI-only package today.
|
||||
| 📦 **Source code** | [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) |
|
||||
| 🐛 **Report a bug** | [open an issue](https://github.com/diegosouzapw/OmniRoute/issues) — attach `npm run system-info` output |
|
||||
| 🤝 **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [Branching & Release Model](docs/ops/BRANCHING_MODEL.md) · pick a `good first issue` |
|
||||
| 💚 **Support the project** | [Ways to support ↑](#-support-omniroute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
|
||||
| ⭐ **Support the project** | [Star the repo](https://github.com/diegosouzapw/OmniRoute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Third-Party Notices
|
||||
|
||||
## codex-chatgpt-web
|
||||
|
||||
Parts of `open-sse/vendor/codex-chatgpt-web/` are adapted from
|
||||
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit
|
||||
`55592fca0ba19a27f1b769cec8fff61ff340a785`.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 codex-chatgpt-web contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, "..");
|
||||
|
||||
export function resolveChatGptWebCodexMcpEntry(rootDir = root, exists = existsSync) {
|
||||
const candidates = [
|
||||
join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"open-sse",
|
||||
"vendor",
|
||||
"codex-chatgpt-web",
|
||||
"adapters",
|
||||
"chatgpt-web",
|
||||
"mcp-server.js"
|
||||
),
|
||||
join(
|
||||
rootDir,
|
||||
"open-sse",
|
||||
"vendor",
|
||||
"codex-chatgpt-web",
|
||||
"adapters",
|
||||
"chatgpt-web",
|
||||
"mcp-server.ts"
|
||||
),
|
||||
];
|
||||
return candidates.find((candidate) => exists(candidate)) ?? null;
|
||||
}
|
||||
|
||||
export async function startChatGptWebCodexMcp(args = process.argv.slice(2), rootDir = root) {
|
||||
const socketIndex = args.indexOf("--broker-socket");
|
||||
const brokerSocketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
|
||||
if (!brokerSocketPath) throw new Error("--broker-socket is required");
|
||||
const entry = resolveChatGptWebCodexMcpEntry(rootDir);
|
||||
if (!entry) throw new Error("ChatGPT Web (Codex) MCP entrypoint was not found");
|
||||
if (entry.endsWith(".ts")) {
|
||||
const { register } = await import("node:module");
|
||||
register("tsx/esm", pathToFileURL(`${rootDir}/`));
|
||||
}
|
||||
const module = await import(pathToFileURL(entry).href);
|
||||
await module.runChatGptMcpServer({ brokerSocketPath });
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
startChatGptWebCodexMcp().catch((error) => {
|
||||
console.error(
|
||||
`ChatGPT Web (Codex) MCP konnte nicht gestartet werden: ${error?.message || error}`
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { quoteShellArgs } from "../utils/winShellArgs.mjs";
|
||||
|
||||
/** OpenAI/Codex env keys stripped from the child so a stale OpenAI key/base-url
|
||||
* in the shell can't shadow the omniroute provider (defense-in-depth). Mirrors
|
||||
@@ -30,23 +29,6 @@ export function resolveCodexSpawn(platform) {
|
||||
return { command: "codex", shell: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* `shell: true` makes Node join argv with plain spaces and no escaping (the
|
||||
* DEP0190 warning). That mangles every launch-codex invocation on Windows, not
|
||||
* just the ones with a multi-word user argument: the injected `-c` provider
|
||||
* flags carry TOML values whose quotes cmd.exe strips
|
||||
* (`model_providers.omniroute.name="OmniRoute"` arrives unquoted and no longer
|
||||
* parses as TOML). Quote the args ourselves on that path; off Windows there is
|
||||
* no shell, so argv is passed through untouched. Same fix as `launch` (#8837).
|
||||
*
|
||||
* @param {string[]} args
|
||||
* @param {NodeJS.Platform|string} platform
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function quoteCodexArgs(args, platform) {
|
||||
return quoteShellArgs(args, platform);
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
let s = String(value);
|
||||
let end = s.length;
|
||||
@@ -171,7 +153,7 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const { command: codexLaunch, shell: shellValue } = resolveCodexSpawn(process.platform);
|
||||
const child = spawn(codexLaunch, quoteCodexArgs(extraArgs, process.platform), {
|
||||
const child = spawn(codexLaunch, extraArgs, {
|
||||
env,
|
||||
stdio: "inherit",
|
||||
shell: shellValue,
|
||||
@@ -213,10 +195,7 @@ export function registerLaunchCodex(program) {
|
||||
.argument("[codexArgs...]", "arguments passed through to the codex binary")
|
||||
.action(async (codexArgs, opts) => {
|
||||
const merged = { ...opts, profile: opts.profile ?? opts.p };
|
||||
// process.exit() here aborted the process with a libuv assertion on
|
||||
// Windows (`!(handle->flags & UV_HANDLE_CLOSING)`, async.c:94): it tears
|
||||
// the loop down while the inherited stdio handles of the just-exited
|
||||
// child are still closing. Setting exitCode lets the loop drain first.
|
||||
process.exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
|
||||
const exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { quoteShellArgs } from "../utils/winShellArgs.mjs";
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
let s = String(value);
|
||||
@@ -91,34 +90,6 @@ export function resolveLaunchTarget(opts = {}) {
|
||||
return { baseUrl, authToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* #8246: on Windows, npm installs claude as a `.cmd` shim — spawn() without a
|
||||
* shell cannot resolve PATHEXT shims (and Node refuses to exec `.cmd` directly
|
||||
* since CVE-2024-27980), so the Windows path must go through cmd.exe.
|
||||
*
|
||||
* @param {NodeJS.Platform|string} platform
|
||||
* @returns {{ command: string, shell: true|undefined }}
|
||||
*/
|
||||
export function resolveClaudeSpawn(platform) {
|
||||
return platform === "win32"
|
||||
? { command: "claude.cmd", shell: true }
|
||||
: { command: "claude", shell: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* `shell: true` makes Node join argv with plain spaces and no escaping (the
|
||||
* DEP0190 warning), so `-p "two words"` used to reach claude as `-p two` plus
|
||||
* three stray positional arguments. Quote the args ourselves on that path.
|
||||
* Off Windows there is no shell, so argv is passed through untouched.
|
||||
*
|
||||
* @param {string[]} args
|
||||
* @param {NodeJS.Platform|string} platform
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function quoteClaudeArgs(args, platform) {
|
||||
return quoteShellArgs(args, platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{port?:string, remote?:string, token?:string, apiKey?:string, profile?:string, claudeHome?:string}} opts
|
||||
* @param {string[]} claudeArgs pass-through args for the claude binary
|
||||
@@ -149,12 +120,13 @@ export async function runLaunchCommand(opts = {}, claudeArgs = []) {
|
||||
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const { command, shell } = resolveClaudeSpawn(process.platform);
|
||||
const child = spawn(command, quoteClaudeArgs(claudeArgs, process.platform), {
|
||||
// #8246: on Windows, npm installs claude as a .cmd shim — spawn() without
|
||||
// shell:true cannot resolve PATHEXT shims and fails with ENOENT.
|
||||
const claudeCommand = process.platform === "win32" ? "claude.cmd" : "claude";
|
||||
const child = spawn(claudeCommand, claudeArgs, {
|
||||
env,
|
||||
stdio: "inherit",
|
||||
shell,
|
||||
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
||||
...(process.platform === "win32" ? { shell: true, windowsHide: true } : {}),
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
if (err && err.code === "ENOENT") {
|
||||
@@ -187,10 +159,7 @@ export function registerLaunch(program) {
|
||||
.allowExcessArguments(true)
|
||||
.argument("[claudeArgs...]", "arguments passed through to the claude binary")
|
||||
.action(async (claudeArgs, opts) => {
|
||||
// process.exit() here aborted the process with a libuv assertion on
|
||||
// Windows (`!(handle->flags & UV_HANDLE_CLOSING)`, async.c:94): it tears
|
||||
// the loop down while the inherited stdio handles of the just-exited
|
||||
// child are still closing. Setting exitCode lets the loop drain first.
|
||||
process.exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
|
||||
const exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,11 +7,6 @@ import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
|
||||
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
|
||||
import {
|
||||
ensureAndroidCacheDir,
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
@@ -67,37 +62,9 @@ export function registerServe(program) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Once-per-process guard so the Android/Termux cache hint is not spammed. */
|
||||
let instrumentationFailureHintPrinted = false;
|
||||
|
||||
/**
|
||||
* If child output looks like Next.js failed to load its instrumentation hook
|
||||
* on Android/Termux, print a clear operator-facing fix hint.
|
||||
* Exported for unit tests.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {boolean} true when a hint was printed
|
||||
*/
|
||||
export function maybeReportInstrumentationHookFailure(text) {
|
||||
if (instrumentationFailureHintPrinted) return false;
|
||||
if (!isFatalInstrumentationHookFailure(text)) return false;
|
||||
instrumentationFailureHintPrinted = true;
|
||||
process.stderr.write(formatAndroidInstrumentationFailureHint(process.env.XDG_CACHE_HOME));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Test-only reset for the once-per-process hint guard. */
|
||||
export function resetInstrumentationFailureHintForTests() {
|
||||
instrumentationFailureHintPrinted = false;
|
||||
}
|
||||
|
||||
export async function runServe(opts = {}) {
|
||||
const startedAt = performance.now();
|
||||
|
||||
// Same prep as bin/omniroute.mjs — keep it here so a direct `runServe()` call
|
||||
// (tests / programmatic) still gets a writable Next.js cache dir before spawn.
|
||||
ensureAndroidCacheDir({ env: process.env });
|
||||
|
||||
const { isNativeBinaryCompatible } =
|
||||
await import("../../../scripts/build/native-binary-compat.mjs");
|
||||
const { getNodeRuntimeSupport, getNodeRuntimeWarning } =
|
||||
@@ -167,11 +134,7 @@ export async function runServe(opts = {}) {
|
||||
"Release",
|
||||
"better_sqlite3.node"
|
||||
);
|
||||
if (
|
||||
!process.versions.bun &&
|
||||
existsSync(sqliteBinary) &&
|
||||
!isNativeBinaryCompatible(sqliteBinary)
|
||||
) {
|
||||
if (!process.versions.bun && existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
|
||||
console.error(
|
||||
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
|
||||
);
|
||||
@@ -267,16 +230,15 @@ export async function runServe(opts = {}) {
|
||||
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
|
||||
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
|
||||
const server = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)), serverJs],
|
||||
{
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
}
|
||||
);
|
||||
const server = spawn(process.versions.bun ? process.execPath : "node", [
|
||||
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
], {
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
});
|
||||
writePidFile("server", server.pid);
|
||||
server.unref();
|
||||
console.log(`\x1b[32m✔ OmniRoute started in background (PID: ${server.pid})\x1b[0m`);
|
||||
@@ -287,15 +249,14 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
|
||||
function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen, startedAt) {
|
||||
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
|
||||
const server = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)), serverJs],
|
||||
{
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
stdio: "pipe",
|
||||
}
|
||||
);
|
||||
const server = spawn(process.versions.bun ? process.execPath : "node", [
|
||||
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
], {
|
||||
cwd: APP_DIR,
|
||||
env,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
writePidFile("server", server.pid);
|
||||
|
||||
@@ -304,7 +265,6 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
|
||||
server.stdout.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
process.stdout.write(text);
|
||||
maybeReportInstrumentationHookFailure(text);
|
||||
if (
|
||||
!started &&
|
||||
(text.includes("Ready") || text.includes("started") || text.includes("listening"))
|
||||
@@ -314,11 +274,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
|
||||
}
|
||||
});
|
||||
|
||||
server.stderr.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
process.stderr.write(text);
|
||||
maybeReportInstrumentationHookFailure(text);
|
||||
});
|
||||
server.stderr.on("data", (data) => process.stderr.write(data));
|
||||
|
||||
server.on("error", (err) => {
|
||||
console.error("\x1b[31m✖ Failed to start server:\x1b[0m", err.message);
|
||||
@@ -427,9 +383,6 @@ export function reportReadinessTimeout(dashboardPort, supervisor) {
|
||||
console.error("--- Recent server output ---");
|
||||
recentLog.forEach((l) => console.error(l));
|
||||
console.error("--- End recent output ---\n");
|
||||
// If the buffered log already shows the Android instrumentation failure,
|
||||
// print the actionable hint even when --log was off (default).
|
||||
maybeReportInstrumentationHookFailure(recentLog.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,11 @@ import {
|
||||
} from "./supervisorPolicy.mjs";
|
||||
import { buildNodeHeapArgs } from "../../../scripts/build/runtime-env.mjs";
|
||||
import { stopProcessGracefully } from "../../../src/shared/platform/windowsProcess.ts";
|
||||
import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
|
||||
export class ServerSupervisor {
|
||||
constructor({
|
||||
serverPath,
|
||||
env,
|
||||
maxRestarts = DEFAULT_MAX_RESTARTS,
|
||||
memoryLimit = 512,
|
||||
onCrashCallback,
|
||||
}) {
|
||||
constructor({ serverPath, env, maxRestarts = DEFAULT_MAX_RESTARTS, memoryLimit = 512, onCrashCallback }) {
|
||||
this.serverPath = serverPath;
|
||||
this.env = env;
|
||||
this.maxRestarts = maxRestarts;
|
||||
@@ -35,13 +25,11 @@ export class ServerSupervisor {
|
||||
this.crashLog = [];
|
||||
this.child = null;
|
||||
this.isShuttingDown = false;
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.crashLog = [];
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
|
||||
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
|
||||
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
|
||||
@@ -53,35 +41,23 @@ export class ServerSupervisor {
|
||||
// silently, so a boot that never becomes ready looked like a dead hang with zero
|
||||
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
|
||||
// stderr so a readiness timeout can surface what the child actually printed.
|
||||
this.child = spawn(
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[...(process.versions.bun ? [] : heapArgs), this.serverPath],
|
||||
{
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
this.child = spawn(process.versions.bun ? process.execPath : "node", [
|
||||
...(process.versions.bun ? [] : heapArgs),
|
||||
this.serverPath,
|
||||
], {
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
writePidFile("server", this.child.pid);
|
||||
|
||||
const bufferOutput = (data) => {
|
||||
const text = data.toString();
|
||||
const lines = text.split("\n").filter(Boolean);
|
||||
const lines = data.toString().split("\n").filter(Boolean);
|
||||
this.crashLog.push(...lines);
|
||||
if (this.crashLog.length > CRASH_LOG_LINES) {
|
||||
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
|
||||
}
|
||||
// Surface Android/Termux instrumentation-hook failures even when --log is
|
||||
// off (output is only buffered otherwise).
|
||||
if (!this.instrumentationFailureHintPrinted && isFatalInstrumentationHookFailure(text)) {
|
||||
this.instrumentationFailureHintPrinted = true;
|
||||
process.stderr.write(
|
||||
formatAndroidInstrumentationFailureHint(
|
||||
this.env?.XDG_CACHE_HOME || process.env.XDG_CACHE_HOME
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.child.stdout) {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Next.js cache-dir prep for Android / Termux.
|
||||
*
|
||||
* Next.js `getCacheDirectory()` has no dedicated branch for
|
||||
* `process.platform === "android"`. On that path it only accepts a cache root
|
||||
* that *already* exists (`fs.existsSync` on `~/.cache` or a generic tmp dir).
|
||||
* If neither exists it prints `Unsupported platform: android` and exits — the
|
||||
* CLI can still look "running" while every request returns a bare HTTP 500
|
||||
* because the instrumentation hook never loads (and so neither does logging).
|
||||
*
|
||||
* Termux Node sometimes reports `platform === "android"` and sometimes
|
||||
* `"linux"` with Termux env signals (`TERMUX_VERSION` / `PREFIX`). Creating
|
||||
* `~/.cache` (and pointing `XDG_CACHE_HOME` at it when unset) makes the probe
|
||||
* succeed on both shapes.
|
||||
*
|
||||
* Call this *before* spawning or loading Next.js. Safe no-op on desktop
|
||||
* platforms that are not Termux.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
|
||||
|
||||
/**
|
||||
* @param {string} [platform]
|
||||
* @param {NodeJS.ProcessEnv} [env]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function needsAndroidCacheDirPrep(platform = process.platform, env = process.env) {
|
||||
return platform === "android" || isTermux(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => string} [homedirFn]
|
||||
* @param {NodeJS.ProcessEnv} [env]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function resolveAndroidCacheDir(homedirFn = homedir, env = process.env) {
|
||||
if (typeof env.XDG_CACHE_HOME === "string" && env.XDG_CACHE_HOME.trim()) {
|
||||
return env.XDG_CACHE_HOME;
|
||||
}
|
||||
return join(homedirFn(), ".cache");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a writable cache directory exists for Next.js on Android/Termux.
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.platform]
|
||||
* @param {NodeJS.ProcessEnv} [options.env]
|
||||
* @param {() => string} [options.homedirFn]
|
||||
* @param {typeof mkdirSync} [options.mkdirSyncFn]
|
||||
* @param {typeof existsSync} [options.existsSyncFn]
|
||||
* @param {boolean} [options.setEnv] When true (default), set `XDG_CACHE_HOME` on `env`
|
||||
* if unset so child processes inherit a known-writable cache root.
|
||||
* @returns {{ prepared: boolean, cacheDir: string | null, created: boolean }}
|
||||
*/
|
||||
export function ensureAndroidCacheDir(options = {}) {
|
||||
const {
|
||||
platform = process.platform,
|
||||
env = process.env,
|
||||
homedirFn = homedir,
|
||||
mkdirSyncFn = mkdirSync,
|
||||
existsSyncFn = existsSync,
|
||||
setEnv = true,
|
||||
} = options;
|
||||
|
||||
if (!needsAndroidCacheDirPrep(platform, env)) {
|
||||
return { prepared: false, cacheDir: null, created: false };
|
||||
}
|
||||
|
||||
const cacheDir = resolveAndroidCacheDir(homedirFn, env);
|
||||
let created = false;
|
||||
if (!existsSyncFn(cacheDir)) {
|
||||
mkdirSyncFn(cacheDir, { recursive: true });
|
||||
created = true;
|
||||
}
|
||||
|
||||
if (setEnv && !(typeof env.XDG_CACHE_HOME === "string" && env.XDG_CACHE_HOME.trim())) {
|
||||
env.XDG_CACHE_HOME = cacheDir;
|
||||
}
|
||||
|
||||
return { prepared: true, cacheDir, created };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Next.js instrumentation-hook failures that leave the server looking
|
||||
* "up" while requests get silent HTTP 500s (typical when the Android cache
|
||||
* probe failed before logging started).
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFatalInstrumentationHookFailure(text) {
|
||||
if (!text) return false;
|
||||
return (
|
||||
/Unsupported platform:\s*android/i.test(text) ||
|
||||
/error occurred while loading instrumentation hook/i.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator-facing hint when that instrumentation failure shows up in child
|
||||
* output — defense in depth if prep was skipped or a future Next.js probe
|
||||
* regresses.
|
||||
*
|
||||
* @param {string} [cacheDir]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatAndroidInstrumentationFailureHint(cacheDir) {
|
||||
const dir = cacheDir || join(homedir(), ".cache");
|
||||
return (
|
||||
`\n\x1b[31m✖ Next.js instrumentation failed on Android/Termux (likely missing cache dir).\x1b[0m\n` +
|
||||
` OmniRoute tried to create a writable cache at:\n` +
|
||||
` \x1b[36m${dir}\x1b[0m\n` +
|
||||
` Manual workaround (survives reinstalls — do NOT patch dist/server.js):\n` +
|
||||
` \x1b[36mmkdir -p ~/.cache\x1b[0m\n` +
|
||||
` then restart: \x1b[36momniroute serve\x1b[0m\n` +
|
||||
` See: docs/guides/TERMUX_GUIDE.md → Troubleshooting → Unsupported platform: android\n`
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Argument escaping for child processes spawned with `shell: true` on Windows.
|
||||
*
|
||||
* The launchers (`omniroute launch`, `omniroute launch-codex`) must go through
|
||||
* cmd.exe on win32 because the target binaries are npm `.cmd` shims that Node
|
||||
* cannot exec directly (CVE-2024-27980). With `shell: true` Node joins argv with
|
||||
* plain spaces and no escaping at all (the DEP0190 warning), so anything with a
|
||||
* space, a quote or a cmd metacharacter reaches the child mangled.
|
||||
*/
|
||||
|
||||
/** cmd.exe metacharacters that stay live inside a quoted argument. */
|
||||
const WIN_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
||||
|
||||
/**
|
||||
* Escape one argument for a cmd.exe command line built by `shell: true`.
|
||||
*
|
||||
* Two layers, in order:
|
||||
* 1. the CRT argv rules the target binary parses (double the backslashes that
|
||||
* precede a quote, escape embedded quotes, wrap in quotes);
|
||||
* 2. cmd.exe's metacharacters, caret-escaped — applied TWICE because the
|
||||
* target is an npm `.cmd` shim that forwards `%*` to node, so the line is
|
||||
* parsed by cmd a second time. Single-escaping truncated any argument at
|
||||
* the first `&` or `|`. (Same rule as cross-spawn's doubleEscapeMetaChars.)
|
||||
*
|
||||
* @param {unknown} arg
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapeWindowsShellArg(arg) {
|
||||
const s = String(arg);
|
||||
if (s === "") return '""';
|
||||
let out = s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
||||
out = `"${out}"`;
|
||||
return out.replace(WIN_META_CHARS, "^$1").replace(WIN_META_CHARS, "^$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a whole argv for the `shell: true` path. Off Windows there is no shell,
|
||||
* so argv is passed through untouched.
|
||||
*
|
||||
* @param {string[]} args
|
||||
* @param {NodeJS.Platform|string} platform
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function quoteShellArgs(args, platform) {
|
||||
const list = [...(args ?? [])];
|
||||
return platform === "win32" ? list.map(escapeWindowsShellArg) : list;
|
||||
}
|
||||
@@ -145,15 +145,6 @@ function loadEnvFile() {
|
||||
|
||||
loadEnvFile();
|
||||
|
||||
// Next.js has no android branch in getCacheDirectory(): if ~/.cache (and tmp)
|
||||
// do not already exist it aborts the instrumentation hook, and every request
|
||||
// then returns a silent HTTP 500 even though the CLI still looks "running".
|
||||
// Create the cache dir (and set XDG_CACHE_HOME when unset) before serve/Next.
|
||||
{
|
||||
const { ensureAndroidCacheDir } = await import("./cli/utils/ensureAndroidCacheDir.mjs");
|
||||
ensureAndroidCacheDir();
|
||||
}
|
||||
|
||||
// Generate STORAGE_ENCRYPTION_KEY if not set (persisted to ~/.omniroute/.env)
|
||||
// This ensures the key survives across upgrades and is not regenerated on each install.
|
||||
// See: https://github.com/diegosouzapw/OmniRoute/issues/1622
|
||||
|
||||
1
changelog.d/features/6022-decision-trace-header.md
Normal file
1
changelog.d/features/6022-decision-trace-header.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sse):** every completion response now carries an `X-OmniRoute-Decision: strategy=<name>; provider=<alias>; latency_ms=<n>` header exposing the routing decision — `<name>` is the combo strategy (`priority`, `weighted`, `fusion`, etc.) or `single` for a non-combo request — for client-side debugging/analytics without server log access (#6022 — thanks @chirag127).
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).
|
||||
1
changelog.d/features/6354-per-model-timeout.md
Normal file
1
changelog.d/features/6354-per-model-timeout.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(sse): configurable per-model upstream header-response timeout override, precedence model > provider > global; applied to codex reasoning-heavy tiers (gpt-5.5-high/xhigh, gpt-5.6-\*-high/xhigh) (#6354)
|
||||
1
changelog.d/features/6540-hidepaid-ui-selects.md
Normal file
1
changelog.d/features/6540-hidepaid-ui-selects.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** Replace free-text model inputs in the Routing (web search route), Combo Defaults (handoff model), and Background Degradation tabs with a `hidePaidModels`-aware `ModelSelectField`, add a fail-open "paid-only pattern" warning to the per-model routing rule pattern field, and reject paid-only model targets at save time on `PATCH /api/settings`, `PATCH /api/settings/combo-defaults`, and `PUT /api/settings/background-degradation` when `hidePaidModels` is on ([#6540](https://github.com/diegosouzapw/OmniRoute/issues/6540))
|
||||
1
changelog.d/features/6556-omniglyph-engine.md
Normal file
1
changelog.d/features/6556-omniglyph-engine.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).
|
||||
1
changelog.d/features/6593-ratelimit-admission-control.md
Normal file
1
changelog.d/features/6593-ratelimit-admission-control.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sse):** rate-limit request queue admission control — `resilienceSettings.requestQueue.maxQueueDepth` (default `0` = disabled, opt-in 0–100000) fast-rejects a request with a typed `RATE_LIMIT_QUEUE_FULL` error once the local per-provider+connection queue already holds `maxQueueDepth` requests, instead of growing the queue unboundedly; the factory default for `requestQueue.maxWaitMs` (how long a request may wait before being dropped) also fell from 120s to 15s so a saturated queue fails fast (#6593 — thanks @chirag127).
|
||||
1
changelog.d/features/6611-native-container-runtimes.md
Normal file
1
changelog.d/features/6611-native-container-runtimes.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).
|
||||
1
changelog.d/features/6636-codex-session-import.md
Normal file
1
changelog.d/features/6636-codex-session-import.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(oauth): accept the full ChatGPT session JSON (not just a bare access token) when pasting Codex credentials manually or via `POST /api/oauth/codex/import-token` (#6636)
|
||||
1
changelog.d/features/6650-g4f-space-gateway.md
Normal file
1
changelog.d/features/6650-g4f-space-gateway.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(sse): add 5 no-key g4f.space gateway providers — `g4f-groq`, `g4f-gemini`, `g4f-pollinations`, `g4f-ollama`, `g4f-nvidia` — a free, no-signup reverse proxy (gpt4free project) fronting Groq, Gemini, Pollinations, Ollama, and NVIDIA NIM, rate-limited to 5 req/min per IP (#6650 — thanks @chirag127).
|
||||
1
changelog.d/features/6653-deepinfra-video-provider.md
Normal file
1
changelog.d/features/6653-deepinfra-video-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(sse): add DeepInfra as a video-generation provider via its native synchronous inference endpoint (#6653)
|
||||
@@ -0,0 +1 @@
|
||||
- feat(providers): add Freepik (Magnific Mystic) API-key image generation provider — async submit/poll flow with realism/fluid/zen/flexible/super_real/editorial_portraits models (#6654)
|
||||
1
changelog.d/features/6655-revai-stt-provider.md
Normal file
1
changelog.d/features/6655-revai-stt-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): add Rev AI speech-to-text provider with async job upload/poll/transcript flow (#6655)
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add **Segmind** as an image + video generation provider — `x-api-key` auth against `POST https://api.segmind.com/v1/{model}`, with a curated starter model list (Flux, Stable Diffusion XL/3.5, Kandinsky for image; Wan, Hunyuan, LTX, Kling for video) (#6656).
|
||||
1
changelog.d/features/6657-gladia-stt-provider.md
Normal file
1
changelog.d/features/6657-gladia-stt-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): add Gladia as an async speech-to-text provider (#6657)
|
||||
1
changelog.d/features/6658-novita-video-gen-provider.md
Normal file
1
changelog.d/features/6658-novita-video-gen-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(video): add Novita AI as a video-generation provider (Wan/Kling async submit-poll) (#6658)
|
||||
1
changelog.d/features/6659-speechmatics-stt-provider.md
Normal file
1
changelog.d/features/6659-speechmatics-stt-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add Speechmatics as an STT provider — async batch transcription (Enhanced operating point), 8 hours/month free tier, no credit card required. Streaming (real-time) mode is out of scope for v1. (#6659)
|
||||
@@ -0,0 +1 @@
|
||||
- feat(providers): add Mixedbread AI as an embeddings provider (`mxbai-embed-large-v1`, `mxbai-embed-2d-large-v1`, free tier) (#6660)
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add Felo (felo.ai) as a free, no-signup, no-API-key chat/search-agent aggregator provider (`felo-web`) — joins the existing `-web` family (DuckDuckGo AI Chat, Blackbox, etc). Five models (`felo-chat`, `felo-search`, `felo-scholar`, `felo-social`, `felo-document`) map to Felo's search categories; the executor opens a search thread then translates Felo's bespoke SSE stream into OpenAI-compatible chunks (#6666).
|
||||
1
changelog.d/features/6667-gtts-audio-tts-provider.md
Normal file
1
changelog.d/features/6667-gtts-audio-tts-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add gTTS (Google Translate TTS) as a free, no-signup audio-speech provider — routes through Google's current `batchexecute` RPC endpoint (the previously proposed `translate_tts` endpoint is deprecated), splitting input at the 100-char-per-request limit. (#6667)
|
||||
1
changelog.d/features/6668-edgetts-audio-tts-provider.md
Normal file
1
changelog.d/features/6668-edgetts-audio-tts-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sse):** add EdgeTTS (Microsoft Edge "Read Aloud") as a free, no-API-key `audio-tts` provider — the first WebSocket-transport speech provider, with per-client-IP rate limiting. (#6668)
|
||||
1
changelog.d/features/6670-freetheai-gateway-provider.md
Normal file
1
changelog.d/features/6670-freetheai-gateway-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): add FreeTheAi as an OpenAI-compatible gateway provider with a free Discord-signup tier (#6670)
|
||||
@@ -0,0 +1 @@
|
||||
- feat(sse): add Microsoft Designer as an unofficial web-session image provider, reverse-engineered submit-then-poll DallE.ashx flow (#6672)
|
||||
1
changelog.d/features/6673-hailuoai-web-provider.md
Normal file
1
changelog.d/features/6673-hailuoai-web-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** add Hailuo Web (`hailuo-web`) — a free, `_token`-based web-cookie chat provider for the MiniMax consumer chat product at hailuo.ai, ported from the g4f reference implementation (MD5-chain request signing, custom `event:`/`data:` SSE parsing). Distinct from the existing paid API-key `minimax`/`minimax-cn` providers. (#6673)
|
||||
1
changelog.d/features/6683-cli-auth-export.md
Normal file
1
changelog.d/features/6683-cli-auth-export.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(cli):** new `omniroute auth export` command dumps DECRYPTED provider credentials (`apiKey`/`accessToken`/`refreshToken`/`idToken`) for one connection (`--id <id>`) or all connections, as `json` or `env` (`--format`), local-only and gated behind `--force` — no DB access happens without it, a stderr warning banner prints before any plaintext, `--out <file>` writes with `0600` permissions, and per-field decrypt failures surface as a `<field>DecryptFailed` boolean instead of aborting the export or leaking the caught error text (#6683)
|
||||
1
changelog.d/features/6684-mitm-root-ca.md
Normal file
1
changelog.d/features/6684-mitm-root-ca.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(mitm):** the AgentBridge static MITM server (`server.cjs`) can now issue a per-host TLS leaf from a persisted local root CA (`src/mitm/cert/rootCa.ts`, reusing the CA/leaf crypto already proven for TPROXY in `tproxy/dynamicCert.ts`) instead of a single static self-signed leaf scoped only to the 4 antigravity hosts — a fresh install covers the full `MITM_TOOL_HOSTS` set automatically; an install that already trusted the old leaf keeps using it until the operator opts in via `MITM_ROOT_CA_ENABLED=true` (`src/mitm/cert/migration.ts`), so no existing install is silently upgraded to the more powerful any-host-signing CA trust model (#6684).
|
||||
1
changelog.d/features/6708-gemma4-thinkingconfig-guard.md
Normal file
1
changelog.d/features/6708-gemma4-thinkingconfig-guard.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)
|
||||
1
changelog.d/features/6709-xai-responses-endpoint.md
Normal file
1
changelog.d/features/6709-xai-responses-endpoint.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).
|
||||
1
changelog.d/features/6737-vary-accept-encoding.md
Normal file
1
changelog.d/features/6737-vary-accept-encoding.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(api):** add `Vary: Accept-Encoding` to token-authenticated `/v1*`/`/v1beta*` responses so downstream caches distinguish compressed vs uncompressed variants (RFC 9110 §12.5.5). (thanks @chirag127)
|
||||
1
changelog.d/features/6758-notion-web-provider.md
Normal file
1
changelog.d/features/6758-notion-web-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(sse): add Notion AI Web (Unofficial/Experimental) cookie-session provider (#6758)
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(dashboard):** add per-routing-combo compression-mode override to the Compression Combos page under Context & Cache, alongside the existing combo-card quick override. (#6760)
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc).
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(sse):** preserve `tools`/`tool_choice` for tool-bearing requests through fusion combos — bypass panel synthesis and route straight to the judge with tools intact (#6771 — thanks @chirag127).
|
||||
1
changelog.d/features/6774-cursor-grok-4-5-effort-fast.md
Normal file
1
changelog.d/features/6774-cursor-grok-4-5-effort-fast.md
Normal file
@@ -0,0 +1 @@
|
||||
- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell).
|
||||
1
changelog.d/features/6776-discover-live-codex-models.md
Normal file
1
changelog.d/features/6776-discover-live-codex-models.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn).
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user