Compare commits

..

1 Commits

Author SHA1 Message Date
diegosouzapw
972744a0c3 fix(combo): always clear the loop-safety timer, not just on the happy path (#11804)
dispatchWithCooldownRetry arms a loop-safety timer (setTimeout, 10 minutes by
default) on every setTry iteration, so a combo that never produces a terminal
response still answers with a 504 instead of hanging. The only clearTimeout in
the whole file sat inside the `if (anySuccess)` branch — the comment said so
verbatim: "clear the safety timer on the happy path".

Every error exit therefore returned the response to the client while leaving a
600s timer pending, its closure retaining orderedTargets and the exhausted
provider/connection sets: all_targets_skipped, all_accounts_inactive, the
aggregated-status return, the final fallback, and the global-timeout branch.
The timer is also re-armed per setTry iteration with no clear in between.

Field evidence from the issue: two requests that failed quality validation
returned 502 to the client immediately, and "Combo loop safety timeout ...
force-terminating" was logged for both exactly 600 seconds later — the leaked
timers firing long after the requests were gone.

Fixed structurally rather than by sprinkling clearTimeout across the five
return sites: the handle is hoisted to function scope and released in a
finally, so a future `return` added to this function cannot silently
reintroduce the leak. The 504 backstop itself is unchanged.

Note the timer already called .unref(), so it never held the event loop open —
this is a memory-retention leak, not a hang.
2026-09-01 00:10:43 -03:00
475 changed files with 13414 additions and 41849 deletions

View File

@@ -812,7 +812,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# CLI_CRUSH_BIN=crush
# CLI_OMP_BIN=omp
# CLI_LETTA_BIN=letta
# CLI_PRIME_AGENT_BIN=prime-agent
# Windsurf has no default binary — set this to enable binary detection for it.
# CLI_WINDSURF_BIN=windsurf
# CLI_AUGGIE_BIN=auggie
@@ -1470,25 +1469,17 @@ CURSOR_USER_AGENT="Cursor/3.4"
# FIRECRAWL_BASE_URL=https://api.firecrawl.dev
# FIRECRAWL_TIMEOUT_MS=30000 # Per-request timeout (default: 30000 = 30s)
# ── ChatGPT browser transport (Firefox-fingerprinted client) ──
# Used by: open-sse/services/chatgptTlsClient.ts — native wreq-js request
# timeout and the JS-side hard-deadline grace layered on top of it.
# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000
# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000
# Max wait for the FIRST streamed byte before switching from direct streaming
# to a buffered response, in milliseconds. Default 30000 (30s). The request's
# hard deadline continues to apply while the buffered body is read.
# OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS=30000
# ── Claude browser transport (Chromium-fingerprinted client) ──
# Used by: open-sse/services/claudeTlsClient.ts — native wreq-js request
# timeout and the JS-side hard-deadline grace layered on top of it.
# ── Claude TLS sidecar (Chromium-fingerprinted client) ──
# Used by: open-sse/services/claudeTlsClient.ts — wire-level timeout for
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
# layered on top of it when the native library is wedged.
# OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS=60000
# OMNIROUTE_CLAUDE_TLS_GRACE_MS=10000
# ── Perplexity browser transport (Firefox-fingerprinted client) ──
# Used by: open-sse/services/perplexityTlsClient.ts — native wreq-js request
# timeout and the JS-side hard-deadline grace layered on top of it.
# ── Perplexity TLS sidecar (Firefox-fingerprinted client) ──
# Used by: open-sse/services/perplexityTlsClient.ts — wire-level timeout for
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
# layered on top of it when the native library is wedged.
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
@@ -1500,16 +1491,18 @@ CURSOR_USER_AGENT="Cursor/3.4"
# meta-commentary. Set to 1/true/yes/on to restore the old behavior.
# OMNIROUTE_PPLX_SEARCH_HINT=0
# ── Grok web browser transport (Chrome-fingerprinted client) ──
# Used by: open-sse/services/grokTlsClient.ts — native wreq-js request timeout
# and the JS-side hard-deadline grace layered on top of it.
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
# top of it when the native library is wedged.
# OMNIROUTE_GROK_TLS_TIMEOUT_MS=60000
# OMNIROUTE_GROK_TLS_GRACE_MS=10000
# ── Notion web browser transport (Chrome-fingerprinted client) ──
# Used by: open-sse/services/notionTlsClient.ts — native wreq-js request timeout
# and the JS-side hard-deadline grace layered on top of it. The notion-web
# executor raises the native timeout per-request to 180000 for long generations.
# ── Notion web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/notionTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
# top of it when the native library is wedged. The notion-web executor raises
# the wire timeout per-request to 180000 for long generations.
# OMNIROUTE_NOTION_TLS_TIMEOUT_MS=30000
# OMNIROUTE_NOTION_TLS_GRACE_MS=10000
@@ -2473,6 +2466,14 @@ APP_LOG_TO_FILE=true
# When enabled, the node authenticates with the API key stored on its connection.
# AUDIO_REMOTE_PROVIDER_NODES=false
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
# CrofAI 1Proxy service. Disable, override URL, or tune the import quality.
# ONEPROXY_ENABLED=true
# ONEPROXY_API_URL=https://1proxy-api.aitradepulse.com
# ONEPROXY_MAX_PROXIES=500
# ONEPROXY_MIN_QUALITY_THRESHOLD=50
# ── Free Proxy Pool (auto-sync scheduler) ──
# Background refresh of the free-proxy pool. Opt-in, OFF by default (parallels
# Hard Rule #20's default-off posture for data-mutating background features).
@@ -2891,14 +2892,6 @@ QUOTA_STORE_DRIVER=sqlite
# PROMPTQL_TOKEN_REFRESH_URL=https://auth.pro.ql.app/ddn/project/token
# PROMPTQL_POLL_TIMEOUT_MS=180000
# ─────────────────────────────────────────────────────────────────────────────
# Kilo Code usage quotas (src/shared/constants/providers/kilocode.ts)
# Personal USD balance and Kilo Pass usage lookup. Optional — the default
# points at the public Kilo API; override only for a relay/test fixture.
# Authentication uses the connection's existing OAuth access token.
# Used by: open-sse/services/usage/kilocode.ts
# ─────────────────────────────────────────────────────────────────────────────
# KILO_API_URL=https://api.kilo.ai
# ─────────────────────────────────────────────────────────────────────────────
# HyperAgent web provider (Unofficial/Experimental — src/shared/constants/providers/web-cookie.ts)
# Reverse-engineered session bridge for hyperagent.com. Optional — defaults
@@ -2918,12 +2911,7 @@ QUOTA_STORE_DRIVER=sqlite
# 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 v2
# CODEX_CHATGPT_WEB_HOME=/var/lib/omniroute/chatgpt-web-codex
# CODEX_CHATGPT_WEB_BROWSER_DIAGNOSTICS=0
# CODEX_CHATGPT_WEB_LAUNCHER=/absolute/path/to/codex-chatgpt-web
# CODEX_CHATGPT_WEB_BUN=/absolute/path/to/bun
# CODEX_WEB_GPT_BUN=/absolute/path/to/bun
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex
# ─────────────────────────────────────────────────────────────────────────────
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)

View File

@@ -237,7 +237,7 @@ jobs:
# optionals (@img/sharp-*, @img/sharp-libvips-*, @ngrok/ngrok-*,
# fsevents) carry linux forks. Replace them with the forks this
# leg's own `npm ci` resolved, then assert every bundled native
# (better-sqlite3 prebuilds, wreq-js, onnxruntime)
# (koffi triplets, better-sqlite3 prebuilds, wreq-js, onnxruntime)
# can service this leg's platform/arch before packaging starts.
run: |
node scripts/build/standaloneBundle.mjs restore --archive web-bundle.tar.gz

View File

@@ -131,14 +131,10 @@ breaker runs on `circuitBreakerThreshold` / `circuitBreakerReset`:
| API key | `7` | `12` | `30s` |
| Local | (derived) | `2` | `15s` |
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2),
`providerFailureWindowMs` (15/30/5 min) and `providerCooldownMs` (5/10/1 min): these power the
**window gate of the opt-in global Provider Cooldown** (`PROVIDER_COOLDOWN_ENABLED`, default
off) — a provider-level entry in `open-sse/services/providerCooldownTracker.ts` only counts as
cooling after `providerFailureThreshold` failures inside `providerFailureWindowMs`, and then
cools for `providerCooldownMs`. They are NOT the live breaker's thresholds — do not tune them
expecting breaker behavior. Every default is overridable through the
`OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2) and `providerCooldownMs`
(5min/10min/1min); those fields are loaded into the profile but have **no runtime consumer
today** — do not tune or document them as the live breaker. Every default is overridable
through the `OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
runtime-accurate reference table lives in `docs/architecture/RESILIENCE_GUIDE.md`.
Only provider-level failure statuses should trip the provider breaker:
@@ -494,12 +490,6 @@ Why this matters: fixing bug A while opening bug B is worse than not fixing at a
pipeline, and A2A skills.
- Do not close a contributor pull request after using its code; merge it through GitHub so
the contributor receives credit.
- **Never merge a PR that touches an agent-instruction surface without explicit operator
approval** — `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `llm.txt` (+ mirrors) and
`skills/**/SKILL.md` are executed as authority by every AI session; a merged instruction
compromises every future agent run. Check with `gh pr diff <N> --name-only` before any
merge. Incident record: PR #11770 (2026-09-01) told agents to execute a third-party
setup script and was swept in by a merge campaign; reverted in #12249.
---

View File

@@ -73,9 +73,6 @@ npm run dev
npm run build # next build → .build/next/ then assembleStandalone → dist/
npm run start
# Fast backend/API-only compile for contributor changes
npm run build:contributor
# Release build (clean rebuild + HEAD sentinel — required for deploy)
npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA
@@ -83,10 +80,6 @@ npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
```
The contributor build performs compile-only validation: it does not assemble the standalone
distribution or build optional native packaging assets. Use the regular production build when
you need to validate the shippable bundle.
### Build Output Layout
| Directory | Contents | Tracked |
@@ -107,11 +100,6 @@ npm run build
`npm run build:release` additionally cleans both directories first and writes
`dist/BUILD_SHA` (= `git rev-parse --short HEAD`) as a deploy integrity sentinel.
`npm run build:contributor` uses the backend-only build profile. It temporarily stubs
dashboard UI files while building, keeps API route handlers, and restores the original files
after the build. Use `npm run build` for changes that affect the dashboard UI or for full
release validation; the contributor profile is not a replacement for the release build.
> **VPS deploy note:** the remote image directory `/usr/lib/node_modules/omniroute/app/`
> is unchanged. The deploy skills rsync the contents of `dist/` into it.
> Only the in-repo build output path moved (`app/` → `dist/`).

View File

@@ -103,11 +103,25 @@ RUN test -f package-lock.json \
# node-gyp comes from npm's own bundled copy (deterministic, already in the image)
# instead of `npx --yes`, which would install an arbitrary registry version
# on-demand and run its lifecycle scripts (Sonar docker:S6505).
#
# tls-client-node (claude-web/grok-web/lmarena/perplexity-web TLS
# impersonation) hits the same --ignore-scripts wall: its own postinstall.js
# fetches a platform .so/.dylib/.dll from the bogdanfinn/tls-client GitHub
# Releases API and is never invoked when npm ci skips lifecycle scripts. Unlike
# better-sqlite3 above, that script never throws on failure — it only
# `console.warn`s and exits 0 — so a rate-limited or offline build would
# otherwise succeed silently with an empty bin/ and only fail at first request
# in production (TlsClientUnavailableError, #7802). Run it explicitly here so
# a broken/rate-limited fetch fails the BUILD loudly instead of shipping a
# broken image.
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& (cd node_modules/better-sqlite3 \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
&& node -e "require('better-sqlite3')(':memory:').close()"
&& node -e "require('better-sqlite3')(':memory:').close()" \
&& node node_modules/tls-client-node/scripts/postinstall.js \
&& (test -n "$(find node_modules/tls-client-node/bin -mindepth 1 -print -quit 2>/dev/null)" \
|| (echo "tls-client-node native binary missing after postinstall — GitHub API fetch likely rate-limited or failed (#7802)" >&2 && exit 1))
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era
# TurbopackInternalError panic ("entered unreachable code: there must be a path to a

View File

@@ -1,5 +1,5 @@
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
FROM oven/bun:1.4.0-slim AS base
FROM oven/bun:1.3.14-slim AS base
WORKDIR /app
RUN apt-get update \
@@ -31,9 +31,9 @@ COPY scripts/dev/sync-env.mjs ./scripts/dev/sync-env.mjs
# Fast Bun native package install
RUN bun install --include=optional --quiet
# Compile native better-sqlite3 Node-API addon under Bun
RUN if [ -d "node_modules/better-sqlite3" ]; then \
(cd node_modules/better-sqlite3 && bunx node-gyp rebuild); \
# Fetch tls-client-node native binary if script exists
RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ] && [ ! -d "node_modules/tls-client-node/bin" ]; then \
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
fi
# Smoke check native database driver used by Bun (bun:sqlite)
@@ -58,7 +58,7 @@ ENV NODE_ENV=production
RUN bun run --quiet build
# ── Runner Base stage (100% Bun Native Production Runtime) ──────────────────
FROM oven/bun:1.4.0-slim AS runner-base
FROM oven/bun:1.3.14-slim AS runner-base
LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \

View File

@@ -50,7 +50,7 @@
[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/U47eFqAXCn)
[![Telegram](https://img.shields.io/badge/Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/omnirouteOficial)
[![WhatsApp Global](https://img.shields.io/badge/WhatsApp_Global-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
[![WhatsApp Brasil](https://img.shields.io/badge/WhatsApp_Brasil-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/KWgatljAjmbELQory59Oti?s=cl&p=a&mlu=4)
[![WhatsApp Brasil](https://img.shields.io/badge/WhatsApp_Brasil-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz)
[![Website](https://img.shields.io/badge/Website-omniroute.online-blue?logo=google-chrome&logoColor=white)](https://omniroute.online)
**Questions, provider tips, roadmap & support → [Discord](https://discord.gg/U47eFqAXCn) · [Telegram](https://t.me/omnirouteOficial) · WhatsApp [🌍 Global](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) / [🇧🇷 Brasil](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz)**
@@ -1183,10 +1183,9 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
| 🐙 **GitHub** — follow for releases & tips | [@diegosouzapw](https://github.com/diegosouzapw) |
| 💬 **Discord** | [discord.gg/U47eFqAXCn](https://discord.gg/U47eFqAXCn) |
| ✈️ **Telegram** | [t.me/omnirouteOficial](https://t.me/omnirouteOficial) |
| 🟢 **WhatsApp — 🌍 Global** | [join the group](https://chat.whatsapp.com/FvuCbrpZmQ6I85n2vW5QIC?s=cl&p=a&mlu=4) |
| 🟢 **WhatsApp — 🇧🇷 Brasil** | [entrar no grupo](https://chat.whatsapp.com/KWgatljAjmbELQory59Oti?s=cl&p=a&mlu=4) |
| 🟢 **WhatsApp — 🌍 Global** | [join the group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) |
| 🟢 **WhatsApp — 🇧🇷 Brasil** | [entrar no grupo](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz) |
| 🌍 **Website** | [omniroute.online](https://omniroute.online) |
| 🌍 **🌍StHub OmniRoute Community (free)** | [portal sthub](https://portal.sthub.com.br/communities/groups/st-hub/channels/Omniroute-World-8kRjmK) |
| 📦 **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` |

View File

@@ -3,8 +3,8 @@
## 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), v4.0.7 commit
`b59d7dc51b84fb1f465ff1d00f5207f3b2b4a494`.
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit
`55592fca0ba19a27f1b769cec8fff61ff340a785`.
MIT License
@@ -25,31 +25,6 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FO
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.
## wreq-js 3.0.0
OmniRoute distributes `wreq-js` and its seven platform-specific native addons from
[`wreq-js@3.0.0`](https://www.npmjs.com/package/wreq-js/v/3.0.0).
MIT License
Copyright (c) 2025 will-work-for-meal
Copyright (c) 2025 Oleksandr Herasymov
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.
## blackwell-systems/gcf-typescript
The generic-profile codec in

View File

@@ -32,20 +32,17 @@ export function resolveChatGptWebCodexMcpEntry(rootDir = root, exists = existsSy
return candidates.find((candidate) => exists(candidate)) ?? null;
}
export async function loadChatGptWebCodexMcpModule(entry) {
if (entry.endsWith(".ts")) {
await import("tsx/esm");
}
return import(pathToFileURL(entry).href);
}
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");
const module = await loadChatGptWebCodexMcpModule(entry);
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 });
}

View File

@@ -1 +0,0 @@
- **perf(sse):** defer `cloneLogPayload()` in the structured SSE collector until after the `maxEvents`/`maxBytes` cap check, eliminating ~9,800 wasted `structuredClone` calls per streaming response (6571% faster `push()`). Reducer snapshot isolation restored for OpenAI and Responses summaries ([#12241](https://github.com/diegosouzapw/OmniRoute/pull/12241)) — thanks @PauloHSOliveira

View File

@@ -1 +0,0 @@
- New `/dashboard/orchestration` page: live unified view of everything running — Cloud Agent, A2A and Conductor as a real-time graph (Agents tab), the combo cascade (Routing tab, reusing the Combo Live Studio) and a state kanban (Overview tab), with a detail drawer (trace, cost, approve/cancel). Read-only over existing APIs — no new backend. Canvas concept credit: PR #11815 design

View File

@@ -1 +0,0 @@
- **feat(providers):** add RPD (Requests Per Day) limit to provider rate limit overrides across UI, schemas, DB, and i18n ([#PR_NUMBER](https://github.com/diegosouzapw/OmniRoute/pull/PR_NUMBER))

View File

@@ -1 +0,0 @@
- **fix(translator):** the leading `system` message now reaches Responses-API upstreams when its `content` is a content-part array — it was read as `typeof content === "string" ? content : ""`, so a prompt-caching client (Anthropic `cache_control`, the shape LiteLLM and the Anthropic SDK emit) had its entire system prompt replaced by an empty `instructions`. The request was still accepted with a normal `prompt_tokens` count, so the model answered with no instructions and nothing in the response said they were missing. Mid-conversation system turns already handled the array shape ([#7056](https://github.com/diegosouzapw/OmniRoute/pull/7056)); only the first one did not ([#12206](https://github.com/diegosouzapw/OmniRoute/issues/12206)). Regression guard: `tests/unit/translator-openai-responses-system-content-parts.test.ts`.

View File

@@ -1 +0,0 @@
- **fix(oauth):** Keep a Claude personal workspace and a Team organization as separate connections — they share the same email and `accountUUID`, so the email-only OAuth dedup let the second login overwrite the first account's tokens; `organizationUUID` now disambiguates them, the way `workspaceId` does for Codex ([#12222](https://github.com/diegosouzapw/OmniRoute/pull/12222))

View File

@@ -1 +0,0 @@
- **fix(sse):** trust `finish_reason: "length"`/`"max_tokens"` over the reasoning-consumed-token ratio in response quality validation, so a reasoning model truncated below the old 90% threshold correctly fails and retries instead of returning empty content as a silent "success" ([#12262](https://github.com/diegosouzapw/OmniRoute/pull/12262))

View File

@@ -1 +0,0 @@
- **fix(combo):** return non-retryable HTTP 400 when all candidates for a pinned native Codex turn are unavailable due to model-scoped lockout, terminating the turn cleanly while preserving turn continuity and enabling standard Combo routing on subsequent turns

View File

@@ -1 +0,0 @@
- **chore(stealth):** replace the `tls-client-node` sidecar/temp-file transport used by the six web-cookie providers with the exactly pinned `wreq-js` 3.0.0 native transport, preserving streaming, proxy isolation, deadlines, EOF policies, binary responses, and cancellation while removing the obsolete downloader and native repair path ([#11753](https://github.com/diegosouzapw/OmniRoute/pull/11753)).

View File

@@ -1 +0,0 @@
- **docs(free-tier):** declare the counting vs deciding regimes for "is it free?" and guard the deciding path from DB-backed catalog resolution ([#12226](https://github.com/diegosouzapw/OmniRoute/pull/12226))

View File

@@ -74,6 +74,12 @@
"justification": "CC-BY-4.0 applies to the caniuse browser-support data (a dataset, not code). The Creative Commons Attribution license requires attribution when distributing — OmniRoute does not distribute caniuse-lite data directly to end users; it is consumed by browserslist/PostCSS at build time to generate CSS compatibility info. This is a widely accepted pattern in the Node.js ecosystem (caniuse-lite is in millions of projects). Attribution is satisfied by keeping the package in node_modules with its original license file.",
"risk": "low",
"reviewAt": "v4.0.0"
},
"tls-client-node": {
"license": "Custom: LICENSE (Apache-2.0 + Commons Clause)",
"justification": "TODO: revisar — tls-client-node uses Apache-2.0 with a 'Commons Clause' addendum that restricts 'Selling' the software (i.e., offering it as a hosted/commercial service whose value derives substantially from tls-client-node). OmniRoute is an open-source proxy; however if deployed as a paid SaaS/hosting service, this restriction could apply. The package is used by grokTlsClient.ts for Grok TLS fingerprinting. RISK: medium — legal review recommended before commercial deployment. Alternatives: consider replacing with a native TLS fingerprinting approach or a truly permissive library.",
"risk": "medium",
"reviewAt": "v3.9.0"
}
}
}

View File

@@ -13,7 +13,6 @@
"@dnd-kit/core",
"@dnd-kit/sortable",
"@dnd-kit/utilities",
"@eslint/compat",
"@huggingface/transformers",
"@lobehub/icons",
"@modelcontextprotocol/sdk",
@@ -41,8 +40,6 @@
"@types/ws",
"@vitejs/plugin-react",
"@xyflow/react",
"ajv",
"ajv-formats",
"axios",
"bcryptjs",
"better-sqlite3",
@@ -66,7 +63,6 @@
"eslint-config-next",
"eslint-plugin-react-hooks",
"eslint-plugin-sonarjs",
"espree",
"express",
"fast-check",
"fetch-socks",
@@ -117,7 +113,6 @@
"pino-abstract-transport",
"pino-pretty",
"playwright",
"playwright-core",
"playwright-ctrf-json-reporter",
"prettier",
"promptfoo",
@@ -138,7 +133,7 @@
"sqlite-vec",
"tailwind-merge",
"tailwindcss",
"tiktoken",
"tls-client-node",
"tsup",
"tsx",
"turndown",

View File

@@ -297,6 +297,11 @@
"count": 1
}
},
"open-sse/handlers/videoGeneration/openai.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"open-sse/mcp-server/__tests__/a2aLifecycle.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -801,11 +806,26 @@
"count": 1
}
},
"open-sse/utils/streamPayloadCollector.ts": {
"no-restricted-syntax": {
"count": 1
}
},
"open-sse/utils/usageTracking.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/environment.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/HomePageClient.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 2
@@ -814,6 +834,14 @@
"src/app/(dashboard)/dashboard/a2a/page.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 6
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/acp-agents/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx": {
@@ -826,11 +854,26 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/batch/components/wizard/CostEstimateStep.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/batch/components/wizard/InputStep.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/batch/components/wizard/JsonlValidationStep.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/batch/files/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/cli-agents/CliAgentsPageClient.tsx": {
"no-restricted-syntax": {
"count": 4
@@ -866,6 +909,21 @@
"count": 6
}
},
"src/app/(dashboard)/dashboard/conductor/ConductorPageClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/conductor/FaroChat.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/conversations/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -886,9 +944,32 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/discovery/DiscoveryPageClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/free-provider-rankings/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/health/ProviderHealthAutopilotCard.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/health/ProviderHealthMatrixCard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/health/TelemetryCard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/health/page.tsx": {
@@ -896,9 +977,17 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/log-export/LogExportPageClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/mcp/page.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 2
},
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/dashboard/onboarding/page.tsx": {
@@ -906,11 +995,26 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": {
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/playground/components/tabs/CompareTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/plugins/[name]/config/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/plugins/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 2
@@ -986,6 +1090,11 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/resilience/connections/components/ConnectionDetail.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -996,6 +1105,19 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/resilience/connections/components/ResilienceConnectionsClient.tsx": {
"react-hooks/purity": {
"count": 1
},
"react-hooks/refs": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/runtime/components/ModelCooldownsCard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/search-tools/components/SearchHistory.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1041,6 +1163,11 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/ModelAliasesTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1066,16 +1193,41 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/SessionInfoCard.tsx": {
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/settings/components/useProxyPoolModal.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentList.tsx": {
"no-restricted-syntax": {
"count": 3
}
},
"src/app/(dashboard)/dashboard/tools/agent-bridge/components/ModelSelectorModal.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/tools/agent-bridge/components/SetupWizard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CustomHostsManager.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/tools/traffic-inspector/components/RequestStreamingList.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1086,6 +1238,11 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/translator/components/MonitorTab.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/translator/components/ResultNarrated.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1101,6 +1258,11 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 3
}
},
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1121,11 +1283,26 @@
"count": 1
}
},
"src/app/(dashboard)/dashboard/webhooks/WebhooksPageClient.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/webhooks/__tests__/webhook-wizard.test.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/webhooks/components/AddWebhookWizard.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/(dashboard)/dashboard/webhooks/components/WebhookDeliveriesPanel.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/api/assess/route.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1384,11 +1561,26 @@
"count": 1
}
},
"src/app/docs/components/FeedbackWidget.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/app/global-error.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/app/login/page.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 3
}
},
"src/app/status/page.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/domain/assessment/assessor.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1417,6 +1609,9 @@
"src/hooks/useLiveDashboard.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
},
"react-hooks/exhaustive-deps": {
"count": 2
}
},
"src/lib/a2a/skills/healthReport.ts": {
@@ -1489,6 +1684,11 @@
"count": 2
}
},
"src/lib/credentialHealth/scheduler.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/lib/db/apiKeys.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 3
@@ -1940,6 +2140,11 @@
"count": 3
}
},
"src/shared/components/PricingModal.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/shared/components/ProxyLogDetail.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1973,6 +2178,26 @@
"count": 1
}
},
"src/shared/components/docs/CodeBlock.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/shared/components/docs/DocsBreadcrumbs.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/shared/components/docs/DocsSidebar.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
}
},
"src/shared/components/docs/DocsThemeProvider.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 3
}
},
"src/shared/constants/agentSkills.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -1988,6 +2213,11 @@
"count": 1
}
},
"src/shared/hooks/cli/useToolBatchStatuses.ts": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/shared/hooks/useTheme.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -3070,6 +3300,9 @@
"tests/unit/cli-nodes-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 15
},
"@typescript-eslint/no-unused-vars": {
"count": 2
}
},
"tests/unit/cli-oauth-commands.test.ts": {
@@ -5053,6 +5286,11 @@
"count": 1
}
},
"tests/unit/stream-payload-collector.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
}
},
"tests/unit/stream-prompt-tokens-zero-upstream.test.ts": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -5197,6 +5435,11 @@
"count": 5
}
},
"tests/unit/translator-antigravity-to-openai.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 4
}
},
"tests/unit/translator-claude-helper-thinking.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 21

View File

@@ -1,7 +1,5 @@
{
"_rebaseline_2026_08_31_chatgpt_web_v4_vendor": "Pinned MIT vendor refresh from codex-chatgpt-web 0.1.16 to v4.0.6 (commit 09877fa21ffdbf20979623ef501046fc02a750d7). browser-worker.ts is preserved as the reviewed upstream browser protocol implementation; splitting the vendored file would destroy source parity and make future security/liveness updates unauditable. OmniRoute-specific DATA_DIR, Docker CDP, credential-marker, and XML decoding adaptations are covered by the ChatGPT Web Codex focused suite.",
"_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).",
"_rebaseline_2026_08_31_12212_openapi_generated": "PR #12212 (docs audit follow-up nº 3): src/app/docs/lib/openapi.generated.ts 171->1347 — the module is emitted by scripts/docs/gen-openapi-module.mjs from docs/openapi.yaml, and the spec now documents all 692 implemented routes (was 276), so the generated output grew with the spec. Frozen at the generator output size; shrink by slimming the spec, never by hand-editing the generated module. Covered by tests/unit/openapi-security-tiers.test.ts (6/6) and the check:api-docs-refs gate (692/692 paths with a real route).",
"_rebaseline_2026_08_21_10987_logfare_provider": "PR #10987 (jonlwheat2-gif, feat/10644-logfare-provider, closes #10644) own growth: src/shared/constants/providers/apikey/gateways.ts 1298->1321 (+23, the logfare APIKEY_PROVIDERS_GATEWAYS catalog entry with Free badge/freeNote/apiHint documenting the request-logging policy, additive data at the existing registry chokepoint, same god-file no-split rationale as the prior gateways.ts rebaselines: #10531 freebuff, merge-storm 2026-08-11). Covered by tests/unit/logfare-registry.test.ts (1/1 passing).",
"_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.",
"_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).",
@@ -461,7 +459,6 @@
"src/shared/components/ModelSelectModal.tsx": 1366,
"src/shared/constants/providers/apikey/gateways.ts": 1618,
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1665,
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": 4410,
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web (Codex) provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1287,
"_rebaseline_2026_08_21_11034_effort_variants": "DRIFT do tip (base-red #9985): modelCapabilities.ts 1016->1072 (+56) acumulado por PRs ja mergeadas no release/v3.8.50 — principalmente #11034 (resolve effort-variant capabilities a partir do modelo base), alem de #10963/#11040/#10987 growth dos catalogos. Tip puro ficou vermelho neste gate; rebaseline no tip por push direto (owner pre-autorizou crescimento legitimo). Nao tocou no arquivo da #11038.",
@@ -498,8 +495,7 @@
"src/shared/components/ModelSelectModal.tsx": 1138,
"src/shared/constants/providers/apikey/gateways.ts": 1250
},
"open-sse/executors/commandCode.ts": 1271,
"src/app/docs/lib/openapi.generated.ts": 1347
"open-sse/executors/commandCode.ts": 1271
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",

View File

@@ -104,9 +104,8 @@
"_rebaseline_2026_07_28_v3849_release": "75.5 -> 99 (+23.5). Aperto EXIGIDO pelo modo --require-tighten do ratchet: a métrica melhorou de verdade no ciclo v3.8.49. A causa é o workflow assíncrono de tradução, que finalmente alcançou o denominador em EN — as rebaselines anteriores (v3.8.39/.44/.47) foram todas afrouxamentos registrando o atraso das traduções, e agora ele foi pago. O coletor SUBTRAI os placeholders (present - placeholder em scripts/quality/collect-metrics.mjs), então os 317 marcadores __MISSING__ que esta release introduziu para o drift de valor já estão descontados dos 99 — o número é honesto, não inflado por placeholder. Medido pelo collect-metrics do CI no run 30404226939."
},
"deadExports": {
"value": 377,
"value": 500,
"direction": "down",
"_tighten_2026_09_01_pr_11950_rebase": "500 -> 377. Measured on PR #11950 after rebasing its unreachable-code cleanup onto release/v3.8.51 tip 4bcd8cee99a0: DEAD_EXPORTS=347 + DEAD_FILES=30 = DEAD_TOTAL=377 via node scripts/check/check-dead-code.mjs.",
"_rebaseline_2026_08_19_v3850_basereds_9985": "415 -> 416. Measured on release/v3.8.50 tip 14a480453 during the #9985 base-red drain. Removed the 2 genuinely-dead symbols traced to a specific recent change (PR #10148, 2026-08-18): the unused src/lib/quota/providerCapabilities.ts file and the unused ProviderQuotaMonitor interface in providerQuotaTelemetry.ts (418 -> 416). The remaining +1 could not be attributed to a single recent commit after checking every dead-list entry touched since the 2026-08-14 baseline measurement (most are pre-existing debt on files edited for unrelated reasons); rebaselining the residual 1 rather than guessing at removals. Structural cleanup stays tracked in #3501.",
"_rebaseline_2026_08_09_v3850_post_sweep": "227 -> 230. Measured by npm run check:dead-code on the unmodified release/v3.8.50 tip 382449d593 during the mandatory --full-ci pre-flight. The +3 is inherited cycle drift from the authorized merge sweep; this repair adds no production exports. Rebaseline records the actual tip so ci.yml quality-gate can run, while structural cleanup remains separate debt.",
"_rebaseline_2026_07_01_v3843_release": "225->227 (+2). v3.8.43 cycle drift, surfaced in the Quality Ratchet job after eslintWarnings was rebaselined (check:dead-code runs there). 227 = measured by check:dead-code (knip) on the release tip 4635076eb. The 5 CI fixes add 0 dead exports: safeHttpHref in linkify.ts is module-local AND used (called by linkifyText); no new exports; test files are not scanned. Tighten via --update next cycle.",

View File

@@ -1,7 +1,10 @@
{
"_comment": "Catraca de test-discovery (check-test-discovery.mjs). Cada entrada e um arquivo de teste que NENHUM runner coleta (ele nunca roda) — divida congelada na auditoria 6A.1 (2026-06-09; 195 originais, 135 religados no node runner em 6A.1c). So pode DIMINUIR: religue o teste (ajustando o glob do runner ou movendo o arquivo) e remova a entrada via --update. NAO adicione novos orfaos — corrija o runner.",
"_remaining_10": "10 orfaos restantes: 4 golden-set + 1 benchmark + 1 teste live + 1 stress (deliberadamente manuais — decidir runner/gating); 3 integration/services (gated RUN_SERVICES_INT=1, sem runner CI).",
"_remaining_13": "13 orfaos restantes: 2 testes de API em settings + 1 snapshot de quota do DB; 4 golden-set + 1 benchmark + 1 teste live + 1 stress (deliberadamente manuais — decidir runner/gating); 3 integration/services (gated RUN_SERVICES_INT=1, sem runner CI).",
"orphans": [
"src/app/api/settings/__tests__/memory.test.ts",
"src/app/api/settings/__tests__/settings.test.ts",
"src/lib/db/__tests__/quotaSnapshots.test.ts",
"tests/benchmarks/pipeline-accuracy.test.ts",
"tests/golden-set/compression-caveman-v2.test.ts",
"tests/golden-set/compression-quality.test.ts",

View File

@@ -1,44 +0,0 @@
{
"package": "wreq-js",
"version": "3.0.0",
"source": "https://registry.npmjs.org/wreq-js/-/wreq-js-3.0.0.tgz",
"npmIntegrity": "sha512-RZCoRSevVPpH4A4B4MxbFGo/pVPFveWd2gbe4ENKpPWlKXEYklZSDESOjBMmrIsmnkHh+nhM4PNJvG+NL7wBPA==",
"license": "MIT",
"nativeAddons": [
{
"path": "rust/wreq-js.darwin-arm64.node",
"size": 7690880,
"sha256": "c82eec39df691adb94f2cd09a8ff51335de8587cf132cd8b3ec797469a4b5002"
},
{
"path": "rust/wreq-js.darwin-x64.node",
"size": 8192028,
"sha256": "073b8a8a4c26aedbce7c14eef3e5567918e62e8dbf4d28296b23f9d2beec2981"
},
{
"path": "rust/wreq-js.linux-arm64-gnu.node",
"size": 8520824,
"sha256": "861d96a78caf7ce02c9ae8d37f1c59f5b0480e3142775c32917fcfe9b88524b0"
},
{
"path": "rust/wreq-js.linux-arm64-musl.node",
"size": 8735472,
"sha256": "2409a3578c8c440df419b4d5abe3ac149bec48881611a6dc1571b95e6246552d"
},
{
"path": "rust/wreq-js.linux-x64-gnu.node",
"size": 9048992,
"sha256": "55b40f4602c52111dfcdcc93db83f9d0de55d0ef7540348757709d58d05a9b64"
},
{
"path": "rust/wreq-js.linux-x64-musl.node",
"size": 8974880,
"sha256": "bd52d15b1bb4704b11561a8aa95648a6c91150082b5af0e39dd1608b7db2d317"
},
{
"path": "rust/wreq-js.win32-x64-msvc.node",
"size": 7967232,
"sha256": "7451a8701b82c946b03ba2be2f15257260a250b9e0ed9910611b22564fbec7a9"
}
]
}

View File

@@ -7,4 +7,4 @@ USER pwuser
EXPOSE 9223
CMD ["/bin/sh", "-lc", "node /opt/cdp-proxy.mjs & chrome_path=$(find /ms-playwright -path '*/chrome-linux/chrome' -type f | head -n 1); test -n \"$chrome_path\"; exec xvfb-run -a --server-args='-screen 0 1920x1080x24 -nolisten tcp' \"$chrome_path\" --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222 --user-data-dir=/browser-profile about:blank"]
CMD ["/bin/sh", "-lc", "node /opt/cdp-proxy.mjs & exec $(find /ms-playwright -path '*/chrome-linux/chrome' -type f | head -n 1) --headless=new --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222 --user-data-dir=/browser-profile about:blank"]

View File

@@ -30,7 +30,6 @@ Simple guides for using OmniRoute — no technical background needed.
- [USER_GUIDE.md](guides/USER_GUIDE.md) — daily usage of the dashboard and API.
- [THINKING_BUDGET.md](guides/THINKING_BUDGET.md) — thinking/reasoning budget modes (passthrough vs auto-strip).
- [FEATURES.md](guides/FEATURES.md) — dashboard feature gallery.
- [CHAOS-MODE.md](guides/CHAOS-MODE.md) — multi-model parallel/collaborative execution (setup, permissions, API).
- [TIERS.md](guides/TIERS.md) — OmniRoute tiers explained (user guide).
- [USAGE_QUOTA_GUIDE.md](guides/USAGE_QUOTA_GUIDE.md) — usage, quota & spend tracking.
- [COST_TRACKING.md](guides/COST_TRACKING.md) — cost and spend tracking.

View File

@@ -17,7 +17,7 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
Core capabilities:
- OpenAI-compatible API surface for CLI/tools (352 providers, 106 executors)
- OpenAI-compatible API surface for CLI/tools (352 providers, 104 executors)
- Request/response translation across provider formats
- Model combo fallback (multi-model sequence)
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`

View File

@@ -315,6 +315,7 @@ Top-level files in `src/lib/`:
- The old `localDb.ts` barrel was removed — consumers import specific `src/lib/db/*` modules directly.
- `proxyHealth.ts`, `proxyLogger.ts`, `tokenHealthCheck.ts`, `localHealthCheck.ts`
- `oneproxyRotator.ts`, `oneproxySync.ts`
- `apiBridgeServer.ts`, `cacheLayer.ts`, `semanticCache.ts`, `settingsCache.ts`
- `cloudSync.ts`, `initCloudSync.ts`
- `cloudflaredTunnel.ts`, `ngrokTunnel.ts`, `tailscaleTunnel.ts`
@@ -421,12 +422,12 @@ Split into focused subdirectories:
`bodySize.ts`, `colors.ts`, `appConfig.ts`, `config.ts`,
`sidebarVisibility.ts`, `visionBridgeDefaults.ts`.
- `validation/``schemas.ts` (~80 Zod schemas), `compressionConfigSchemas.ts`,
`providerSchema.ts`, `settingsSchemas.ts`, `helpers.ts`.
`oneproxySchemas.ts`, `providerSchema.ts`, `settingsSchemas.ts`, `helpers.ts`.
- `contracts/` — public API contracts shipped to npm.
- `types/` — shared TS types.
- `utils/``circuitBreaker.ts`, `apiAuth.ts`, `apiKey.ts`, `apiKeyPolicy.ts`,
`api.ts`, `classify429.ts`, `cliCompat.ts`, `clipboard.ts`, `cloud.ts`, `cn.ts`,
`cors.ts`, `featureFlags.ts`,
`apiResponse.ts`, `api.ts`, `classify429.ts`, `cliCompat.ts`, `clipboard.ts`,
`cloud.ts`, `cn.ts`, `cors.ts`, `costEstimator.ts`, `featureFlags.ts`,
`fetchTimeout.ts`, `formatting.ts`, `inputSanitizer.ts`, `logger.ts`,
`machine.ts`, `machineId.ts`, `maskEmail.ts`, `modelCatalogSearch.ts`,
`nodeRuntimeSupport.ts`, `parseApiKeys.ts`, `providerHints.ts`,
@@ -449,7 +450,7 @@ open-sse/
├── types.d.ts
├── config/ Provider registries, header profiles, identity, …
├── handlers/ Request handlers (chat, embeddings, audio, image, …)
├── executors/ 106 provider-specific HTTP executors
├── executors/ 104 provider-specific HTTP executors
├── translator/ Format conversion (OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro)
├── transformer/ Responses API ↔ Chat Completions stream transformer
├── services/ 80+ service modules (combos, fallback, quotas, identity, …)
@@ -479,7 +480,7 @@ open-sse/
### 4.2 `open-sse/executors/`
106 provider executors, each extending `BaseExecutor` (`base.ts`):
104 provider executors, each extending `BaseExecutor` (`base.ts`):
`antigravity`, `azure-openai`, `blackbox-web`, `cliproxyapi`,
`chatgpt-web-codex`, `cloudflare-ai`, `codex`, `commandCode`, `cursor`, `default`, `devin-cli`,

View File

@@ -201,6 +201,7 @@ src/
| `playground/` | Playground Studio shared helpers: `codeExport.ts` (curl/Python/TS generator), `promptImprover.ts` (meta-prompt builder), `streamMetrics.ts` (pure TTFT/TPS), `types.ts` (pricing table) — see `docs/frameworks/PLAYGROUND_STUDIO.md` |
| `webhookDispatcher.ts` | HMAC webhook delivery — see `docs/frameworks/WEBHOOKS.md` |
| `cloudflaredTunnel.ts`, `ngrokTunnel.ts` | Tunnel managers — see `docs/ops/TUNNELS_GUIDE.md` |
| `oneproxySync.ts`, `oneproxyRotator.ts` | 1proxy free proxy marketplace — see `docs/ops/PROXY_GUIDE.md` |
| `cloudSync.ts`, `initCloudSync.ts` | Optional cloud sync of state |
| `localDb.ts` | Re-export barrel for db modules (no logic — re-exports only) |
| `cacheLayer.ts`, `idempotencyLayer.ts` | Request caching + idempotency |

View File

@@ -50,25 +50,6 @@ OmniRoute has three distinct but related resilience mechanisms. Each has a diffe
---
### Opt-in global Provider Cooldown (window gate)
A fourth, **opt-in** layer (`PROVIDER_COOLDOWN_ENABLED`, default **off**) keeps a
cross-request memory of failing providers in
`open-sse/services/providerCooldownTracker.ts`, consulted by combo target
resolution so consecutive combo requests stop re-walking a provider that just
failed. Provider-level entries honor the `PROVIDER_PROFILES` window gate:
| Profile | trips after (`providerFailureThreshold`) | inside (`providerFailureWindowMs`) | cools for (`providerCooldownMs`) |
| ------- | ---------------------------------------: | ---------------------------------: | -------------------------------: |
| OAuth | `10` | `15min` | `5min` |
| API key | `15` | `30min` | `10min` |
Below the threshold the provider is **not** considered cooling; a success clears
the window. Connection-level entries (`provider:connectionId`) keep the
exponential `minRetryCooldownMs → maxRetryCooldownMs` backoff instead. Overrides:
`OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}`.
Regression guard: `tests/unit/provider-cooldown-window-gate.test.ts`.
## 2. Connection Cooldown
**Scope:** single provider connection/account/key.

View File

@@ -182,6 +182,22 @@ With Stacked: 10K-2.5K tokens sent (78-95% eligible RTK+Caveman range
---
## Output Styles
Output styles inject a system prompt instruction to steer the model's writing style. They are defined in the output style catalog and support multiple languages and intensity levels (`lite`, `full`, `ultra`).
| Style | Description | Supported Languages | Levels |
| --- | --- | --- | --- |
| `terse-prose` | Drop filler/articles/hedging; keep technical substance exact. | `en`, `pt-BR`, `ja`, `id`, `vi` | `lite`, `full`, `ultra` |
| `less-code` | YAGNI ladder: smallest working change, no unrequested abstractions. | `en`, `pt-BR`, `vi`, `ja`, `id` | `lite`, `full`, `ultra` |
| `ponytail` | Lazy senior-dev discipline: climb the YAGNI ladder, fix root cause, smallest working diff. | `en`, `pt-BR`, `vi`, `ja`, `id` | `lite`, `full`, `ultra` |
| `i-have-adhd` | Action-first output: next action leads, steps numbered, one concrete next step, no preamble. | `en`, `pt-BR`, `vi`, `ja`, `id` | `lite`, `full`, `ultra` |
| `terse-cjk` | Classical-Chinese ultra-terse style (locale-gated to zh). | `zh` | `lite`, `full`, `ultra` |
Each level appends a shared boundary clause ensuring that code blocks, URLs, file paths, commands, and identifiers remain verbatim.
---
## Configuration
### Dashboard
@@ -456,11 +472,11 @@ together and are injected in catalog order.
| Style | `id` | What it does | Instruction languages |
| --- | --- | --- | --- |
| Terse prose | `terse-prose` | Drop filler/articles/hedging; keep technical substance exact. Same text as the legacy caveman output mode (referenced, not re-typed). | en, pt-BR, es, de, fr, it, ru, zh, ja, id, vi |
| Less code | `less-code` | YAGNI ladder: smallest working change, no unrequested abstractions. | en, pt-BR, es, de, fr, it, ru, zh, ja, id, vi |
| Ponytail (lazy senior dev) | `ponytail` | "The best code is the code never written": reuse > rewrite, root cause > symptom, shortest working diff. | en, pt-BR, es, de, fr, it, ru, zh, ja, id, vi |
| I have ADHD (action-first) | `i-have-adhd` | Action first (command/path/snippet before prose), numbered bounded steps, ONE concrete next step, no preamble/recap/closers. Adapted from [ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd) (MIT). | en, pt-BR, es, de, fr, it, ru, zh, ja, id, vi |
| Terse CJK (文言) | `terse-cjk` | Classical-Chinese ultra-terse style. | zh (locale-gated: only offered when the resolved language is `zh`) |
| Terse prose | `terse-prose` | Drop filler/articles/hedging; keep technical substance exact. Same text as the legacy caveman output mode (referenced, not re-typed). | en, pt-BR, ja, id |
| Less code | `less-code` | YAGNI ladder: smallest working change, no unrequested abstractions. | en only (backlog: [#10426](https://github.com/diegosouzapw/OmniRoute/issues/10426)) |
| Ponytail (lazy senior dev) | `ponytail` | "The best code is the code never written": reuse > rewrite, root cause > symptom, shortest working diff. | en, pt-BR, vi, ja, id |
| I have ADHD (action-first) | `i-have-adhd` | Action first (command/path/snippet before prose), numbered bounded steps, ONE concrete next step, no preamble/recap/closers. Adapted from [ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd) (MIT). | en, pt-BR, vi, ja, id |
| Terse CJK (文言) | `terse-cjk` | Classical-Chinese ultra-terse style. | zh (locale-gated: only offered when the detected language is `zh`) |
Every style ships three intensity levels — `lite`, `full`, `ultra` — and every level
ends with the shared boundaries clause, which keeps code blocks, file paths, commands,
@@ -492,11 +508,7 @@ the selection as:
```
Back-compat: the legacy `outputMode: "caveman"` combo setting still works and maps to
`terse-prose`, byte-identical to the old injection in every legacy language.
Language selection: with `languageConfig.enabled` on, `autoDetect` picks the
language of the latest user message (same detector as the input engines);
turning `autoDetect` off pins `defaultLanguage`. Off → English.
`terse-prose`, byte-identical to the old injection in all four legacy languages.
The style × language matrix is pinned by
`tests/unit/compression/output-styles-i18n-matrix.test.ts`: a new style cannot ship

View File

@@ -583,7 +583,7 @@ persistence and telemetry all enumerate the catalog — there is no other list t
The instruction text must be **static and deterministic** per
`(id, level, language)` — `${SHARED_BOUNDARIES}` is the only interpolation allowed.
2. **Translate it.** Ship at least a `pt-BR` block under `i18n`; `ponytail` and
`i-have-adhd` (en, pt-BR, es, de, fr, it, ru, zh, ja, id, vi) are the reference shape. A deliberately
`i-have-adhd` (en, pt-BR, vi, ja, id) are the reference shape. A deliberately
single-language style sets `locale` instead (like `terse-cjk` → `zh`) and is then
only offered under that locale.
3. **Update the matrix guard** — add the style's languages to `BASELINE_LANGUAGES` in

View File

@@ -13,10 +13,10 @@ Mermaid sources (`.mmd`) and exported SVGs for OmniRoute v3.8.0 architecture flo
| Source | Exported | Used in |
| ---------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ |
| [request-pipeline.mmd](./request-pipeline.mmd) | [SVG](./exported/request-pipeline.svg) | docs/architecture/ARCHITECTURE.md, docs/architecture/CODEBASE_DOCUMENTATION.md |
| [auto-combo-scoring.mmd](./auto-combo-scoring.mmd) | [SVG](./exported/auto-combo-scoring.svg) | docs/routing/AUTO-COMBO.md |
| [auto-combo-12factor.mmd](./auto-combo-12factor.mmd) | [SVG](./exported/auto-combo-12factor.svg) | docs/routing/AUTO-COMBO.md |
| [resilience-3layers.mmd](./resilience-3layers.mmd) | [SVG](./exported/resilience-3layers.svg) | docs/architecture/RESILIENCE_GUIDE.md, CLAUDE.md |
| [i18n-flow.mmd](./i18n-flow.mmd) | [SVG](./exported/i18n-flow.svg) | docs/guides/I18N.md |
| [mcp-tools.mmd](./mcp-tools.mmd) | [SVG](./exported/mcp-tools.svg) | docs/frameworks/MCP-SERVER.md |
| [mcp-tools-107.mmd](./mcp-tools-107.mmd) | [SVG](./exported/mcp-tools-107.svg) | docs/frameworks/MCP-SERVER.md |
| [cloud-agent-flow.mmd](./cloud-agent-flow.mmd) | [SVG](./exported/cloud-agent-flow.svg) | docs/frameworks/CLOUD_AGENT.md |
| [authz-pipeline.mmd](./authz-pipeline.mmd) | [SVG](./exported/authz-pipeline.svg) | docs/architecture/AUTHZ_GUIDE.md |
| [db-schema-overview.mmd](./db-schema-overview.mmd) | [SVG](./exported/db-schema-overview.svg) | docs/architecture/CODEBASE_DOCUMENTATION.md |

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -59,11 +59,8 @@ infrastructure and settings. Three tiers exist, applied in priority order:
```
┌─────────────────────────────────────────────────────────────┐
│ TIER 0 — Keyword (FTS5) │
Probe-driven availability: FTS5 when the SQLite build
supports it (better-sqlite3 / node:sqlite / bun:sqlite);
│ unavailable on FTS5-less builds (e.g. sql.js/WASM — │
│ "no such module: fts5"). Used when strategy = "exact" or │
│ as fallback; engine-status keyword reflects the probe. │
Always available. SQLite FTS5 full-text search over
content + key. Used when strategy = "exact" or as fallback.
└──────────────────────────────────┬──────────────────────────┘
│ strategy = semantic|hybrid?

View File

@@ -1,109 +0,0 @@
---
title: "Chaos Mode"
version: 3.8.51
lastUpdated: 2026-09-01
---
# Chaos Mode
> **Dashboard:** **Chaos Mode** (sidebar) → `/dashboard/chaos`
> **API:** `GET` / `PUT` `/api/chaos/config` · `POST /api/chaos/run` (dashboard session) · `POST /api/skills/collect/chaos` (API key)
> **Source:** `src/lib/chaos/chaosExecutor.ts`, `src/lib/chaos/chaosConfig.ts`
Chaos Mode sends **one task to several providers at once** — every participating provider
contributes one model instance, and you get all the answers side by side (or chained). It is a
multi-model execution surface, not a routing strategy: your normal `/v1/chat/completions`
traffic is never affected by it.
**Disambiguation — three different things ship with "chaos" in the name:**
| Thing | What it is | Where documented |
| ------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **Chaos Mode** | The dashboard page + API described here: fan one task out to many providers (parallel or collaborative). | This guide |
| `auto/chaos` | An Auto-Combo model id with fault-injection scoring weights, for resilience testing. Nothing to configure. | [AUTO-COMBO.md](../routing/AUTO-COMBO.md) |
| Chaos combo config | A persisted combo with `config.chaos.enabled` fans out to a panel with an optional judge model (API-only). | `open-sse/services/autoCombo/chaosEngine.ts` |
## Setup
1. Open **Dashboard → Chaos Mode** (`/dashboard/chaos`).
2. Turn it **on** — Chaos Mode ships **disabled by default** (`enabled: false` in
`src/lib/chaos/chaosConfig.ts`). While disabled, `POST /api/chaos/run` answers
`400 — "Chaos Mode is not enabled. Enable it in Dashboard → Chaos Mode."`.
3. Pick the participants and defaults (persisted per instance via the settings store):
| Field | Meaning | Default / limits |
| ------------------- | ------------------------------------------------------------------- | --------------------------------------- |
| `enabled` | Master switch | `false` |
| `defaultMode` | `parallel` or `collaborative` (see below) | `parallel` |
| `providerOverrides` | Per-provider participation (`providerId`, optional `modelId`, `enabled`) | empty = every active provider, max 200 |
| `systemPrompt` | Override for the built-in Chaos system prompt | optional, max 10 000 chars |
| `timeoutMs` | Max time per model call | `120000` (5 000600 000) |
| `maxTokens` | `max_tokens` per model call | `4096` (256128 000) |
4. Run a **test from the page itself** — the results panel shows each provider's answer,
status and duration.
## Execution modes
- **`parallel`** — every model gets the same task simultaneously; you receive all answers
independently.
- **`collaborative`** — models run **in a chain**: each one sees the previous model's output and
is asked to refine, extend, critique or offer an alternative. The response's `summary` field
concatenates the successful outputs in chain order (parallel runs have no `summary`).
## API
### `POST /api/chaos/run` — dashboard session
Cookie-authenticated (the management session — see
[MANAGEMENT-AUTH.md](MANAGEMENT-AUTH.md)); used by the dashboard page.
```jsonc
// body
{
"task": "Compare approaches to X", // required
"providers": ["glm", "kimi"], // optional filter
"mode": "parallel", // optional — overrides defaultMode
"systemPrompt": "…", // optional override
"maxTokens": 4096 // optional override
}
```
### `POST /api/skills/collect/chaos` — API key
Bearer-token variant for external callers. The key must carry the **Chaos Mode permission**
(`chaosModeEnabled`), which is **off by default** — enable it per key in
**Dashboard → API Manager → edit key → permissions → Chaos Mode**. Same body as above.
```bash
curl -X POST http://localhost:20128/api/skills/collect/chaos \
-H "Authorization: Bearer $OMNIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"task":"Compare approaches to X","mode":"parallel"}'
```
Both endpoints return the same shape:
```jsonc
{
"task": "…",
"mode": "parallel",
"startedAt": "2026-09-01T00:00:00.000Z",
"totalProviders": 3,
"totalResults": 3,
"models": [
{ "providerId": "glm", "providerName": "GLM", "modelId": "glm-4.7",
"status": "success", "content": "…", "durationMs": 3210 }
],
"summary": "…" // collaborative mode only
}
```
## Troubleshooting
- **`400 Chaos Mode is not enabled`** — step 2 above: the global switch is off.
- **API key gets rejected on `/api/skills/collect/chaos`** — the key lacks the per-key
`chaosModeEnabled` permission (off by default; this is a setting, not an error).
- **A provider you expected is missing from the results** — check `providerOverrides` on the
Chaos Mode page (a disabled override excludes it) and whether the provider connection is
active.

View File

@@ -75,7 +75,7 @@ When you run `npm install -g omniroute`, you may see a wall of warnings like `np
The warnings come from stale peer-dependency ranges in third-party packages OmniRoute doesn't control:
1. **`marked-terminal` wants `marked >=1 <16`, found `marked@18`** — works fine in practice; the upstream peer range is just stale.
2. **`deprecated prebuild-install@7.1.3`** — a transitive native-binary helper used by another dependency. The pinned `wreq-js@3.0.0` package bundles its seven supported platform addons directly; this warning does not diagnose the web-cookie transport.
2. **`deprecated prebuild-install@7.1.3`** — the native-binary fetch helper. Only relevant later if a web-cookie provider reports a missing `tls-client-node` native binary (a separate issue, not caused by this warning).
**No action needed** — the warnings cannot be fully silenced without forking upstream packages.
@@ -148,10 +148,9 @@ desktop app, for example:
- `resources/app/.build/next/node_modules/playwright-<hash>/lib/…/agentParser.js` and
`workerProcessEntry.js` — [Playwright](https://playwright.dev), the browser-automation
library used for in-app provider login and browser-backed chat.
- `resources/app/.build/next/node_modules/wreq-js-<hash>/rust/wreq-js.win32-x64-msvc.node`
— the declared MIT-licensed native addon from pinned `wreq-js@3.0.0`, used for
browser-fingerprinted HTTP on some web providers. Its expected SHA-256 is recorded in
`config/release/wreq-js-native-manifest.json`.
- `resources/app/.build/next/node_modules/tls-client-node-<hash>/bin/tls-client-windows-64-<ver>.dll`
— the native binary from `tls-client-node`, used for Cloudflare-tolerant HTTP on some web
providers.
**Why it fires:** the Windows installer is **not yet code-signed**, so an unsigned NSIS
installer has zero reputation and behavioral heuristics run at maximum aggression. Combined

View File

@@ -8,7 +8,6 @@
"DOCKER_GUIDE",
"ELECTRON_GUIDE",
"FEATURES",
"CHAOS-MODE",
"FREE_PROVIDER_RANKINGS",
"COST_TRACKING",
"I18N",

View File

@@ -316,6 +316,7 @@ Pliki najwyższego poziomu w `src/lib/`:
- `localDb.ts` — wyłącznie warstwa re-export. **Nigdy** nie dodawaj tu logiki.
- `proxyHealth.ts`, `proxyLogger.ts`, `tokenHealthCheck.ts`, `localHealthCheck.ts`
- `oneproxyRotator.ts`, `oneproxySync.ts`
- `apiBridgeServer.ts`, `cacheLayer.ts`, `semanticCache.ts`, `settingsCache.ts`
- `cloudSync.ts`, `initCloudSync.ts`
- `cloudflaredTunnel.ts`, `ngrokTunnel.ts`, `tailscaleTunnel.ts`
@@ -422,7 +423,7 @@ Podzielone na skupione podkatalogi:
`bodySize.ts`, `colors.ts`, `appConfig.ts`, `config.ts`,
`sidebarVisibility.ts`, `visionBridgeDefaults.ts`.
- `validation/``schemas.ts` (~80 schematów Zod), `compressionConfigSchemas.ts`,
`providerSchema.ts`, `settingsSchemas.ts`, `helpers.ts`.
`oneproxySchemas.ts`, `providerSchema.ts`, `settingsSchemas.ts`, `helpers.ts`.
- `contracts/` — publiczne kontrakty API dostarczane do npm.
- `types/` — współdzielone typy TS.
- `utils/``circuitBreaker.ts`, `apiAuth.ts`, `apiKey.ts`, `apiKeyPolicy.ts`,

View File

@@ -202,6 +202,7 @@ src/
| `playground/` | Współdzielone helpery Playground Studio: `codeExport.ts` (generator curl/Python/TS), `promptImprover.ts` (builder meta-promptów), `streamMetrics.ts` (czyste TTFT/TPS), `types.ts` (tabela cen) — zob. `docs/frameworks/PLAYGROUND_STUDIO.md` |
| `webhookDispatcher.ts` | Dostarczanie webhooków HMAC — zob. `docs/frameworks/WEBHOOKS.md` |
| `cloudflaredTunnel.ts`, `ngrokTunnel.ts` | Managery tuneli — zob. `docs/ops/TUNNELS_GUIDE.md` |
| `oneproxySync.ts`, `oneproxyRotator.ts` | Marketplace darmowych proxy 1proxy — zob. `docs/ops/PROXY_GUIDE.md` |
| `cloudSync.ts`, `initCloudSync.ts` | Opcjonalna synchronizacja stanu w chmurze |
| `localDb.ts` | Barrel re-exportów modułów db (bez logiki — tylko re-eksporty) |
| `cacheLayer.ts`, `idempotencyLayer.ts` | Cache żądań + idempotencja |
@@ -244,7 +245,7 @@ src/
| -------------------------------- | ------------------------------------------------------------------------- |
| `constants/providers.ts` | **329 wpisów providerów** z walidacją Zod (źródło prawdy) |
| `constants/cliTools.ts` | Rejestr zewnętrznych narzędzi CLI |
| `constants/routingStrategies.ts` | **19 publicznych strategii routingu** z priorytetami |
| `constants/routingStrategies.ts` | **19 publicznych strategii routingu** z priorytetami |
| `constants/publicApiRoutes.ts` | Trasy wymagające auth Bearer (vs management) |
| `constants/upstreamHeaders.ts` | Denylist nagłówków dla żądań upstream |
| `validation/schemas.ts` | ~80 schematów Zod (jedno źródło prawdy dla kontraktów API) |

View File

@@ -79,6 +79,8 @@ Nawet poza zablokowanymi regionami proxy są przydatne do:
| **Settings Route** | `src/app/api/settings/proxy/route.ts` | Legacy API konfiguracji proxy (GET/PUT/DELETE) |
| **Management Route** | `src/app/api/v1/management/proxies/route.ts` | Registry CRUD API (GET/POST/PATCH/DELETE) |
| **1proxy DB** | `src/lib/db/oneproxy.ts` | Trwałość darmowego marketplace proxy |
| **1proxy Sync** | `src/lib/oneproxySync.ts` | Pobiera proxy z API 1proxy |
| **1proxy Rotator** | `src/lib/oneproxyRotator.ts` | Strategie rotacji (quality/random/sequential) |
---
@@ -503,9 +505,13 @@ Aby wystawić instancję OmniRoute do publicznego internetu (Cloudflare/ngrok/Ta
## Zmienne środowiskowe
| Zmienna | Domyślna | Opis |
| --------------------- | -------- | --------------------------------------------------------- |
| `ENABLE_SOCKS5_PROXY` | `true` | Włącza obsługę SOCKS5 (domyślnie `true` w `.env.example`) |
| Zmienna | Domyślna | Opis |
| -------------------------------- | ------------------------------------- | --------------------------------------------------------- |
| `ENABLE_SOCKS5_PROXY` | `true` | Włącza obsługę SOCKS5 (domyślnie `true` w `.env.example`) |
| `ONEPROXY_ENABLED` | `true` | Włącza integrację 1proxy |
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | Endpoint API 1proxy |
| `ONEPROXY_MAX_PROXIES` | `500` | Maks. liczba proxy do synchronizacji |
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | Minimalny quality score do importu |
---
@@ -766,6 +772,49 @@ Use `random`
evenly)
```
### Konfiguracja strategii rotacji
```ts
import { rotateOneproxyProxy } from "omniroute/oneproxyRotator";
// In a one-off script
const proxy = await rotateOneproxyProxy({ strategy: "quality" });
if (proxy) {
console.log(`Selected: ${proxy.host}:${proxy.port}, quality=${proxy.qualityScore}`);
}
```
### Reset indeksu sequential
Przy strategii `sequential` wewnętrzny indeks narasta. Aby zresetować:
```ts
import { resetSequentialIndex } from "omniroute/oneproxyRotator";
resetSequentialIndex();
```
Przydatne gdy:
- Restartujesz load test
- Odzyskujesz się po awarii proxy (żeby nie cyklonować najpierw martwych)
- Ręcznie rebalansujesz po dodaniu nowych proxy
### Oznaczanie proxy jako failed
Gdy proxy systematycznie pada, oznacz je ręcznie, by rotator je pomijał:
```ts
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
const removed = await failOneproxyProxy("203.0.113.7", 8080);
if (removed) {
console.log("Proxy marked as failed; rotator will skip it");
}
```
Proxy **nie jest usuwane** — jest oznaczane jako unhealthy i nie będzie wybierane do następnego udanego health check (przez `proxyHealth.ts`) lub ręcznego resetu.
---
> 📖 **Powiązana dokumentacja:**

View File

@@ -163,9 +163,9 @@ Helper detekcji żyje w `src/lib/combos/modelNameCollision.ts`.
Silnik Auto-Combo dynamicznie wybiera najlepszego providera/model dla każdego żądania przy użyciu **13-czynnikowej funkcji scoringu** (zdefiniowanej w `open-sse/services/autoCombo/scoring.ts``DEFAULT_WEIGHTS`). Wszystkie wagi sumują się do **1.0**.
![Auto-Combo 13-factor scoring](../diagrams/exported/auto-combo-scoring.svg)
![Auto-Combo 13-factor scoring](../diagrams/exported/auto-combo-12factor.svg)
> Źródło: [diagrams/auto-combo-scoring.mmd](../diagrams/auto-combo-scoring.mmd) (regeneruj przez `npm run docs:render-diagrams`). Historyczna nazwa pliku pochodzi sprzed dodania kolejnych czynników; bieżący diagram pokazuje wszystkie 13.
> Źródło: [diagrams/auto-combo-12factor.mmd](../diagrams/auto-combo-12factor.mmd) (regeneruj przez `npm run docs:render-diagrams`). Historyczna nazwa pliku pochodzi sprzed dodania kolejnych czynników; bieżący diagram pokazuje wszystkie 13.
| Czynnik | Domyślna waga | Opis |
| :-------------------- | :------------ | :------------------------------------------------------------------------------------------------------- |

View File

@@ -298,6 +298,7 @@ v1/
- `localDb.ts` — 仅作重新导出层。**切勿**在此添加逻辑。
- `proxyHealth.ts``proxyLogger.ts``tokenHealthCheck.ts``localHealthCheck.ts`
- `oneproxyRotator.ts``oneproxySync.ts`
- `apiBridgeServer.ts``cacheLayer.ts``semanticCache.ts``settingsCache.ts`
- `cloudSync.ts``initCloudSync.ts`
- `cloudflaredTunnel.ts``ngrokTunnel.ts``tailscaleTunnel.ts`
@@ -403,7 +404,7 @@ server/
`bodySize.ts``colors.ts``appConfig.ts``config.ts`
`sidebarVisibility.ts``visionBridgeDefaults.ts`
- `validation/``schemas.ts`(约 80 个 Zod Schema`compressionConfigSchemas.ts`
`providerSchema.ts``settingsSchemas.ts``helpers.ts`
`oneproxySchemas.ts``providerSchema.ts``settingsSchemas.ts``helpers.ts`
- `contracts/` — 发布到 npm 的公开 API 契约。
- `types/` — 共享 TS 类型。
- `utils/``circuitBreaker.ts``apiAuth.ts``apiKey.ts``apiKeyPolicy.ts`

View File

@@ -944,6 +944,10 @@ CLI_COMPAT_ALL=1
| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | 设为 `1` 可禁用上游 TLS 校验(仅限开发)。 |
| `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | 代理连接的空闲套接字超时(毫秒);超过该时间的空闲套接字会被拆除,避免泄露半打开隧道。 |
| `MITM_VERBOSE` | `1` | `src/mitm/server.cjs`, `src/mitm/_internal/bypass.cjs` | 路由决策日志详细程度:`0` 静默,值越大记录越多 bypass/路由决策。 |
| `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | 启用 1Proxy 出口池同步。 |
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy 服务 API URL 覆盖。 |
| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | 每次同步导入的最大代理数。 |
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | `src/lib/oneproxySync.ts` | 导入代理的最低质量分。 |
| `FREE_PROXY_1PROXY_ENABLED` | `true` | `src/lib/freeProxyProviders/oneproxy.ts` | 启用 1proxy 免费代理源。设为 `false` 可禁用。 |
| `FREE_PROXY_1PROXY_API_URL` | _(见 oneproxy.ts)_ | `src/lib/freeProxyProviders/oneproxy.ts` | 1proxy API URL 覆盖。 |
| `FREE_PROXY_1PROXY_MAX` | `500` | `src/lib/freeProxyProviders/oneproxy.ts` | 从 1proxy 每次同步获取的最大代理数。 |

View File

@@ -104,9 +104,9 @@ handleComboChat与持久化 Combo 相同的引擎)
Auto-Combo 引擎使用**13 因子评分函数**(定义在 `open-sse/services/autoCombo/scoring.ts``DEFAULT_WEIGHTS`)为每次请求动态选择最佳服务商/模型。所有权重之和为 **1.0**
![Auto-Combo 13-factor scoring](../diagrams/exported/auto-combo-scoring.svg)
![Auto-Combo 13-factor scoring](../diagrams/exported/auto-combo-12factor.svg)
> 来源:[diagrams/auto-combo-scoring.mmd](../diagrams/auto-combo-scoring.mmd)(通过 `npm run docs:render-diagrams` 重新生成)。文件名为历史名称;当前图表包含全部 13 个因子。
> 来源:[diagrams/auto-combo-12factor.mmd](../diagrams/auto-combo-12factor.mmd)(通过 `npm run docs:render-diagrams` 重新生成)。文件名为历史名称;当前图表包含全部 13 个因子。
| 因子 | 默认权重 | 描述 |
| :---------------------- | :------- | :--------------------------------------------------------------------------------------------- |

View File

@@ -311,6 +311,7 @@ v1/
- `localDb.ts` — 僅重新匯出層。**切勿**在此新增邏輯。
- `proxyHealth.ts``proxyLogger.ts``tokenHealthCheck.ts``localHealthCheck.ts`
- `oneproxyRotator.ts``oneproxySync.ts`
- `apiBridgeServer.ts``cacheLayer.ts``semanticCache.ts``settingsCache.ts`
- `cloudSync.ts``initCloudSync.ts`
- `cloudflaredTunnel.ts``ngrokTunnel.ts``tailscaleTunnel.ts`
@@ -417,7 +418,7 @@ server/
`bodySize.ts``colors.ts``appConfig.ts``config.ts`
`sidebarVisibility.ts``visionBridgeDefaults.ts`
- `validation/``schemas.ts`(約 80 個 Zod 架構)、`compressionConfigSchemas.ts`
`providerSchema.ts``settingsSchemas.ts``helpers.ts`
`oneproxySchemas.ts``providerSchema.ts``settingsSchemas.ts``helpers.ts`
- `contracts/` — 發布到 npm 的公開 API 合約。
- `types/` — 共用 TS 型別。
- `utils/``circuitBreaker.ts``apiAuth.ts``apiKey.ts``apiKeyPolicy.ts`

View File

@@ -116,9 +116,9 @@ handleComboChat與持久化組合使用相同引擎
自動組合引擎使用**13 因子評分函數**(定義於 `open-sse/services/autoCombo/scoring.ts``DEFAULT_WEIGHTS`)為每個請求動態選擇最佳的提供者/模型。所有權重合計為 **1.0**
![自動組合 13 因子評分](../diagrams/exported/auto-combo-scoring.svg)
![自動組合 13 因子評分](../diagrams/exported/auto-combo-12factor.svg)
> 來源:[diagrams/auto-combo-scoring.mmd](../diagrams/auto-combo-scoring.mmd)(可透過 `npm run docs:render-diagrams` 重新生成)。檔名是歷史名稱;目前圖表包含全部 13 個因子。
> 來源:[diagrams/auto-combo-12factor.mmd](../diagrams/auto-combo-12factor.mmd)(可透過 `npm run docs:render-diagrams` 重新生成)。檔名是歷史名稱;目前圖表包含全部 13 個因子。
| 因子 | 預設權重 | 說明 |
| :-------------------------------------- | :------- | :--------------------------------------------------------------------------- |

File diff suppressed because it is too large Load Diff

View File

@@ -79,6 +79,8 @@ Even outside blocked regions, proxies are useful for:
| **Settings Route** | `src/app/api/settings/proxy/route.ts` | Legacy proxy config API (GET/PUT/DELETE) |
| **Management Route** | `src/app/api/v1/management/proxies/route.ts` | Registry CRUD API (GET/POST/PATCH/DELETE) |
| **1proxy DB** | `src/lib/db/oneproxy.ts` | Free proxy marketplace persistence |
| **1proxy Sync** | `src/lib/oneproxySync.ts` | Fetches proxies from 1proxy API |
| **1proxy Rotator** | `src/lib/oneproxyRotator.ts` | Rotation strategies (quality/random/sequential) |
---
@@ -503,9 +505,13 @@ For exposing your OmniRoute instance to the public internet (Cloudflare/ngrok/Ta
## Environment Variables
| Variable | Default | Description |
| --------------------- | ------- | -------------------------------------------------------------- |
| `ENABLE_SOCKS5_PROXY` | `true` | Enable SOCKS5 proxy support (default `true` in `.env.example`) |
| Variable | Default | Description |
| -------------------------------- | ------------------------------------- | -------------------------------------------------------------- |
| `ENABLE_SOCKS5_PROXY` | `true` | Enable SOCKS5 proxy support (default `true` in `.env.example`) |
| `ONEPROXY_ENABLED` | `true` | Enable 1proxy integration |
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | 1proxy API endpoint |
| `ONEPROXY_MAX_PROXIES` | `500` | Maximum proxies to sync |
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | Minimum quality score to import |
---
@@ -766,10 +772,55 @@ Use `random`
evenly)
```
### Configuring Rotation Strategy
```ts
import { rotateOneproxyProxy } from "omniroute/oneproxyRotator";
// In a one-off script
const proxy = await rotateOneproxyProxy({ strategy: "quality" });
if (proxy) {
console.log(`Selected: ${proxy.host}:${proxy.port}, quality=${proxy.qualityScore}`);
}
```
### Resetting Sequential Index
When using `sequential` strategy, the internal index accumulates. To reset:
```ts
import { resetSequentialIndex } from "omniroute/oneproxyRotator";
resetSequentialIndex();
```
Useful when:
- Restarting a load test
- Recovering from a proxy outage (so you don't cycle through dead ones first)
- Manually rebalancing after adding new proxies
### Marking a Proxy as Failed
When a proxy consistently fails, mark it manually so the rotator will skip it:
```ts
import { failOneproxyProxy } from "omniroute/oneproxyRotator";
const removed = await failOneproxyProxy("203.0.113.7", 8080);
if (removed) {
console.log("Proxy marked as failed; rotator will skip it");
}
```
The proxy is **not deleted** — it's marked unhealthy and won't be selected until the next successful health check (via `proxyHealth.ts`) or manual reset.
---
## Automatic Failure Exclusion for Your Own Proxies
The 1proxy marketplace pool already auto-degrades failed proxies on its own (see
[Proxy Quality Scores](#proxy-quality-scores)). For
`failOneproxyProxy()` above only covers the 1proxy marketplace pool, which already
auto-degrades on failure (see [Proxy Quality Scores](#proxy-quality-scores)). For
proxies **you** added to the registry, the background health scheduler
(`src/lib/proxyHealth/scheduler.ts`) provides the same "exclude a dead member from
the chain automatically" behavior, without deleting anything:

View File

@@ -1,7 +1,7 @@
---
title: "Providers — ChatGPT Web (Codex)"
version: 3.8.51
lastUpdated: 2026-08-31
version: 3.8.50
lastUpdated: 2026-08-26
---
# Providers — ChatGPT Web (Codex)
@@ -9,8 +9,7 @@ lastUpdated: 2026-08-31
`chatgpt-web-codex` (alias `cgpt-codex`) bridges Codex Responses turns through an
authenticated ChatGPT browser session. It is independent from the retired common
`chatgpt-web` provider and uses the MIT-noticed implementation under
`open-sse/vendor/codex-chatgpt-web/`, refreshed through upstream v4.0.7 commit
`b59d7dc51b84fb1f465ff1d00f5207f3b2b4a494`.
`open-sse/vendor/codex-chatgpt-web/`.
## Common provider retirement
@@ -19,7 +18,7 @@ provenance of their pre-key/proof-of-work implementation could not be cleared. E
requests to either ID, including slash-prefixed model IDs and persisted aliases, fail
closed with HTTP `410` and code **PROVIDER_RETIRED** before any upstream request.
Migration `168_retire_chatgpt_web.sql` tombstones matching provider connections and
Migration `163_retire_chatgpt_web.sql` tombstones matching provider connections and
invalidates their active session leases. It preserves connection history and API-key
allowlists; it does not add replacement access to an allowlist. The Codex provider and
its connections are not matched by this retirement.
@@ -27,24 +26,21 @@ its connections are not matched by this retirement.
## Prerequisites
- a full Cookie header from a signed-in ChatGPT session;
- Chrome or Chromium plus a graphical session or Xvfb display for npm, systemd, and PM2
installs;
- Chrome or Chromium for npm, systemd, and PM2 installs;
- with the Docker `web` profile, the internal Chromium service from
`docker-compose.yml`;
- OpenAI `tunnel-client` v0.0.13 and a ChatGPT custom connector for local Codex tools.
- an OpenAI tunnel and a ChatGPT custom connector for local Codex tools.
The tunnel is only needed for tool turns. Every listed route, including `pro`, can use the
same turn-bound local tool capability when the tunnel and connector are configured.
The tunnel is only needed for tool turns. The `pro` model is read-only and does not need
a local tool connector.
## Dashboard setup
1. Open the **ChatGPT Web (Codex)** provider and add a connection.
2. Paste the full ChatGPT Cookie header, tunnel ID, runtime key, and custom connector
name. New tool-capable setups must use a newly created connector named exactly
`OmniRoute Codex v2`, with Authentication set to None and Permissions set to Allow all
actions.
3. Run the connection check. OmniRoute opens a browser-backed Temporary Chat and detects
whether Sol and Pro are available for the account.
name.
3. Run the connection check. OmniRoute opens a headless Temporary Chat and detects
whether `pro` is available for the account.
4. Save the connection. OmniRoute replaces the pasted cookie with the verified
Playwright storage state and stores it with the runtime key through the encrypted
credential abstraction.
@@ -61,8 +57,6 @@ connector, and tool round-trip separately.
The fixed model routes are:
- `chatgpt-web-codex/luna` — GPT-5.6 Luna, low effort
- `chatgpt-web-codex/think` — GPT-5.6 Luna, medium effort
- `chatgpt-web-codex/instant`
- `chatgpt-web-codex/medium`
- `chatgpt-web-codex/high`
@@ -73,15 +67,8 @@ Add one of them to a combo like any other model. The Codex app sends the combo n
`model` to the regular Responses endpoint, `/v1/responses`; there is no separate Codex
endpoint or mode switch.
Free/Go accounts expose the Luna routes. Sol-capable accounts expose Instant through
High, and Pro-capable accounts additionally expose Extra High and Pro. Each route has a
fixed backend model and reasoning effort; a conflicting explicit Responses effort fails
closed instead of silently changing the selected browser mode.
Do not rename or reuse an older `Codex Native` or `OmniRoute Codex` connector. ChatGPT
caches the public MCP contract by connector identity, while the refreshed bridge uses a
new direct turn-token contract. The runtime rejects those legacy identities and requires
a new `OmniRoute Codex v2` connector.
`pro` does not run local tools. A forced tool makes that combo target incompatible. With
optional tools, the turn runs read-only and reports the limitation as commentary.
## Security model
@@ -99,30 +86,26 @@ a new `OmniRoute Codex v2` connector.
- Cookies, runtime keys, storage state, and capability tokens do not appear in provider
responses or request logs.
## Displayless VPS and Docker
## Headless VPS and Docker
For npm, systemd, and PM2 installs, OmniRoute detects common Chrome and Chromium paths.
Alternatively, set `CHATGPT_WEB_CODEX_CHROME_PATH`. Runtime turns deliberately use headed
Chrome because ChatGPT rejects the true-headless browser shape. A displayless host must therefore
run OmniRoute with a private Xvfb display; setting the Chrome path alone does not provide one.
Alternatively, set `CHATGPT_WEB_CODEX_CHROME_PATH`.
The Docker `web` profile starts `chatgpt-web-codex-browser` on the internal Compose
network. The sidecar runs headed Chrome inside Xvfb, so no physical display is required. Its CDP
port is not published on the host. The protected browser profile volume is separate from the
OmniRoute data volume, and the browser receives enough shared memory. The internal CDP proxy
listens only on port `9223` inside the Compose network; Chrome remains bound to loopback in the
sidecar.
network. Its CDP port is not published on the host. The protected browser profile volume
is separate from the OmniRoute data volume, and the browser receives enough shared
memory. The internal CDP proxy listens only on port `9223` inside the Compose network;
Chrome remains bound to loopback in the sidecar.
A supervisor lease under `DATA_DIR` prevents multiple OmniRoute processes from owning
the same tunnel and broker state. A conflict is reported by the doctor.
## Interactive recovery
The automated Docker path has no host-visible window, but Chrome itself is headed inside the
private Xvfb display. When ChatGPT requires an interactive sign-in or challenge, the existing VNC
browser infrastructure can be used for recovery. Browser UI and CDP must remain reachable only
over loopback, an authenticated management connection, or an SSH tunnel; noVNC stays disabled
during normal operation.
The normal path is headless. When ChatGPT requires an interactive sign-in or challenge,
the existing VNC browser infrastructure can be used for recovery. Browser UI and CDP
must remain reachable only over loopback, an authenticated management connection, or an
SSH tunnel; noVNC stays disabled during normal operation.
## WebSocket fallback

View File

@@ -13,7 +13,7 @@ OmniRoute integrates with three categories of CLI tools spread across three dedi
| Page | Route | Concept | Count |
| -------------- | ----------------------- | ------------------------------------------------------------------------- | ------------ |
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 26 |
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 10 |
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 9 |
| **ACP Agents** | `/dashboard/acp-agents` | CLIs that OmniRoute spawns as backend via stdio/ACP (reverse flow) | see registry |
Legacy routes redirect via 308: `/dashboard/cli-tools``/dashboard/cli-code`, `/dashboard/agents``/dashboard/acp-agents`.

View File

@@ -415,7 +415,6 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex,
| `CLI_CRUSH_BIN` | `crush` | `src/shared/services/cliRuntime.ts` | Custom path to the Crush CLI binary. |
| `CLI_OMP_BIN` | `omp` | `src/shared/services/cliRuntime.ts` | Custom path to the Oh My Pi (`omp`) agent binary. |
| `CLI_LETTA_BIN` | `letta` | `src/shared/services/cliRuntime.ts` | Custom path to the Letta CLI binary. |
| `CLI_PRIME_AGENT_BIN` | `prime-agent` | `src/shared/services/cliRuntime.ts` | Custom path to the Prime Agent (Prime Intellect) binary. |
| `CLI_WINDSURF_BIN` | _(none)_ | `src/shared/services/cliRuntime.ts` | Custom path to the Windsurf binary. Windsurf ships **no default command** — binary detection stays disabled until this is set. |
| `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. |
| `DEVIN_DESKTOP_VERSION` | `3.6.27` | `open-sse/executors/devin-desktop.ts` | Devin Desktop `ide_version`. Overrides must use `x.y.z` format; invalid values fall back to the verified default. |
@@ -764,18 +763,15 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_PROVIDER_PROBE_TIMEOUT_MS` | `8000` | Timeout (ms) for the `validationRead` and `modelsProbe` presets in `src/shared/network/safeOutboundFetch.ts`. Raise for slow endpoints (Cerebras, Cloudflare AI, Groq) to prevent flapping between active/error in the dashboard. Falls back to 8000ms for invalid (<1000) or non-numeric values. |
| `OMNIROUTE_RELAY_FETCH_TIMEOUT_MS` | `25000` | Relay-specific fetch timeout in `open-sse/utils/proxyFetch.ts` (#9158). A hung relay must fail before the client/agent timeout (~30s) so callers see a relay-specific failure instead of a generic upstream timeout. Capped at `29000` so it always fires first. |
| `OMNIROUTE_RETRY_BACKOFF_MS` | `10` | Shared retry backoff for the direct/relay/proxy retry-once paths in `open-sse/utils/proxyFetch.ts` (#9158). `0` = retry immediately. |
| `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | Native wreq-js request timeout (`chatgptTlsClient.ts`). |
| `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_CHATGPT_STREAM_FIRST_BYTE_TIMEOUT_MS` | `30000` (30s) | Max wait for the first streamed byte before ChatGPT switches to a buffered response; the hard request deadline remains active. |
| `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | Native wreq-js request timeout (`claudeTlsClient.ts`). |
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Native wreq-js request timeout (`perplexityTlsClient.ts`). |
| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | JS-side hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`claudeTlsClient.ts`). |
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). |
| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_PPLX_SEARCH_HINT` | `0` (off) | Appends "You have built-in web search. Answer questions directly using search results." to the caller's system message (`perplexity-web/protocol.ts`). Off by default — Perplexity searches anyway, and the sentence leaks into replies as meta-commentary for coding clients. Set `1`/`true`/`yes`/`on` to restore. |
| `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | Native wreq-js request timeout (`grokTlsClient.ts`). |
| `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | JS-side hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Native wreq-js request timeout (`notionTlsClient.ts`); `notion-web` raises it per request to `180000` for long generations. |
| `OMNIROUTE_NOTION_TLS_GRACE_MS` | `10000` | JS-side hard-deadline grace added on top of the native timeout. |
| `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`grokTlsClient.ts`). |
| `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`notionTlsClient.ts`); the `notion-web` executor raises it per-request to `180000` for long generations. |
| `OMNIROUTE_NOTION_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_BROWSER_POOL` | `on` | Shared Playwright browser pool for browser-backed web-cookie chat (`browserPool.ts`); set `off` to disable. |
| `WEB_COOKIE_USE_BROWSER` | `0` | Opt a web-cookie chat request into the browser-backed path (`browserBackedChat.ts`); `1` to enable. |
| `KIMI_WEB_BASE_URL` | `https://www.kimi.ai` | Base URL for the Kimi Web (international kimi.ai Connect-RPC) executor (`kimi-web.ts`); override only for mirror/proxy endpoints. |
@@ -979,16 +975,6 @@ Reverse-engineered session bridge for hyperagent.com (`src/shared/constants/prov
---
## Kilo Code Usage Quotas
Personal USD balance and Kilo Pass usage lookup for the Kilo Code provider. Optional — the default points at the public Kilo API; override only for a relay/test fixture. Authentication uses the connection's existing OAuth access token.
| Variable | Default | Source File | Description |
| ---------------- | ---------------------- | ----------------------------------------- | ------------------------------------------------------- |
| `KILO_API_URL` | `https://api.kilo.ai` | `open-sse/services/usage/kilocode.ts` | Base URL used to fetch personal Kilo Code balance and Kilo Pass usage. |
---
## Adobe Firefly Web Provider (Unofficial/Experimental)
Browser-driven session refresh for the Adobe Firefly web provider
@@ -1293,6 +1279,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `OMNIROUTE_SKIP_DNS_WRITE` | _(unset)_ | `src/mitm/dns/dnsConfig.ts` | Set `1` to skip writing to the hosts file when adding/removing DNS entries — for sandboxed or read-only test environments. |
| `OMNIROUTE_SKIP_SYSTEM_TRUST` | `0` | `src/mitm/cert/install.ts`, `src/mitm/tproxy/caTrust.ts` | Test/CI-only guard: set `1` to make cert trust install/uninstall a no-op so the suite never mutates the OS trust store. Set automatically by the test setup and CI workflows. |
| `CHANGELOG_BASE_REF` | _(auto)_ | `scripts/check/check-changelog-integrity.mjs` | Explicit base ref for the anti CHANGELOG-eat gate (defaults to the PR base branch in CI, or the highest `release/v*`). |
| `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | Enable the 1Proxy egress pool sync. |
| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. |
| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. |
| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | `src/lib/oneproxySync.ts` | Minimum quality score for imported proxies. |
| `FREE_PROXY_AUTO_SYNC_ENABLED` | `false` | `src/lib/freeProxyProviders/scheduler.ts` | Set `true` to enable the background free-proxy pool auto-sync scheduler. Opt-in, off by default. |
| `FREE_PROXY_AUTO_SYNC_INTERVAL_MS` | `1800000` | `src/lib/freeProxyProviders/scheduler.ts` | Auto-sync interval in milliseconds (default 30 min). |
| `FREE_PROXY_1PROXY_ENABLED` | `true` | `src/lib/freeProxyProviders/oneproxy.ts` | Enable the 1proxy free proxy source. Set to `false` to disable. |
@@ -1619,12 +1609,7 @@ Globale Defaults für den headless Browser und den ausgehenden Tool-Tunnel. Im D
| `CHATGPT_WEB_CODEX_CDP_URL` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Interner CDP-Endpunkt; Docker verwendet den Sidecar auf Port `9223`. |
| `CHATGPT_WEB_CODEX_TUNNEL_ID` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Globale OpenAI-Tunnel-ID für lokale Codex-Tool-Runden. |
| `CHATGPT_WEB_CODEX_RUNTIME_KEY` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Globaler Tunnel Runtime-Key; niemals in Logs ausgeben. |
| `CHATGPT_WEB_CODEX_CONNECTOR_NAME` | `OmniRoute Codex v2` | `open-sse/executors/chatgpt-web-codex.ts` | Exakter Name des neu erstellten ChatGPT-Custom-Connectors für die MCP-Brücke. |
| `CODEX_CHATGPT_WEB_HOME` | `<DATA_DIR>/chatgpt-web-codex` | `open-sse/vendor/codex-chatgpt-web/config.ts` | Dediziertes Verzeichnis für Browser-, Broker- und Tunnelzustand. |
| `CODEX_CHATGPT_WEB_BROWSER_DIAGNOSTICS` | `0` | `open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts` | Bei `1` werden Browser-Diagnosebilder an jedem Checkpoint erfasst. |
| `CODEX_CHATGPT_WEB_LAUNCHER` | _(unset)_ | `open-sse/vendor/codex-chatgpt-web/config.ts` | Optionaler absoluter Pfad zu einem dauerhaften Launcher-Binary. |
| `CODEX_CHATGPT_WEB_BUN` | _(auto-detect)_ | `open-sse/vendor/codex-chatgpt-web/config.ts` | Optionaler absoluter Pfad zum Bun-Runtime-Binary. |
| `CODEX_WEB_GPT_BUN` | _(unset)_ | `open-sse/vendor/codex-chatgpt-web/config.ts` | Legacy-Fallback für `CODEX_CHATGPT_WEB_BUN`; neue Setups verwenden den kanonischen Namen. |
| `CHATGPT_WEB_CODEX_CONNECTOR_NAME` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Name des ChatGPT-Custom-Connectors für die MCP-Brücke. |
---
## OmniConductor Bridge

View File

@@ -51,23 +51,24 @@ used when neither a DB override nor an environment variable is present.
### Security (7)
| Key | Type | Default | Description |
| --------------------------------- | ------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. |
| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. |
| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. |
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | enum | `high` | Minimum severity blocked when mode is `block` (`high`/`medium`/`low`). Medium families are observe-only at default. |
| `INJECTION_GUARD_BLOCK_THRESHOLD` | enum | _(unset)_ | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. |
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). |
| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. |
| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. |
| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. |
| Key | Type | Default | Description |
| -------------------------------- | ------- | -------- | ----------------------------------------------------------------------------- |
| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. |
| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. |
| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. |
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | enum | `high` | Minimum severity blocked when mode is `block` (`high`/`medium`/`low`). Medium families are observe-only at default. |
| `INJECTION_GUARD_BLOCK_THRESHOLD` | enum | _(unset)_ | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. |
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). |
| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. |
| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. |
| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Block outbound requests to private/internal IP ranges. |
### Network (8)
| Key | Type | Default | Restart | Description |
| ----------------------------------------------- | ------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_TLS_FINGERPRINT` | boolean | `false` | ✓ | Enable TLS fingerprint stealth mode. |
| `ONEPROXY_ENABLED` | boolean | `true` | | Enable 1proxy request proxying. |
| `PROXY_AUTO_SELECT_ENABLED` | boolean | `false` | | When no proxy is assigned to a connection, auto-select the first working proxy from the registry. Off by default (otherwise any registry proxy becomes a global fallback — #3332). |
| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | boolean | `false` | | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Off by default because this can change egress IP. |
| `MITM_DISABLE_TLS_VERIFY` | boolean | `false` | ✓ | Disable TLS certificate verification for the MITM proxy. **Danger.** |
@@ -77,27 +78,27 @@ used when neither a DB override nor an environment variable is present.
### Policies (3)
| Key | Type | Default | Restart | Description |
| ------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. |
| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. |
| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. |
| Key | Type | Default | Restart | Description |
| ----------------------------------------- | ------- | ---------- | ------- | ---------------------------------------------------------------------- |
| `TOOL_POLICY_MODE` | enum | `disabled` | | Tool-use policy enforcement mode. Values: `disabled`, `warn`, `block`. |
| `RATE_LIMIT_AUTO_ENABLE` | boolean | `false` | | Automatically enable rate limiting based on usage patterns. |
| `DISABLE_CONTEXT_WINDOW_CHECKS` | boolean | `false` | | Skip OmniRoute's local context-window / max-input-token check for direct single-model requests. Upstream limits still apply. |
### Runtime (11)
| Key | Type | Default | Restart | Description |
| ------------------------------------------- | ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Key | Type | Default | Restart | Description |
| ------------------------------------------- | ------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EXPOSE_CC_DISCOVERY_ALIASES` | boolean | `false` | | Advertise `claude/<provider>/<model>` mirror ids on `/v1/models` so Claude Code gateway model discovery lists non-Claude models. Global level of the three-level gate (env wins over the dashboard override). See [Claude Code configuration](../guides/CLAUDE-CODE-CONFIGURATION.md#discovery-aliases--surface-non-claude-models-in-the-model-picker). |
| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. |
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. |
| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. |
| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). |
| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. |
| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20129 by default). |
| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. |
| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) |
| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. |
| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. |
| `OMNIROUTE_MCP_ENFORCE_SCOPES` | boolean | `true` | | Enforce scope restrictions on MCP tool access. |
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | boolean | `false` | | Compress MCP tool descriptions to reduce token usage. |
| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | boolean | `false` | | Enable background task processing at runtime. |
| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | boolean | `false` | ✓ | Disable all background services (quota refresh, sync, etc). |
| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | boolean | `false` | | Trust project-level RTK filters without validation. |
| `OMNIROUTE_ENABLE_LIVE_WS` | boolean | `true` | ✓ | Start the real-time dashboard WebSocket server on import (port 20129 by default). |
| `OMNIROUTE_CODEX_WS_ENABLED` | boolean | `true` | | Allow Codex to use the Responses-over-WebSocket transport. When off, Codex falls back to HTTP Responses. |
| `OMNIROUTE_EMERGENCY_FALLBACK` | boolean | `true` | | Route budget-exhausted requests to the emergency free fallback provider/model. (See [Emergency Budget Fallback](#emergency-budget-fallback) below.) |
| `MODEL_CATALOG_INCLUDE_NAMES` | boolean | `true` | | Include display-friendly name fields in `/v1/models` responses. Disable for clients that expect model IDs only. |
| `ARENA_ELO_SYNC_ENABLED` | boolean | `true` | | Enable periodic Arena AI leaderboard ELO sync for model intelligence rankings. |
### CLI (3)

View File

@@ -1,7 +1,7 @@
---
title: "Free Tiers & Free-Token Budget"
version: 3.8.50
lastUpdated: 2026-08-31
lastUpdated: 2026-08-26
---
# Free Tiers & Free-Token Budget
@@ -49,23 +49,6 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve
---
## Two regimes — counting vs deciding
OmniRoute answers "is it free?" through two regimes that intentionally read
different sources:
| Regime | Source of truth | Surfaces |
|---|---|---|
| **Counting / displaying** | Resolved catalog — the shipped baseline overlaid by the Radar feed (`getRadarCatalog`) | Free-tier totals, budget card, dashboards |
| **Deciding** | Shipped catalog only (`FREE_MODEL_BUDGETS` in `open-sse/config/freeModelCatalog.data.ts`) plus the local heuristics (`:free` suffix, zero pricing, `grantsFreeAccess`) | Every consumer of `src/shared/utils/freeModels.ts`: model import, `auto/*` routing, `GET /v1/models`, and the browser previews |
Counting can improve whenever a feed is available. Deciding stays on the
release artifact, so the answer is identical in the browser and on the server,
reproducible offline, and testable without a database. Letting the browser
preview read one source while the server import reads another would produce a
preview that disagrees with what happens on click — the split is kept on
purpose.
## Methodology & caveats
- Numbers are **upper-bound estimates** from each provider's documented free-tier limits as of **2026-06-17**, gathered by web research (confidence tagged per row). Free tiers change constantly — re-verify before relying on a figure.

View File

@@ -186,9 +186,9 @@ See [#7992](https://github.com/diegosouzapw/OmniRoute/issues/7992) and [#7111](h
The Auto-Combo Engine dynamically selects the best provider/model for each request using a **15-factor scoring function** (defined in `open-sse/services/autoCombo/scoring.ts``DEFAULT_WEIGHTS`). The default weights sum to `1.0`; custom weights are renormalized by `normalizeScoringWeights()`. Two of the fifteen — `cacheAffinity` and `resetWindowAffinity` — carry a default weight of `0`: they are still computed for every candidate, and `cacheAffinity` gates prompt-cache deduplication outside the score, so they are declared factors that simply do not vote by default.
![Auto-Combo 15-factor scoring](../diagrams/exported/auto-combo-scoring.svg)
![Auto-Combo 15-factor scoring](../diagrams/exported/auto-combo-12factor.svg)
> Source: [diagrams/auto-combo-scoring.mmd](../diagrams/auto-combo-scoring.mmd) (regenerate via `npm run docs:render-diagrams`). The filename is historical; the source and rendered diagram show all 15 factors declared in `DEFAULT_WEIGHTS`.
> Source: [diagrams/auto-combo-12factor.mmd](../diagrams/auto-combo-12factor.mmd) (regenerate via `npm run docs:render-diagrams`). The filename is historical; the source and rendered diagram show all 15 factors declared in `DEFAULT_WEIGHTS`.
| Factor | Default Weight | Description |
| :-------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -289,26 +289,6 @@ OmniRoute's combo engine supports **19 routing strategies** (declared in `src/sh
⭐ = New in v3.8.0 · 🧬 = New in v3.8.36
### `weighted` semantics
`weighted` is a **proportional random draw per request**
(`open-sse/services/combo/targetSorters.ts``selectWeightedTarget`), not an equalizer:
- Each request draws **one** step with probability `weight / totalWeight`; the remaining steps
are ordered by descending weight as the fallback chain for that request.
- A step whose weight is `0` (or missing) is **never drawn** while any other step has a
weight > 0 — it can only serve as a fallback after the drawn step fails. Only when **all**
weights are 0 does selection become uniform.
- Steps whose targets are all unavailable — provider circuit breaker `OPEN`, connection
cooldown, model lockout — are removed from the draw before it happens
(`open-sse/services/combo/targetResolution.ts`), so a single healthy step can temporarily
win every request.
- `stickyWeightedLimit` (combo config, default `1` = off) pins the drawn step for that many
consecutive successes before re-drawing.
For strict rotation use `round-robin`; equal weights on `weighted` give statistical — not
strict — balance.
## Fusion Strategy
`fusion` is the one strategy that does **not** pick a single target. It fans the prompt

View File

@@ -1,13 +1,13 @@
---
title: "Stealth Guide"
version: 3.8.50
lastUpdated: 2026-08-26
version: 3.8.40
lastUpdated: 2026-06-28
---
# Stealth Guide
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{tlsClientBase,chatgptTlsClient,claudeTlsClient,perplexityTlsClient,grokTlsClient,notionTlsClient,lmarenaTlsClient,claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
> **Last updated:** 2026-08-26 — v3.8.50
> **Source of truth:** `open-sse/utils/tlsClient.ts`, `open-sse/services/{claudeCodeCCH,claudeCodeFingerprint,claudeCodeObfuscation,claudeCodeCompatible}.ts`, `open-sse/config/cliFingerprints.ts`, `src/mitm/`
> **Last updated:** 2026-06-28 — v3.8.40
> **Audience:** Engineers maintaining provider-specific stealth integrations.
OmniRoute integrates with providers whose edges actively fingerprint non-official clients (TLS JA3/JA4, header ordering, JSON body shape, integrity tokens). This page documents the stealth surfaces OmniRoute exposes and where they are implemented.
@@ -29,41 +29,6 @@ Lazy-loaded `wreq-js` session that impersonates **Chrome 124 on macOS**. Used as
- Timeout: `TLS_CLIENT_TIMEOUT_MS` (inherits from `FETCH_TIMEOUT_MS`, default 600000)
- `wreq-js` Response is fetch-compatible (`headers`, `text()`, `json()`, `clone()`, `body`).
### Web-cookie provider transport — wreq-js 3.0.0
`open-sse/services/tlsClientBase.ts` is the shared transport for ChatGPT, Claude, Perplexity,
Grok, Notion, and LMArena web sessions. Each thin provider wrapper selects a browser/OS profile;
the base loads `wreq-js` lazily, reuses only transport-level connections keyed by
profile + OS + resolved proxy, and gives every request an ephemeral cookie scope. It never shares a
wreq session or cookie jar between accounts or requests.
| Provider | Profile | Emulated OS | Stream EOF policy |
| ---------- | ------------- | ----------- | -------------------------------- |
| ChatGPT | `firefox_148` | macOS | include `[DONE]` |
| Claude | `chrome_146` | Linux | include `[DONE]` |
| Perplexity | `firefox_148` | macOS | include `event: end_of_stream` |
| Grok | `chrome_146` | Linux | exclude `[DONE]` |
| Notion | `chrome_146` | Windows | include `[DONE]` |
| LMArena | `chrome_146` | Windows | no sentinel; close on native EOF |
- Streaming uses the native response `ReadableStream` directly; no temp file or sidecar is created.
- Up to 256 initial bytes are inspected before exposing a stream. SSE providers buffer non-SSE
errors; Grok/LMArena map Cloudflare challenges to `403` and HTML interstitials to `502`.
- The native request timeout remains wrapped by an absolute JS hard deadline. A hang invalidates
and closes only the affected profile/OS/proxy transport before the next request recreates it.
- Proxy resolution priority is per-call `proxyUrl` → request-scoped account/dashboard context →
`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` (including lowercase variants). Resolution errors fail
closed instead of leaking a direct connection. LMArena deliberately resolves against `arena.ai`.
- `byteResponse` returns a content-typed `data:` URL without UTF-8 corruption.
- Errors are `TlsClientUnavailableError` (package/addon unavailable) and `TlsClientHangError`
(deadline exceeded).
The profiles are supported by the pinned package, but real WAF acceptance can change independently
of local contract tests. Validate fingerprint changes against an explicitly authorized live account
before claiming parity with an upstream browser.
---
## Claude Code Stealth Bundle
When `cliCompatMode` is on, OmniRoute reshapes outgoing Claude requests so they are indistinguishable from `claude-cli` traffic. Three modules collaborate:

View File

@@ -1,6 +1,4 @@
import { fixupConfigRules } from "@eslint/compat";
import nextVitals from "eslint-config-next/core-web-vitals";
import * as espree from "espree";
import tseslint from "typescript-eslint";
// #7879: bar NEW local `toNumber` definitions outside the canonical helper.
@@ -39,22 +37,7 @@ const IMPORT_BOUNDARY_RESTRICTIONS = {
/** @type {import("eslint").Linter.Config[]} */
const eslintConfig = [
...fixupConfigRules(nextVitals),
// eslint-config-next's Babel parser has not adopted ESLint 10's ScopeManager
// finalize contract yet. Plain JS/JSX does not need that parser, so use the
// ESLint-native parser while retaining Next's plugins and rules.
{
files: ["**/*.{js,jsx,mjs,cjs}"],
languageOptions: {
parser: espree,
},
},
{
files: ["**/*.{mts,cts}"],
languageOptions: {
parser: tseslint.parser,
},
},
...nextVitals,
// Pacote 4 (plano mestre testes+CI, 2026-07-04) — zero-warning policy: TODA regra roda
// como "error" e a dívida pré-existente vive congelada por arquivo+regra em
// config/quality/eslint-suppressions.json (ESLint bulk suppressions nativo). Violação
@@ -150,45 +133,28 @@ const eslintConfig = [
"no-restricted-syntax": "off",
},
},
// Relaxed rules for TypeScript in open-sse and tests (incremental adoption).
// eslint-config-next already registers @typescript-eslint for every TS file;
// registering a second plugin object is rejected by ESLint 10.
// Relaxed rules for open-sse and tests (incremental adoption)
{
files: ["open-sse/**/*.ts", "tests/**/*.ts"],
files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@next/next/no-assign-module-variable": "off",
"react-hooks/rules-of-hooks": "off",
},
},
{
files: ["tests/**/*.mjs"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@next/next/no-assign-module-variable": "off",
"react-hooks/rules-of-hooks": "off",
},
},
// JS/JSX files do not match eslint-config-next's TypeScript block. Register
// the plugin only for that disjoint scope so the shared unused-vars ratchet
// works without redefining the plugin for TS/TSX under ESLint 10.
{
files: ["src/**/*.{js,jsx}"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
},
// Ratchet: bar NEW unused vars/args/catches outside the `_` escape hatch.
// Pre-existing violations are frozen via config/quality/eslint-suppressions.json
// (same pattern as #7879 toNumber); only genuinely NEW unused bindings fail
// lint. `args: "all"` (not `after-used`) so a leading unused param is never
// silently skipped, e.g. `function handle(req, _opts, next)` must flag `req`.
// eslint-config-next already registers @typescript-eslint; registering it again
// in this block is rejected by ESLint 10.
{
files: ["src/**/*.{ts,tsx,js,jsx}", "open-sse/**/*.ts", "tests/**/*.{ts,tsx,mjs}"],
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",

View File

@@ -74,43 +74,12 @@ function isNextIntlExtractorDynamicImportWarning(warning) {
);
}
const IGNORED_INFRASTRUCTURE_BUILD_DEPENDENCY_MODULES = [
"/node_modules/fumadocs-mdx/dist/load-from-file-",
"/node_modules/next-intl/dist/esm/production/extractor/format/index.js",
];
function isKnownInfrastructureBuildDependencyWarning(args) {
const message = args
.filter((value) => typeof value === "string")
.join(" ")
.replaceAll("\\", "/");
return (
message.includes("webpack.FileSystemInfo") &&
message.includes("for build dependencies failed at 'import(") &&
message.includes("incorrect cache invalidation") &&
IGNORED_INFRASTRUCTURE_BUILD_DEPENDENCY_MODULES.some((modulePath) =>
message.includes(modulePath)
)
);
}
function filterKnownInfrastructureWarnings(baseConsole) {
const filteredConsole = Object.create(baseConsole);
filteredConsole.warn = (...args) => {
if (isKnownInfrastructureBuildDependencyWarning(args)) return;
Reflect.apply(baseConsole.warn, baseConsole, args);
};
return filteredConsole;
}
// OMNIROUTE_BUILD_PROFILE=minimal physically removes four optional privileged
// modules (MITM cert install, Zed keychain import, Cloud Sync, 9router
// installer) from the built bundle by aliasing them to feature-disabled stubs.
// The resulting artifact is intended to be published as `omniroute-secure`
// for security-sensitive environments. See docs/security/SOCKET_DEV_FINDINGS.md.
const isMinimalBuild = process.env.OMNIROUTE_BUILD_PROFILE === "minimal";
// Contributor builds validate compilation only and do not need a shippable standalone bundle.
const isContributorBuild = process.env.OMNIROUTE_BUILD_PROFILE === "contributor";
// #10273: `null` unless the operator opts in with DASHBOARD_ALLOW_EMBED=vscode. Read at build
// time like every other knob in this file (OMNIROUTE_BASE_PATH, OMNIROUTE_BUILD_PROFILE, …),
@@ -163,7 +132,9 @@ const nextConfig = {
// instead of keeping the old generation in control. Falls back to a
// value that is unique per build run when git is absent (CI tarball).
NEXT_PUBLIC_SW_BUILD_ID:
process.env.OMNIROUTE_SW_BUILD_ID || process.env.SOURCE_VERSION || `${Date.now()}`,
process.env.OMNIROUTE_SW_BUILD_ID ||
process.env.SOURCE_VERSION ||
`${Date.now()}`,
},
distDir,
// Turbopack config: redirect native modules to stubs at build time
@@ -220,7 +191,7 @@ const nextConfig = {
},
],
},
...(isContributorBuild ? {} : { output: "standalone" }),
output: "standalone",
compress: true,
productionBrowserSourceMaps: false,
// OmniRoute is a proxy for AI APIs — request bodies routinely include
@@ -288,9 +259,6 @@ const nextConfig = {
// (better-sqlite3 → node:sqlite → sql.js). Next traces sql-wasm.js but can
// omit the runtime sql-wasm.wasm asset from the standalone bundle.
"./node_modules/sql.js/dist/sql-wasm.wasm",
// tiktoken is server-externalized below so Node selects its CommonJS entry.
// That entry reads the tokenizer WASM beside itself at runtime.
"./node_modules/tiktoken/tiktoken_bg.wasm",
],
},
outputFileTracingExcludes: {
@@ -339,12 +307,11 @@ const nextConfig = {
"keytar",
"wreq-js",
"zod",
"tls-client-node",
"koffi",
"tough-cookie",
"@ngrok/ngrok",
"@huggingface/transformers",
// The ESM entry imports tiktoken_bg.wasm as a module. Turbopack can compile
// that graph but omits the runtime asset, making provider routes fail during
// module evaluation. Keep Node's CommonJS loader and colocated WASM intact.
"tiktoken",
// copilot-m365-web.ts imports 'ws' as a client-side WebSocket. When bundled,
// ws cannot resolve its 'bufferutil' native addon (frame masking) and throws
// TypeError: b.mask is not a function on the first outgoing frame, causing
@@ -377,11 +344,6 @@ const nextConfig = {
...(config.ignoreWarnings || []),
isNextIntlExtractorDynamicImportWarning,
];
const infrastructureLogging = config.infrastructureLogging || {};
config.infrastructureLogging = {
...infrastructureLogging,
console: filterKnownInfrastructureWarnings(infrastructureLogging.console || console),
};
const nextDefaultSplitChunks = config.optimization?.splitChunks;
config.optimization = config.optimization || {};
config.optimization.splitChunks = {

View File

@@ -19,12 +19,15 @@ export const chatgpt_web_codexProvider: RegistryEntry = {
authHeader: "cookie",
forceStream: true,
models: [
{ id: "luna", name: "ChatGPT Web — Luna", ...NATIVE_CAPABILITIES },
{ id: "think", name: "ChatGPT Web — Think", ...NATIVE_CAPABILITIES },
{ id: "instant", name: "ChatGPT Web — Instant", ...NATIVE_CAPABILITIES },
{ id: "medium", name: "ChatGPT Web — Medium", ...NATIVE_CAPABILITIES },
{ id: "high", name: "ChatGPT Web — High", ...NATIVE_CAPABILITIES },
{ id: "extra-high", name: "ChatGPT Web — Extra High", ...NATIVE_CAPABILITIES },
{ id: "pro", name: "ChatGPT Web — Pro", ...NATIVE_CAPABILITIES },
{
id: "pro",
name: "ChatGPT Web — Pro (read-only)",
...NATIVE_CAPABILITIES,
toolCalling: false,
},
],
};

View File

@@ -1,10 +1,5 @@
import { existsSync } from "node:fs";
import {
CHATGPT_WEB_CODEX_CONNECTOR_NAME,
CHATGPT_WEB_CODEX_RUNTIME_HEADED,
} from "@/shared/constants/chatgptWebCodex";
import { isVerifiedNativeCodexRequest } from "../config/codexIdentity.ts";
import { FORMATS } from "../translator/formats.ts";
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
@@ -121,44 +116,21 @@ function responseStateNamespace(connectionId: string, parsed: CodexParsedRequest
return `${connectionId}:${identity.threadId}:${identity.turnId}`;
}
function itemType(value: unknown): string {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const type = (value as Record<string, unknown>).type;
return typeof type === "string" ? type : "";
}
function itemRole(value: unknown): string {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const role = (value as Record<string, unknown>).role;
return typeof role === "string" ? role : "";
}
export function inputHasSelfContainedCodexContinuation(body: Record<string, unknown>): boolean {
const input = Array.isArray(body.input) ? body.input : [];
let hasUser = false;
let hasToolOutput = false;
for (const item of input) {
if (itemRole(item) === "user" || itemType(item) === "message") hasUser = true;
if (itemType(item) === "function_call_output" || itemType(item) === "custom_tool_call_output") {
hasToolOutput = true;
}
}
return hasUser && hasToolOutput;
}
export function resolveChatGptWebCodexPreviousResponse(
function previousResponseBelongsToTurn(
body: Record<string, unknown>,
namespace: string
): { body: Record<string, unknown>; ok: boolean } {
connectionId: string,
parsed: CodexParsedRequest
): boolean {
if (typeof body.previous_response_id !== "string" || !body.previous_response_id.trim()) {
return { body, ok: true };
return true;
}
try {
const namespace = responseStateNamespace(connectionId, parsed);
const expanded = expandPreviousResponseInput(body, namespace);
return expanded !== body;
} catch {
return false;
}
const expanded = expandPreviousResponseInput(body, namespace);
if (expanded !== body) return { body: record(expanded), ok: true };
if (!inputHasSelfContainedCodexContinuation(body)) return { body, ok: false };
const next = { ...body };
delete next.previous_response_id;
return { body: next, ok: true };
}
function toolModeRequired(parsed: CodexParsedRequest): boolean {
@@ -184,46 +156,44 @@ function buildProviderConfig(
throw new Error("No supported Chrome or Chromium executable was found");
}
const solAvailable = data.solAvailable !== false;
const proAvailable = data.proAvailable === true;
if (route.sol !== solAvailable) {
throw new Error(
route.sol
? "ChatGPT Sol models are not available for this Luna-only connection"
: "ChatGPT Luna models are only available for Luna-only connections"
);
}
if (route.pro && !proAvailable) {
throw new Error(`${route.id} is not available for this non-Pro connection`);
throw new Error("ChatGPT Pro is not available for this connection");
}
const hasTools = toolModeRequired(parsed);
const requiredChoice =
parsed.options.toolChoice === "required" || typeof parsed.options.toolChoice === "object";
if (route.pro && requiredChoice) {
throw new Error("ChatGPT Web Pro is read-only and cannot satisfy a required tool choice");
}
const connector =
configuredString(data, "connectorName", "appName") ??
(process.env.CHATGPT_WEB_CODEX_CONNECTOR_NAME?.trim() || CHATGPT_WEB_CODEX_CONNECTOR_NAME);
process.env.CHATGPT_WEB_CODEX_CONNECTOR_NAME?.trim();
if (!route.pro && hasTools && !connector) {
throw new Error("ChatGPT Web (Codex) tools require a ready tunnel and Custom Connector");
}
parsed.modelId = route.backendModel;
parsed.modelId = "gpt-5.6-sol";
parsed.options.reasoning = route.effort;
return {
adapter: "chatgpt-web",
baseUrl: "https://chatgpt.com",
defaultModel: route.backendModel,
models: [route.backendModel],
defaultModel: "gpt-5.6-sol",
models: ["gpt-5.6-sol"],
chatgptWeb: {
appName: connector,
...(connector ? { appName: connector } : {}),
storageStatePath,
...(chromeExecutablePath ? { chromeExecutablePath } : {}),
...(cdpEndpoint ? { cdpEndpoint } : {}),
brokerSocketPath: paths.brokerSocketPath,
threadEnvironmentStatePath: paths.threadEnvironmentStatePath,
lunaCheckpointStatePath: paths.lunaCheckpointStatePath,
headed: CHATGPT_WEB_CODEX_RUNTIME_HEADED,
localToolsEnabled: hasTools,
solAvailable,
headed: false,
localToolsEnabled: !route.pro && hasTools,
proAvailable,
experimentalBiggerContext: data.experimentalBiggerContext === true,
autoApproveToolCalls: hasTools,
autoApproveToolCalls: !route.pro && hasTools,
},
};
}
@@ -290,8 +260,7 @@ export class ChatGptWebCodexExecutor extends BaseExecutor {
const initialBody = nativeBody(input.body);
const initialParsed = parseRequest(initialBody);
const namespace = responseStateNamespace(connectionId, initialParsed);
const resolvedPrevious = resolveChatGptWebCodexPreviousResponse(initialBody, namespace);
if (!resolvedPrevious.ok) {
if (!previousResponseBelongsToTurn(initialBody, connectionId, initialParsed)) {
return wrapped(
errorResponse(
409,
@@ -301,7 +270,7 @@ export class ChatGptWebCodexExecutor extends BaseExecutor {
initialBody
);
}
const expandedBody = resolvedPrevious.body;
const expandedBody = expandPreviousResponseInput(initialBody, namespace);
const parsed = parseRequest(expandedBody);
responseStateNamespace(connectionId, parsed);
@@ -333,20 +302,17 @@ export class ChatGptWebCodexExecutor extends BaseExecutor {
const runtimePaths = connectionRuntimePaths(connectionId);
const loginConfig = {
mode: "browser-only" as const,
appName:
configuredString(providerData, "connectorName", "appName") ??
CHATGPT_WEB_CODEX_CONNECTOR_NAME,
appName: configuredString(providerData, "connectorName", "appName") ?? "OmniRoute Codex",
...(chromeExecutablePath ? { chromeExecutablePath } : {}),
...(cdpEndpoint ? { cdpEndpoint } : {}),
storageStatePath,
brokerSocketPath: runtimePaths.brokerSocketPath,
headed: CHATGPT_WEB_CODEX_RUNTIME_HEADED,
headed: false,
proAvailable: providerData.proAvailable === true,
autoApproveToolCalls: false,
};
if (!browserLoginStateExists(loginConfig)) {
const capabilities = await inspectBrowserLoginCapabilities(loginConfig);
providerData.solAvailable = capabilities.solAvailable;
providerData.proAvailable = capabilities.proAvailable;
providerData.browserVerified = true;
if (chromeExecutablePath) providerData.chromeExecutablePath = chromeExecutablePath;
@@ -354,7 +320,6 @@ export class ChatGptWebCodexExecutor extends BaseExecutor {
await input.onCredentialsRefreshed?.({
providerSpecificData: {
...record(input.credentials.providerSpecificData),
solAvailable: capabilities.solAvailable,
proAvailable: capabilities.proAvailable,
browserVerified: true,
...(chromeExecutablePath ? { chromeExecutablePath } : {}),
@@ -362,7 +327,7 @@ export class ChatGptWebCodexExecutor extends BaseExecutor {
},
});
}
const routeUsesTools = toolModeRequired(parsed);
const routeUsesTools = !route.pro && toolModeRequired(parsed);
if (routeUsesTools) {
const tunnelId =
configuredString(providerData, "tunnelId") ??

View File

@@ -37,7 +37,6 @@ export async function getChatGptWebCodexDoctorStatus(connection: {
);
let storageState = false;
let login = false;
let solAvailable = data.solAvailable !== false;
let proAvailable = data.proAvailable === true;
let credential = false;
try {
@@ -45,13 +44,22 @@ export async function getChatGptWebCodexDoctorStatus(connection: {
credential = Boolean(secrets.storageState);
if (credential) ensureConnectionStorageStateFromCredential(connectionId, secrets);
storageState = existsSync(paths.storageStatePath);
login = browserLoginStateExists({ storageStatePath: paths.storageStatePath });
login = browserLoginStateExists({
mode: "browser-only",
appName: "OmniRoute Codex",
storageStatePath: paths.storageStatePath,
brokerSocketPath: paths.brokerSocketPath,
...(chrome ? { chromeExecutablePath: chrome } : {}),
...(cdpConfigured ? { cdpEndpoint: process.env.CHATGPT_WEB_CODEX_CDP_URL } : {}),
headed: false,
proAvailable,
autoApproveToolCalls: false,
});
if (login) {
try {
const marker = JSON.parse(
readFileSync(`${paths.storageStatePath}.verified.json`, "utf8")
) as Record<string, unknown>;
if (typeof marker.solAvailable === "boolean") solAvailable = marker.solAvailable;
if (typeof marker.proAvailable === "boolean") proAvailable = marker.proAvailable;
} catch {
// Marker detail is optional.
@@ -97,7 +105,6 @@ export async function getChatGptWebCodexDoctorStatus(connection: {
toolRoundtrip: { ready: tunnel.ok && runtime.brokers > 0 },
runtime,
lease,
solAvailable,
proAvailable,
recovery: {
interactiveLoginRequired: storageState && !login,

View File

@@ -2,29 +2,16 @@ export type ChatGptWebCodexEffort = "low" | "medium" | "high" | "xhigh" | "max";
export interface ChatGptWebCodexModelRoute {
id: string;
backendModel: "gpt-5.6-sol" | "gpt-5.6-luna";
effort: ChatGptWebCodexEffort;
pro: boolean;
sol: boolean;
}
const ROUTES = new Map<string, ChatGptWebCodexModelRoute>([
["luna", { id: "luna", backendModel: "gpt-5.6-luna", effort: "low", pro: false, sol: false }],
[
"think",
{ id: "think", backendModel: "gpt-5.6-luna", effort: "medium", pro: false, sol: false },
],
["instant", { id: "instant", backendModel: "gpt-5.6-sol", effort: "low", pro: false, sol: true }],
[
"medium",
{ id: "medium", backendModel: "gpt-5.6-sol", effort: "medium", pro: false, sol: true },
],
["high", { id: "high", backendModel: "gpt-5.6-sol", effort: "high", pro: false, sol: true }],
[
"extra-high",
{ id: "extra-high", backendModel: "gpt-5.6-sol", effort: "xhigh", pro: true, sol: true },
],
["pro", { id: "pro", backendModel: "gpt-5.6-sol", effort: "max", pro: true, sol: true }],
["instant", { id: "instant", effort: "low", pro: false }],
["medium", { id: "medium", effort: "medium", pro: false }],
["high", { id: "high", effort: "high", pro: false }],
["extra-high", { id: "extra-high", effort: "xhigh", pro: false }],
["pro", { id: "pro", effort: "max", pro: true }],
]);
export function requireChatGptWebCodexRoute(model: string): ChatGptWebCodexModelRoute {

View File

@@ -16,7 +16,6 @@ export function connectionRuntimePaths(connectionId: string) {
storageStatePath: join(root, "storage-state.json"),
brokerSocketPath: join(getConfigDir(), "runtime", "turn-broker.sock"),
threadEnvironmentStatePath: join(root, "thread-environments.json"),
lunaCheckpointStatePath: join(root, "luna-checkpoints.json"),
};
}
@@ -42,9 +41,8 @@ function parseCookies(raw: string): Array<Record<string, unknown>> {
return pairs.map(([name, value]) => ({
name,
value,
domain: name.startsWith("__Host-") ? "chatgpt.com" : ".chatgpt.com",
domain: ".chatgpt.com",
path: "/",
expires: -1,
secure: true,
httpOnly: name.startsWith("__Secure-") || name.startsWith("__Host-"),
sameSite: "Lax",

View File

@@ -1,5 +1,5 @@
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { createHash } from "node:crypto";
import {
chmodSync,
closeSync,
@@ -16,8 +16,7 @@ import { unzipSync } from "fflate";
import { atomicWriteFile, getConfigDir } from "../../vendor/codex-chatgpt-web/config.ts";
export const CHATGPT_WEB_CODEX_TUNNEL_VERSION = "0.0.13";
const MIGRATABLE_TUNNEL_VERSIONS = new Set(["0.0.10", "0.0.12"]);
export const CHATGPT_WEB_CODEX_TUNNEL_VERSION = "0.0.10";
const RELEASE_BASE = `https://github.com/openai/tunnel-client/releases/download/v${CHATGPT_WEB_CODEX_TUNNEL_VERSION}`;
const MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
@@ -56,14 +55,6 @@ function sha256(bytes: Uint8Array): string {
return createHash("sha256").update(bytes).digest("hex");
}
export function tunnelClientInstallAction(installedVersion: string): "reuse" | "upgrade" {
if (installedVersion === CHATGPT_WEB_CODEX_TUNNEL_VERSION) return "reuse";
if (MIGRATABLE_TUNNEL_VERSIONS.has(installedVersion)) return "upgrade";
throw new Error(
`Installed tunnel-client version ${installedVersion} is not a trusted upgrade source`
);
}
export function tunnelPlatformAsset(platform = process.platform, arch = process.arch): string {
const os =
platform === "darwin"
@@ -207,63 +198,20 @@ export function releaseTunnelSupervisorLease(): void {
ownsSupervisorLease = false;
}
type TunnelClientPaths = ReturnType<typeof tunnelClientPaths>;
type PreviousInstallation = { binary: Uint8Array; manifestText: string };
function requireReportedTunnelVersion(
binary: string,
expectedVersion: string,
errorMessage: string
): void {
const version = spawnSync(binary, ["--version"], { encoding: "utf8" });
if (version.status !== 0 || !`${version.stdout}\n${version.stderr}`.includes(expectedVersion)) {
throw new Error(errorMessage);
}
}
function inspectExistingTunnelInstallation(
paths: TunnelClientPaths
): { action: "reuse" | "upgrade"; previousInstallation: PreviousInstallation } | undefined {
if (!existsSync(paths.binary) || !existsSync(paths.manifest)) return undefined;
const manifestText = readFileSync(paths.manifest, "utf8");
const manifest = JSON.parse(manifestText) as Partial<InstallManifest>;
const installedBinary = new Uint8Array(readFileSync(paths.binary));
const actual = sha256(installedBinary);
if (
manifest.version !== 1 ||
typeof manifest.tunnelClientVersion !== "string" ||
manifest.binarySha256 !== actual
) {
throw new Error("Existing tunnel-client failed integrity validation");
}
requireReportedTunnelVersion(
paths.binary,
manifest.tunnelClientVersion,
`Existing tunnel-client did not report version ${manifest.tunnelClientVersion}`
);
return {
action: tunnelClientInstallAction(manifest.tunnelClientVersion),
previousInstallation: { binary: installedBinary, manifestText },
};
}
function restoreTunnelInstallation(
paths: TunnelClientPaths,
previousInstallation: PreviousInstallation | undefined
): void {
if (!previousInstallation) return;
atomicWriteFile(paths.binary, previousInstallation.binary);
if (process.platform !== "win32") chmodSync(paths.binary, 0o700);
atomicWriteFile(paths.manifest, previousInstallation.manifestText);
}
export async function ensureTunnelClientInstalled(): Promise<string> {
const paths = tunnelClientPaths();
const existing = inspectExistingTunnelInstallation(paths);
if (existing?.action === "reuse") return paths.binary;
const previousInstallation = existing?.previousInstallation;
if (existsSync(paths.binary) && existsSync(paths.manifest)) {
const manifest = JSON.parse(readFileSync(paths.manifest, "utf8")) as Partial<InstallManifest>;
const actual = sha256(readFileSync(paths.binary));
if (
manifest.version === 1 &&
manifest.tunnelClientVersion === CHATGPT_WEB_CODEX_TUNNEL_VERSION &&
manifest.binarySha256 === actual
) {
return paths.binary;
}
throw new Error("Existing tunnel-client failed integrity validation");
}
const asset = tunnelPlatformAsset();
const [archive, checksumFile] = await Promise.all([
@@ -278,18 +226,8 @@ export async function ensureTunnelClientInstalled(): Promise<string> {
const executableName = process.platform === "win32" ? "tunnel-client.exe" : "tunnel-client";
const entry = Object.entries(files).find(([name]) => basename(name) === executableName);
if (!entry) throw new Error(`${asset} does not contain ${executableName}`);
const stagedBinary = `${paths.binary}.install-${process.pid}-${randomUUID()}`;
atomicWriteFile(stagedBinary, entry[1]);
try {
if (process.platform !== "win32") chmodSync(stagedBinary, 0o700);
requireReportedTunnelVersion(
stagedBinary,
CHATGPT_WEB_CODEX_TUNNEL_VERSION,
"Installed tunnel-client did not report the pinned version"
);
} finally {
rmSync(stagedBinary, { force: true });
}
atomicWriteFile(paths.binary, entry[1]);
if (process.platform !== "win32") chmodSync(paths.binary, 0o700);
const manifest: InstallManifest = {
version: 1,
tunnelClientVersion: CHATGPT_WEB_CODEX_TUNNEL_VERSION,
@@ -297,13 +235,14 @@ export async function ensureTunnelClientInstalled(): Promise<string> {
archiveSha256,
binarySha256: sha256(entry[1]),
};
try {
atomicWriteFile(paths.binary, entry[1]);
if (process.platform !== "win32") chmodSync(paths.binary, 0o700);
atomicWriteFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}\n`);
} catch (error) {
restoreTunnelInstallation(paths, previousInstallation);
throw error;
atomicWriteFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}\n`);
const version = spawnSync(paths.binary, ["--version"], { encoding: "utf8" });
if (
version.status !== 0 ||
!`${version.stdout}\n${version.stderr}`.includes(CHATGPT_WEB_CODEX_TUNNEL_VERSION)
) {
throw new Error("Installed tunnel-client did not report the pinned version");
}
return paths.binary;
}
@@ -415,23 +354,27 @@ export function parseTunnelRuntimeStatus(output: string, exitStatus = 0): Tunnel
}
}
export function buildTunnelRuntimeStatusArgs(alias: string): string[] {
return ["runtimes", "status", alias, "--json"];
}
export function buildTunnelRuntimeStopArgs(alias: string): string[] {
return ["runtimes", "stop", alias, "--json"];
}
export async function getTunnelRuntimeStatus(
config: Pick<TunnelRuntimeConfig, "alias" | "profile">
): Promise<TunnelRuntimeStatus> {
const binary = await ensureTunnelClientInstalled();
const paths = tunnelClientPaths();
const alias = config.alias ?? "omniroute-chatgpt-web-codex";
const result = spawnSync(binary, buildTunnelRuntimeStatusArgs(alias), {
encoding: "utf8",
timeout: 5_000,
});
const profile = config.profile ?? "omniroute";
const result = spawnSync(
binary,
[
"runtimes",
"status",
alias,
"--profile",
profile,
"--profile-dir",
paths.profileDir,
"--json",
],
{ encoding: "utf8", timeout: 5_000 }
);
return parseTunnelRuntimeStatus(String(result.stdout || result.stderr || ""), result.status ?? 1);
}
@@ -498,10 +441,20 @@ export function ensureTunnelRuntimeReady(
export async function stopChatGptWebCodexTunnelRuntime(): Promise<void> {
const paths = tunnelClientPaths();
if (ownsSupervisorLease && existsSync(paths.binary)) {
spawnSync(paths.binary, buildTunnelRuntimeStopArgs("omniroute-chatgpt-web-codex"), {
encoding: "utf8",
timeout: 10_000,
});
spawnSync(
paths.binary,
[
"runtimes",
"stop",
"omniroute-chatgpt-web-codex",
"--profile",
"omniroute",
"--profile-dir",
paths.profileDir,
"--json",
],
{ encoding: "utf8", timeout: 10_000 }
);
}
connectedRuntimes.clear();
for (const runtimeKeyFile of runtimeKeyFiles) rmSync(runtimeKeyFile, { force: true });

View File

@@ -939,8 +939,8 @@ export class GrokWebExecutor extends BaseExecutor {
// Fetch from Grok via TLS-impersonating client (#3180).
// Grok sits behind Cloudflare Enterprise which rejects Node's native TLS
// fingerprint even with valid sso+sso-rw cookies. The pinned wreq-js
// transport sends a Chrome-like handshake instead.
// fingerprint even with valid sso+sso-rw cookies. We use tls-client-node
// to send a Chrome-like handshake instead.
let tlsResult: TlsFetchResult;
try {
tlsResult = await tlsFetchGrok(GROK_CHAT_API, {

View File

@@ -2,8 +2,8 @@
* LMArenaExecutor — Arena (formerly LMArena) web-session provider.
*
* Routes requests through arena.ai create-evaluation with session cookies.
* Upstream sits behind Cloudflare; traffic goes through wreq-js Chrome
* impersonation with isolated ephemeral cookies (see services/lmarenaTlsClient.ts).
* Upstream sits behind Cloudflare; traffic goes through tls-client-node Chrome
* impersonation (see services/lmarenaTlsClient.ts).
*
* Helpers: open-sse/executors/lmarena/{cookie,models,stream,response}.ts
*/
@@ -174,6 +174,7 @@ export class LMArenaExecutor extends BaseExecutor {
body: JSON.stringify(transformedBody),
signal: ctx.signal,
stream: ctx.stream,
streamEofSymbol: "__OMNIROUTE_LMARENA_EOF_NEVER__",
});
const failed = mapFailedTlsResult({

View File

@@ -6,9 +6,9 @@ export const LMARENA_API_BASE = "https://arena.ai";
export const LMARENA_STREAM_URL = `${LMARENA_API_BASE}/nextjs-api/stream/create-evaluation`;
/**
* Current Chrome stable UA (header surface).
* TLS JA3/JA4 profile is separate: the provider-tested wreq-js profile is pinned
* to chrome_146 in lmarenaTlsClient.ts while headers track the live browser string.
* Treat that deliberate version skew as a WAF-sensitive compatibility surface.
* TLS JA3 profile is separate: tls-client-node tops out at chrome_146 — see
* LMARENA_PROFILE in lmarenaTlsClient.ts. Headers track the live browser string;
* fingerprint stays at the newest native profile we can actually impersonate.
*/
export const LMARENA_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";

View File

@@ -114,7 +114,7 @@ export function mapTlsUnavailable(
return {
response: errorResponse(
502,
`Arena TLS impersonation unavailable: ${error.message}. Verify the wreq-js 3.0.0 native addon.`,
`Arena TLS impersonation unavailable: ${error.message}. Install/repair tls-client-node native binary.`,
"upstream_error",
"TLS_CLIENT_UNAVAILABLE"
),

View File

@@ -22,7 +22,7 @@
* chunk — safer than assuming unverified incremental-delta semantics.
*
* Auth: Cookie-based (token_v2 [+ optional space_id, notion_browser_id, user_id])
* Method: Browser-TLS impersonation via pinned wreq-js (Chrome JA3/JA4). Plain
* Method: Browser-TLS impersonation via tls-client-node (Chrome JA3). Plain
* Node/undici fetch is rejected by Notion's edge with in-band
* `temporarily-unavailable` (HTTP 200, empty assistant text) — curl/Schannel
* and Chrome work with the same cookie + body. See services/notionTlsClient.ts.
@@ -60,7 +60,10 @@ import {
messagesForNotionTranscript,
type NotionAgentOptions,
} from "../services/notionTranscriptBuilder.ts";
import { tlsFetchNotion, TlsClientUnavailableError } from "../services/notionTlsClient.ts";
import {
tlsFetchNotion,
TlsClientUnavailableError,
} from "../services/notionTlsClient.ts";
// Re-exported for unit tests that destructure `mod.<name>` on this module.
export {
@@ -222,6 +225,7 @@ function extractUserIdFromCookie(cookie: string): string {
return extractNotionUserIdFromCookie(cookie);
}
/**
* Notion's undocumented inference API does not return token usage.
* Emit a cheap char-based estimate so clients don't see a constant
@@ -232,7 +236,9 @@ export function estimateNotionUsage(
messages: NotionMessage[] | undefined,
content: string
): { prompt_tokens: number; completion_tokens: number; total_tokens: number; estimated: true } {
const promptText = (messages || []).map((m) => extractNotionMessageText(m?.content)).join("\n");
const promptText = (messages || [])
.map((m) => extractNotionMessageText(m?.content))
.join("\n");
// ~4 chars/token (English-ish); at least 1 when there is any text.
const prompt_tokens = promptText ? Math.max(1, Math.ceil(promptText.length / 4)) : 0;
const completion_tokens = content ? Math.max(1, Math.ceil(content.length / 4)) : 0;
@@ -387,8 +393,9 @@ function buildNotionExecuteHeaders(opts: {
const isCustom = Boolean(opts.agent?.workflowId);
// Browser uses /agent/<workflowId without dashes>?wfv=chat for custom agents.
const agentPathId = (opts.agent?.workflowId || "").replace(/-/g, "");
const referer =
isCustom && agentPathId ? `${BASE_URL}/agent/${agentPathId}?wfv=chat` : `${BASE_URL}/ai`;
const referer = isCustom && agentPathId
? `${BASE_URL}/agent/${agentPathId}?wfv=chat`
: `${BASE_URL}/ai`;
const reqHeaders: Record<string, string> = {
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
@@ -446,8 +453,11 @@ export function resolveNotionAgentOptions(
"agent_id",
]) || "";
const pageFromPs =
readProviderSpecificString(ps, ["contextPageId", "context_page_id", "notionContextPageId"]) ||
"";
readProviderSpecificString(ps, [
"contextPageId",
"context_page_id",
"notionContextPageId",
]) || "";
const readCookie = (name: string): string => {
const m = cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`, "i"));
@@ -467,7 +477,10 @@ export function resolveNotionAgentOptions(
readCookie("agent_id")
);
const contextPageId =
pageFromPs || readCookie("context_page_id") || readCookie("notion_context_page_id") || "";
pageFromPs ||
readCookie("context_page_id") ||
readCookie("notion_context_page_id") ||
"";
return {
workflowId: workflowId || undefined,
@@ -497,7 +510,8 @@ async function sendNotionInferenceRequest(opts: {
body: JSON.stringify(reqBody),
signal: signal ?? undefined,
// Inference can take a while (tool-autoload + LLM first token).
timeoutMs: Number.parseInt(process.env.OMNIROUTE_NOTION_TLS_TIMEOUT_MS || "", 10) || 180_000,
timeoutMs:
Number.parseInt(process.env.OMNIROUTE_NOTION_TLS_TIMEOUT_MS || "", 10) || 180_000,
});
status = tlsRes.status;
rawText = tlsRes.text ?? "";
@@ -620,7 +634,8 @@ export class NotionWebExecutor extends BaseExecutor {
const inboundHeaders =
(input.clientHeaders as Record<string, string> | null | undefined) ??
((input as { headers?: Record<string, string> }).headers as
Record<string, string> | undefined);
| Record<string, string>
| undefined);
const clientThreadId = readClientThreadId(requestBody, inboundHeaders ?? undefined);
// Namespace the thread cache PER CALLER (hash of the caller's cookie) AND by custom
// agent, so (a) two users of the same Notion space never share a cached thread
@@ -723,10 +738,7 @@ export class NotionWebExecutor extends BaseExecutor {
// One automatic retry for transient Notion faults — same threadId, never create again
if (isFailedAttempt(attempt) && attempt.retryable) {
const delayMs =
process.env.NODE_ENV === "test" || process.env.VITEST
? 20
: 700 + Math.floor(Math.random() * 400);
const delayMs = process.env.NODE_ENV === "test" || process.env.VITEST ? 20 : 700 + Math.floor(Math.random() * 400);
await new Promise((r) => setTimeout(r, delayMs));
attempt = await runOnce({ createThread: false, threadId });
}

View File

@@ -501,7 +501,7 @@ export class PerplexityWebExecutor extends BaseExecutor {
if (isCloudflareChallenge(response.text)) {
errMsg =
"Cloudflare blocked the request — Perplexity's edge rejected this server's TLS fingerprint " +
"(common on VPS/datacenter IPs). Verify the wreq-js 3.0.0 native addon, " +
"(common on VPS/datacenter IPs). Ensure tls-client-node is installed with its native binary, " +
"or route perplexity-web through a residential proxy.";
log?.error?.("PPLX-WEB", "Cloudflare challenge detected — TLS bypass failed");
} else {

View File

@@ -1580,12 +1580,12 @@ export async function handleChatCore({
await import("../services/compression/outputStyles/backCompat.ts");
const selection = resolveOutputStyleSelection(config);
if (selection.length > 0) {
const { applyOutputStyles, resolveOutputStyleLanguage } =
const { applyOutputStyles } =
await import("../services/compression/outputStyles/apply.ts");
const outputStyleLanguage = resolveOutputStyleLanguage(
config.languageConfig,
body as Parameters<typeof resolveOutputStyleLanguage>[1]
);
const outputStyleLanguage =
config.languageConfig?.enabled === true
? config.languageConfig.defaultLanguage
: "en";
outputStyleResult = applyOutputStyles(
body as Parameters<typeof applyOutputStyles>[0],
selection,
@@ -2731,7 +2731,6 @@ export async function handleChatCore({
const previousResponseIdPolicy = applyResponsesPreviousResponseIdPolicy(translatedBody, {
mode: settings.responsesPreviousResponseIdMode,
provider,
sourceFormat,
targetFormat,
credentials,

View File

@@ -1,9 +1,5 @@
import { retrieveMemories } from "@/lib/memory/retrieval";
import {
getMemorySettings,
DEFAULT_MEMORY_SETTINGS,
toMemoryRetrievalConfig,
} from "@/lib/memory/settings";
import { getMemorySettings, DEFAULT_MEMORY_SETTINGS, toMemoryRetrievalConfig } from "@/lib/memory/settings";
import { injectMemory, shouldInjectMemory } from "@/lib/memory/injection";
import { injectSkills } from "@/lib/skills/injection";
import { buildMemoryToolsForProvider } from "@/lib/skills/memoryBuiltins";
@@ -13,25 +9,7 @@ import { detectCachingContext } from "../../services/compression/cachingAware.ts
type MemorySkillsLogger = { debug?: (...args: unknown[]) => void } | null | undefined;
function getToolName(tool: unknown): string {
if (!tool || typeof tool !== "object") return "";
const r = tool as Record<string, unknown>;
if (typeof r.name === "string") return r.name;
if (r.function && typeof r.function === "object") {
const fn = r.function as Record<string, unknown>;
if (typeof fn.name === "string") return fn.name;
}
return "";
}
export function sortToolsByName<T>(tools: T[]): T[] {
if (!Array.isArray(tools) || tools.length <= 1) return tools;
return [...tools].sort((a, b) => getToolName(a).localeCompare(getToolName(b)));
}
export function getSkillsProviderForFormat(
format: string
): "openai" | "anthropic" | "google" | "other" {
export function getSkillsProviderForFormat(format: string): "openai" | "anthropic" | "google" | "other" {
switch (format) {
case FORMATS.CLAUDE:
return "anthropic";
@@ -123,7 +101,7 @@ export async function injectMemoryAndSkills({
}
return "";
}
if (Array.isArray(body.messages)) {
const r = pickFrom(body.messages);
if (r) return r;
@@ -182,7 +160,8 @@ export async function injectMemoryAndSkills({
getSkillsProviderForFormat(sourceFormat)
).filter((tool) => {
const record = tool as Record<string, unknown>;
const name = (record.function as Record<string, unknown> | undefined)?.name ?? record.name;
const name =
(record.function as Record<string, unknown> | undefined)?.name ?? record.name;
return typeof name === "string" && !existingToolNames.has(name);
});
if (memoryTools.length > 0) {
@@ -229,12 +208,5 @@ export async function injectMemoryAndSkills({
}
}
if (Array.isArray(body.tools) && body.tools.length > 1) {
body = {
...body,
tools: sortToolsByName(body.tools),
};
}
return { body, memorySettings };
}

View File

@@ -1,5 +1,6 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Worker } from "node:worker_threads";
import { findDeepSeekPowNonce, MAX_DEEPSEEK_POW_DIFFICULTY } from "./deepseek-pow-hash.js";
@@ -73,21 +74,10 @@ function solveSynchronously({ challenge, prefix, difficulty }: ValidatedChalleng
return findDeepSeekPowNonce(prefix, challenge, difficulty);
}
// Anchored on process.cwd(), never import.meta.url: the production standalone bundle
// freezes import.meta.url to the build-machine path (same app-wide gotcha documented on
// GATE_DEP_REL in open-sse/services/compression/engines/llmlingua/worker.ts), so an
// import.meta.url-relative fallback here was silently dead in production. It also gave
// this function two return branches, one of which contained a `new URL(literal,
// import.meta.url)` construct -- Turbopack's dev-mode static worker-chunk detector
// partially resolves that pattern independent of which branch actually runs, producing
// an inconsistent module-graph node and crashing turbo-tasks on startup (bisected to
// 657d3a484). A single non-branching path resolution avoids both problems.
function resolveWorkerPath(): string {
const workerPath = path.join(process.cwd(), "open-sse/lib/deepseek-pow-worker.mjs");
if (!existsSync(workerPath)) {
throw new Error(`DeepSeek PoW worker script not found at ${workerPath}`);
}
return workerPath;
const tracedPath = path.join(process.cwd(), "open-sse/lib/deepseek-pow-worker.mjs");
if (existsSync(tracedPath)) return tracedPath;
return fileURLToPath(new URL("./deepseek-pow-worker.mjs", import.meta.url));
}
function solveInWorker(

View File

@@ -1,17 +1,16 @@
/**
* Regression tests for the proxy-leak fix in grokTlsClient.
*
* Bug context (#3180): tlsFetchGrok() built its native transport options
* without a `proxyUrl` field, so every grok-web call
* Bug context (#3180): tlsFetchGrok() built its native tls-client-node
* requestOptions without a `proxyUrl` field, so every grok-web call
* egressed with the bare host IP regardless of the dashboard proxy config
* or HTTP_PROXY / HTTPS_PROXY env vars. Native browser transports require the
* resolved proxy to be passed explicitly.
* or HTTP_PROXY / HTTPS_PROXY env vars (the koffi-loaded Go binary does not
* consult Go's `http.ProxyFromEnvironment`).
*
* These tests pin the resolution-order contract:
* 1. Per-call `options.proxyUrl` wins.
* 2. Request-scoped dashboard/account proxy context.
* 3. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
* 4. Otherwise undefined (no proxy).
* 2. POSIX-standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (and lowercase variants).
* 3. Otherwise undefined (no proxy).
*
* They also pin that the resolved proxy is actually placed on the
* requestOptions object handed to the native binding — the original bug

View File

@@ -600,7 +600,9 @@ export function shouldDeferAntigravityQuotaStateToCaller(
hasCallerOwner: boolean
): boolean {
const canonicalProvider = getCanonicalLockProvider(provider);
return hasCallerOwner && (canonicalProvider === "antigravity" || canonicalProvider === "agy");
return (
hasCallerOwner && (canonicalProvider === "antigravity" || canonicalProvider === "agy")
);
}
export async function recordCoreOwnedAntigravityQuotaState({
@@ -621,7 +623,15 @@ export async function recordCoreOwnedAntigravityQuotaState({
profileOverride?: ProviderProfile | null;
}) {
const profile = profileOverride ?? (await getRuntimeProviderProfile(provider));
const fallback = checkFallbackError(status, errorText, 0, model, provider, headers, profile);
const fallback = checkFallbackError(
status,
errorText,
0,
model,
provider,
headers,
profile
);
const lockout = recordModelLockoutFailure(
provider,
connectionId,
@@ -637,7 +647,9 @@ export async function recordCoreOwnedAntigravityQuotaState({
: (fallback.quotaResetHintMs ?? null),
maxCooldownMs: profile.maxCooldownMs,
scope: "exact",
exactCooldownIsUpstreamReset: retryHintBypassesMaxCooldownMs(fallback.retryHintSource),
exactCooldownIsUpstreamReset: retryHintBypassesMaxCooldownMs(
fallback.retryHintSource
),
}
);
return { cooldownMs: lockout.cooldownMs, failureCount: lockout.failureCount };
@@ -1681,18 +1693,6 @@ export function checkFallbackError(
};
}
const previousResponseBindingMiss =
structuredError?.code === "invalid_previous_response_binding" ||
(status === 409 && /previous_response_id does not belong/i.test(String(errorText || "")));
if (previousResponseBindingMiss) {
return {
shouldFallback: false,
cooldownMs: 0,
reason: "invalid_previous_response_binding",
skipProviderBreaker: true,
};
}
const svc = serviceSupervisorCooldown(status, headers);
if (svc) return svc;
const rg = rot.gateFor(status, rotation?.account);
@@ -1753,7 +1753,10 @@ export function checkFallbackError(
if (waitMs > 0) return { retryAfterMs: waitMs, provenance: "header" };
}
const detailedJsonHint = parseDetailedRetryHintFromJsonBody(errorStr, MAX_PROVIDER_COOLDOWN_MS);
const detailedJsonHint = parseDetailedRetryHintFromJsonBody(
errorStr,
MAX_PROVIDER_COOLDOWN_MS
);
if (detailedJsonHint) {
return {
retryAfterMs: detailedJsonHint.retryAfterMs,

View File

@@ -123,10 +123,7 @@ async function resolveCloakLaunch(): Promise<((opts: unknown) => Promise<Browser
if (state.cloakLaunchResolved) return state.cloakLaunch;
state.cloakLaunchResolved = true;
try {
const mod = (await import(
/* webpackIgnore: true */
getCloakbrowserModuleId()
)) as unknown as {
const mod = (await import(getCloakbrowserModuleId())) as unknown as {
launch?: (opts: unknown) => Promise<Browser>;
};
state.cloakLaunch = mod.launch ?? null;

View File

@@ -2,8 +2,8 @@
* Browser-TLS-impersonating HTTP client for claude.ai.
*
* Thin re-export over the shared `tlsClientBase.ts` factory
* (`createTlsClientModule`). All provider-agnostic logic (wreq-js transport
* pooling, direct streaming, proxy resolution, deadlines, SSE detection) lives
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
* streaming tail-file, proxy resolution, error classes, SSE detection) lives
* in the base module; this file supplies only Claude-specific config and
* preserves the original public export surface.
*/
@@ -24,13 +24,13 @@ const HARD_TIMEOUT_GRACE_MS =
export const tlsClientModule = createTlsClientModule({
providerName: "Claude",
tlsProfile: `chrome_${CLAUDE_TLS_BROWSER_MAJOR_VERSION}`,
emulationOs: "linux",
domain: "https://claude.ai",
streamEofPolicy: "include",
tempDirPrefix: "cgpt-stream-",
tailFileVariant: "A",
responseValidation: "sse",
exportCloudflareCheck: false,
exposeStreamingForTesting: true,
// Claude allows the native/hard request deadline to bound a slow first SSE byte.
// Claude waits indefinitely for the first SSE byte (original 2-arg waitForContent).
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
hardTimeoutGraceMs: HARD_TIMEOUT_GRACE_MS,
firstByteTimeoutMs: Number.POSITIVE_INFINITY,

View File

@@ -7,7 +7,7 @@
* 3. Waits for Turnstile challenge to appear
* 4. Waits for challenge to be solved (with retry)
* 5. Extracts cf_clearance cookie
* 6. Returns a fresh cookie for the isolated wreq-js request
* 6. Returns fresh cookie for tls-client-node
*/
import type { Browser, Page } from "playwright";

View File

@@ -231,8 +231,6 @@ export {
import { applyComboTargetExhaustion } from "./combo/targetExhaustion.ts";
import {
applyNativeCodexTurnPin,
areAllPinnedTargetsModelScopedUnusable,
createPinnedModelUnavailableResponse,
getNativeCodexTurnPin,
pinNativeCodexTurn,
} from "./combo/nativeCodexTurnPin.ts";
@@ -962,49 +960,30 @@ async function handleComboChatInner({
const { stickyWeightedLimit, getWeightedStepKeyForTarget, preScreenMap } = targetResolution;
const _sticky = targetResolution.sticky;
let orderedTargets = targetResolution.orderedTargets;
const quotaCutoffResetWindowConfig = resolveResetWindowConfig(config as Record<string, unknown>);
if (activeNativeTurnPin) {
const pinnedTargets = applyNativeCodexTurnPin(orderedTargets, activeNativeTurnPin);
if (pinnedTargets.length === 0) {
//#11371: quota-share ordering reserved a winner slot; release on
//early exit (idempotent).
orderedTargets = applyNativeCodexTurnPin(orderedTargets, activeNativeTurnPin);
if (orderedTargets.length === 0) {
// #11371: quota-share ordering already reserved a winner slot; release it on
// this early exit (idempotent).
targetResolution.quotaShareRelease?.();
log.warn(
"COMBO",
`Native Codex turn cannot continue: pinned model ${activeNativeTurnPin.modelStr} unavailable (target not in combo); preserving turn pin and terminating turn`
);
return createPinnedModelUnavailableResponse();
}
const allPinnedUnusable = await areAllPinnedTargetsModelScopedUnusable({
pinnedTargets,
resilienceSettings,
quotaCutoffResetWindowConfig,
comboName: combo.name,
body: body as Record<string, unknown>,
log,
isModelAvailable,
});
if (allPinnedUnusable) {
targetResolution.quotaShareRelease?.();
log.warn(
"COMBO",
`Native Codex turn cannot continue: pinned model ${activeNativeTurnPin.modelStr} is unavailable (model-scoped); preserving turn pin and terminating turn`
);
return createPinnedModelUnavailableResponse();
} else {
orderedTargets = pinnedTargets;
log.info(
"COMBO",
`Native Codex turn pinned to ${activeNativeTurnPin.modelStr} on connection ${activeNativeTurnPin.connectionId.slice(0, 8)}`
return errorResponse(
409,
"The pinned native Codex turn target is no longer available; the turn cannot be moved to another provider"
);
}
log.info(
"COMBO",
`Native Codex turn pinned to ${activeNativeTurnPin.modelStr} connection ${activeNativeTurnPin.connectionId.slice(0, 8)}`
);
}
// #5923 (Finding #4) — reset-window config for the shared per-target quota-
// exhaustion cutoff below. The "auto" strategy already applies its own cutoff
// via buildAutoCandidates/routableCandidates, so this only affects the other
// 16 strategies (priority, weighted, etc.) that funnel through executeTarget.
const quotaCutoffResetWindowConfig = resolveResetWindowConfig(config as Record<string, unknown>);
// QA P0 diagnostics: record the order in which targets were actually attempted
// (provider/model ids only) so a terminal combo failure can report the attempt
// sequence alongside pool size + exhaustion reasons. Accumulates across set retries.
const comboAttemptOrder: Array<{ provider: string; model: string }> = [];
@@ -1286,8 +1265,7 @@ async function handleComboChatInner({
if (
resilienceSettings.providerCooldown.enabled &&
Boolean(provider && provider !== "unknown") &&
(isProviderInCooldown(provider, target.connectionId ?? undefined, resilienceSettings) ||
isProviderInCooldown(provider, undefined, resilienceSettings))
isProviderInCooldown(provider, target.connectionId ?? undefined, resilienceSettings)
) {
log.info("COMBO", `Skipping ${modelStr} — provider ${provider} in global cooldown`);
recordComboDecision(traceInvocationId, {

View File

@@ -10,7 +10,7 @@ import { EXECUTOR_CONTRACT_VIOLATION_CODE } from "../../config/constants.ts";
import { errorResponse } from "../../utils/error.ts";
import { parseModel } from "../model.ts";
import { isSelfInflictedUpstreamTimeout } from "../../handlers/chatCore/cooldownClassification.ts";
import { isLocalStreamLifecycleError, isLocalExecutionError } from "@/shared/utils/circuitBreaker";
import { isLocalStreamLifecycleError } from "@/shared/utils/circuitBreaker";
import { CONTEXT_OVERFLOW_PATTERNS, MODEL_ACCESS_DENIED_PATTERNS } from "../accountFallback.ts";
import { isResourceNotFoundResponse } from "../errorClassifier.ts";
import { getTrustedLocalRateLimitResponse } from "../rateLimitManager/errors.ts";
@@ -216,8 +216,7 @@ export function shouldRecordProviderBreakerFailure(args: {
(!args.sameProviderNext || args.isProxyUnreachable === true) &&
!args.skipProviderBreaker &&
!args.requestScopedFailure &&
!isLocalStreamLifecycleError(args.error) &&
!isLocalExecutionError(args.error)
!isLocalStreamLifecycleError(args.error)
);
}
@@ -314,7 +313,6 @@ export function shouldSkipConnDisable(
// Client abort surfaced as a bare error (no statusCode → defaults to 502):
// a local lifecycle event, not a provider failure (#4602 policy).
isLocalStreamLifecycleError(result.error) ||
isLocalExecutionError(result.error) ||
(result.response ? getTrustedLocalRateLimitResponse(result.response) !== null : false) ||
result.errorCode === "plugin_block" ||
result.errorType === "plugin_block" ||
@@ -472,30 +470,6 @@ export function hasFutureRateLimitUntil(value: unknown): boolean {
return Number.isFinite(time) && time > Date.now();
}
/**
* #12168: mirrors ERROR_LABEL_GRACE_MS in src/lib/quota/connectionRecovery.ts — a
* bare status label with no cooldown timestamp is only trusted while the failure
* that wrote it is recent. Kept in sync with that constant deliberately: both
* answer the same question ("is this label still meaningful?") and they must not
* disagree, or a connection the recovery job considers healthy would still be
* pre-skipped by combo dispatch.
*/
const UNAVAILABLE_LABEL_GRACE_MS = 60 * 1000;
/**
* True when a bare `unavailable` label should still be honoured: the recorded
* failure is inside the grace window. A missing/unparseable lastErrorAt is
* treated as stale (not blocking) — an unbounded skip is exactly the failure
* mode #12168 reported, and one extra upstream attempt is far cheaper than a
* permanently dark connection pool.
*/
export function isWithinUnavailableGrace(lastErrorAt: unknown): boolean {
if (lastErrorAt == null || lastErrorAt === "") return false;
const time = new Date(String(lastErrorAt)).getTime();
if (!Number.isFinite(time)) return false;
return Date.now() - time < UNAVAILABLE_LABEL_GRACE_MS;
}
export function getConnectionStatusQuotaCutoffReason(
connection: Record<string, unknown> | undefined
): string | undefined {
@@ -532,28 +506,13 @@ export function getPersistedConnectionCooldownSkipReason(
if (QUOTA_BLOCKING_CONNECTION_STATUSES.has(status)) {
return `Skipping ${target.modelStr} — connection ${target.connectionId} status=${status}`;
}
// `unavailable` with no rateLimitedUntil still means AUTH took this connection out
// of rotation — markAccountUnavailable() writes the status before, and sometimes
// without, a timestamp ("Using zai account …" then a real upstream 429). Without
// this branch a burst still dispatched against a connection AUTH had already retired.
//
// #12168: but the skip must be BOUNDED. The original version returned here for any
// `unavailable` row, which is the raw-label anti-pattern AGENTS.md warns about — the
// resilience layers are supposed to recover lazily. Its stated justification
// ("clearAccountError() resets the status on first success") does not hold on this
// path: this gate runs BEFORE dispatch, so it prevents the very successful request
// that would call clearAccountError(). A connection left with a stale `unavailable`
// label and no timestamp could therefore never dispatch again, and the out-of-band
// recovery job cannot rescue it either — hasElapsedCooldown() there requires a
// rateLimitedUntil to be present. Result: a whole combo pool could report
// ALL_TARGETS_SKIPPED with zero upstream attempts, forever.
//
// Bound it the same way src/lib/quota/connectionRecovery.ts bounds a bare error
// label: honour the skip only while the failure is recent (lastErrorAt within the
// grace window). Past that, treat the label as stale and let the request through —
// one real attempt then either succeeds (clearing the status) or re-arms the
// cooldown with a fresh timestamp.
if (status === "unavailable" && isWithinUnavailableGrace(connection.lastErrorAt)) {
// `unavailable` with no (or an already-expired) rateLimitedUntil still means AUTH
// took this connection out of rotation — markAccountUnavailable() writes the status
// before, and sometimes without, a timestamp ("Using zai account …" then a real
// upstream 429). Without this branch the pre-skip only fired once the timestamp had
// landed, so a burst still dispatched against a connection AUTH had already retired.
// Lazy recovery is unaffected: clearAccountError() resets the status on first success.
if (status === "unavailable") {
return `Skipping ${target.modelStr} — connection ${target.connectionId} status=unavailable`;
}
return null;

View File

@@ -1,15 +1,4 @@
import { createHash } from "node:crypto";
import { buildErrorBody } from "../../utils/error.ts";
import { isModelLocked, hasPerModelQuota } from "../accountFallback.ts";
import { isProviderInCooldown } from "../providerCooldownTracker.ts";
import { getCircuitBreaker } from "../../../src/shared/utils/circuitBreaker.ts";
import type { ResilienceSettings } from "../../../src/lib/resilience/settings";
import { checkCredentialGate } from "../credentialGate.ts";
import { canAffordRequest } from "../../../src/lib/quota/quotaScheduler.ts";
import { resolveQuotaExhaustionCutoffForTarget } from "./quotaExhaustionCutoff.ts";
import type { ResetWindowConfig } from "./quotaScoring.ts";
import { parseModel } from "../model.ts";
import type { ComboLogger, IsModelAvailable } from "./types.ts";
import type { ResolvedComboTarget } from "./types.ts";
@@ -161,124 +150,6 @@ export function revokeNativeCodexTurnPinsForConnection(connectionId: string): nu
return revoked;
}
export const NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE_CODE = "NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE";
export const NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE_MESSAGE =
"The model handling this native Codex turn is no longer available. This turn cannot switch providers or models after output has been emitted. Start a new turn to allow Combo routing to select another model.";
export function createPinnedModelUnavailableResponse(): Response {
const body = buildErrorBody(400, NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE_MESSAGE, undefined, {
code: NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE_CODE,
type: "invalid_request_error",
});
return new Response(JSON.stringify(body), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
export interface CheckPinnedTargetsModelScopedUnusableOptions {
pinnedTargets: ResolvedComboTarget[];
resilienceSettings?: ResilienceSettings | null;
quotaCutoffResetWindowConfig?: ResetWindowConfig;
comboName: string;
body: Record<string, unknown>;
log?: ComboLogger;
isModelAvailable?: IsModelAvailable;
}
export async function isPinnedTargetModelScopedUnusable(args: {
target: ResolvedComboTarget;
resilienceSettings?: ResilienceSettings | null;
quotaCutoffResetWindowConfig?: ResetWindowConfig;
comboName: string;
body: Record<string, unknown>;
log?: ComboLogger;
isModelAvailable?: IsModelAvailable;
}): Promise<boolean> {
const {
target,
resilienceSettings,
quotaCutoffResetWindowConfig,
comboName,
body,
log,
isModelAvailable,
} = args;
const provider = target.provider;
const connectionId = target.connectionId || "";
const rawModel = parseModel(target.modelStr).model || target.modelStr;
if (provider && provider !== "unknown") {
const cb = getCircuitBreaker(provider);
if (cb.getStatus().state === "OPEN") return false;
if (
resilienceSettings?.providerCooldown?.enabled &&
(isProviderInCooldown(provider, connectionId || undefined, resilienceSettings) ||
isProviderInCooldown(provider, undefined, resilienceSettings))
) {
return false;
}
}
if (
connectionId &&
checkCredentialGate(connectionId, provider, target.modelStr).allowed === false
) {
return false;
}
if (provider && rawModel && isModelLocked(provider, connectionId, rawModel)) return true;
if (
process.env.OMNIROUTE_QUOTA_AWARE_ROUTING === "1" &&
provider &&
connectionId &&
!canAffordRequest(connectionId, target.modelStr, body).affordable
) {
return true;
}
if (provider && connectionId && quotaCutoffResetWindowConfig) {
const cutoff = await resolveQuotaExhaustionCutoffForTarget(
provider,
connectionId,
resilienceSettings,
quotaCutoffResetWindowConfig,
comboName,
log ?? { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
);
if (cutoff.blocked) return true;
}
if (isModelAvailable) {
const available = await Promise.resolve(isModelAvailable(target.modelStr, target)).catch(
() => true
);
if (
!available &&
provider &&
rawModel &&
(isModelLocked(provider, connectionId, rawModel) || hasPerModelQuota(provider, rawModel))
) {
return true;
}
}
return false;
}
export async function areAllPinnedTargetsModelScopedUnusable(
options: CheckPinnedTargetsModelScopedUnusableOptions
): Promise<boolean> {
if (!options.pinnedTargets?.length) return false;
for (const target of options.pinnedTargets) {
if (!(await isPinnedTargetModelScopedUnusable({ target, ...options }))) {
return false;
}
}
return true;
}
export function clearNativeCodexTurnPinsForTests(): void {
pins.clear();
}

View File

@@ -745,24 +745,6 @@ export async function validateResponseQuality(
// tokens or falls back to a non-reasoning model.
const contentIsEmpty = content === null || content === undefined || content === "";
if (contentIsEmpty && hasReasoningContent && !hasToolCalls) {
// The 90%-of-completion-tokens ratio below is a proxy for "the request was
// truncated mid-reasoning" for providers that don't report finish_reason
// reliably. When finish_reason IS reported as "length" (or the Anthropic-shape
// "max_tokens"), that's a direct, unambiguous signal of truncation — trust it
// over the ratio instead of requiring reasoning to also clear 90%. A response
// truncated at, say, 60% reasoning still has zero usable content for the
// caller. This does not affect the deliberate-tiny-probe case (e.g.
// `max_tokens: 1` connectivity pings, see errorClassifier.ts's
// LEGIT_EMPTY_OPENAI_FINISH): those produce no reasoning_content at all, so
// hasReasoningContent is already false and this branch never runs for them.
const finishReason =
typeof firstChoice.finish_reason === "string" ? firstChoice.finish_reason : "";
if (finishReason === "length" || finishReason === "max_tokens") {
return {
valid: false,
reason: `reasoning truncated at token limit (finish_reason: ${finishReason}) — no content output`,
};
}
const usage = json?.usage as Record<string, unknown> | undefined;
if (usage) {
const completionTokens = Number(usage.completion_tokens) || 0;

View File

@@ -69,21 +69,6 @@ export const CAVEMAN_INSTRUCTION_BY_LANGUAGE = {
full: `Trả lời cộc lốc như người tối cổ thông minh. Bỏ mạo từ, từ đệm, sáo rỗng, rào đón. Chấp nhận câu rút gọn. Dùng từ đồng nghĩa ngắn. Giữ nguyên mọi nội dung kỹ thuật, code, lỗi, URL và định danh. ${SHARED_BOUNDARIES}`,
ultra: `Trả lời cực kỳ cộc lốc. Nén tối đa. Như điện tín. Viết tắt (DB/auth/config/req/res/fn/impl), bỏ liên từ, dùng mũi tên cho quan hệ nhân quả (X → Y). Một từ nếu một từ là đủ. Không bao giờ viết tắt ký hiệu code, tên API, chuỗi lỗi, URL hoặc định danh. ${SHARED_BOUNDARIES}`,
},
it: {
lite: `Rispondi conciso. Togli riempitivi, convenevoli e incertezze. Mantieni termini tecnici, codice, errori, URL e identificatori esatti. ${SHARED_BOUNDARIES}`,
full: `Rispondi secco e compatto. Frammenti OK. Mantieni tutto il contenuto tecnico, codice, errori, URL e identificatori esatti. ${SHARED_BOUNDARIES}`,
ultra: `Rispondi ultra compatto. Usa prosa tecnica breve e abbreviazioni comuni come DB/auth/config/req/res/fn. Mai abbreviare simboli di codice, API, errori, URL o identificatori. ${SHARED_BOUNDARIES}`,
},
ru: {
lite: `Отвечай кратко. Убирай воду, любезности и оговорки. Технические термины, код, ошибки, URL и идентификаторы сохраняй точно. ${SHARED_BOUNDARIES}`,
full: `Отвечай сухо и сжато. Фрагменты допустимы. Всё техническое содержимое, код, ошибки, URL и идентификаторы сохраняй точно. ${SHARED_BOUNDARIES}`,
ultra: `Отвечай ультракратко. Короткая техническая проза и общепринятые сокращения вроде DB/auth/config/req/res/fn. Никогда не сокращай символы кода, API, строки ошибок, URL или идентификаторы. ${SHARED_BOUNDARIES}`,
},
zh: {
lite: `回答要简洁。去掉废话、客套和含糊措辞。技术术语、代码、错误、URL 和标识符保持原样。${SHARED_BOUNDARIES}`,
full: `回答干脆紧凑。可用短句。所有技术内容、代码、错误、URL 和标识符保持原样。${SHARED_BOUNDARIES}`,
ultra: `回答极度紧凑。用简短技术表述和常见缩写如 DB/auth/config/req/res/fn。绝不缩写代码符号、API 名、错误串、URL 或标识符。${SHARED_BOUNDARIES}`,
},
} as const;
const CAVEMAN_OUTPUT_MARKER = "[OmniRoute Caveman Output Mode]";

View File

@@ -1,5 +1,4 @@
import { SHARED_BOUNDARIES, shouldBypassCavemanOutputMode } from "../outputMode.ts";
import { detectCompressionLanguage } from "../languageDetector.ts";
import { OUTPUT_STYLE_IDS, outputStyleMeta } from "./catalog.ts";
export type OutputStyleLevel = "lite" | "full" | "ultra";
@@ -30,51 +29,6 @@ export interface OutputStylesResult {
appliedStyles?: OutputStyleSelectionEntry[];
}
interface OutputStyleLanguageConfig {
enabled?: boolean;
autoDetect?: boolean;
defaultLanguage?: string;
}
function lastUserText(body: ChatRequestBody): string {
const messages = Array.isArray(body.messages) ? body.messages : [];
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message?.role !== "user") continue;
if (typeof message.content === "string" && message.content.trim()) return message.content;
if (Array.isArray(message.content)) {
const text = message.content
.map((part) =>
part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string"
? (part as { text: string }).text
: ""
)
.join(" ")
.trim();
if (text) return text;
}
}
return "";
}
/**
* Resolve which language the output-style instructions inject in.
* Disabled → en. autoDetect → language of the latest user message (the input
* engines already use the same detector); otherwise the configured default.
*/
export function resolveOutputStyleLanguage(
languageConfig: OutputStyleLanguageConfig | undefined,
body: ChatRequestBody
): string {
if (languageConfig?.enabled !== true) return "en";
if (languageConfig.autoDetect === true) {
const text = lastUserText(body);
if (text) return detectCompressionLanguage(text);
}
return languageConfig.defaultLanguage || "en";
}
/** Single idempotency marker guarding the unified injection (D-A: one marker for all styles). */
export const OUTPUT_STYLE_MARKER = "[OmniRoute Output Styles]";

View File

@@ -39,12 +39,6 @@ export const OUTPUT_STYLE_CATALOG: Record<string, OutputStyle> = {
levels: CAVEMAN_INSTRUCTION_BY_LANGUAGE.en,
i18n: {
"pt-BR": CAVEMAN_INSTRUCTION_BY_LANGUAGE["pt-BR"],
es: CAVEMAN_INSTRUCTION_BY_LANGUAGE.es,
de: CAVEMAN_INSTRUCTION_BY_LANGUAGE.de,
fr: CAVEMAN_INSTRUCTION_BY_LANGUAGE.fr,
it: CAVEMAN_INSTRUCTION_BY_LANGUAGE.it,
ru: CAVEMAN_INSTRUCTION_BY_LANGUAGE.ru,
zh: CAVEMAN_INSTRUCTION_BY_LANGUAGE.zh,
ja: CAVEMAN_INSTRUCTION_BY_LANGUAGE.ja,
id: CAVEMAN_INSTRUCTION_BY_LANGUAGE.id,
vi: CAVEMAN_INSTRUCTION_BY_LANGUAGE.vi,
@@ -81,36 +75,6 @@ export const OUTPUT_STYLE_CATALOG: Record<string, OutputStyle> = {
full: `Bertindak seperti dev senior malas yang menerapkan YAGNI. Hanya perubahan terkecil yang berfungsi. Tanpa abstraksi yang tidak diminta, generalisasi prematur, lapisan ekstra, atau scaffolding defensif yang tidak diminta. Pakai ulang kode yang ada daripada menambah kode baru. ${SHARED_BOUNDARIES}`,
ultra: `Disiplin diff minimal. Sentuh baris sesedikit mungkin yang membuatnya berfungsi. Nol file, kelas, atau config baru kecuali sangat diperlukan. Inline daripada abstract. Tanpa tambahan "mumpung di sini". ${SHARED_BOUNDARIES}`,
},
es: {
lite: `Escribe el cambio más pequeño que satisfaga la petición. Evita abstracciones especulativas. ${SHARED_BOUNDARIES}`,
full: `Actúa como un dev senior perezoso aplicando YAGNI. Solo el cambio funcional más pequeño. Sin abstracciones no pedidas, sin generalización prematura, sin capas extra, sin andamiaje defensivo que la petición no pidió. Reutiliza código existente antes que añadir código nuevo. ${SHARED_BOUNDARIES}`,
ultra: `Disciplina de diff mínimo. Toca las menos líneas que lo hagan funcionar. Cero archivos, clases o config nuevos salvo estricta necesidad. Inline antes que abstracto. Sin extras de "ya que estamos". ${SHARED_BOUNDARIES}`,
},
de: {
lite: `Schreibe die kleinste Änderung, die die Anforderung erfüllt. Keine spekulativen Abstraktionen. ${SHARED_BOUNDARIES}`,
full: `Handle wie ein fauler Senior-Entwickler mit YAGNI. Nur die kleinste funktionierende Änderung. Keine unbestellten Abstraktionen, keine vorzeitige Generalisierung, keine Extra-Schichten, kein defensives Gerüst, das die Anforderung nicht verlangt hat. Bestehenden Code wiederverwenden statt neuen hinzufügen. ${SHARED_BOUNDARIES}`,
ultra: `Minimal-Diff-Disziplin. So wenige Zeilen anfassen wie nötig. Null neue Dateien, Klassen oder Config, außer zwingend erforderlich. Inline statt abstrakt. Keine "Wenn wir schon dabei sind"-Extras. ${SHARED_BOUNDARIES}`,
},
fr: {
lite: `Écris le plus petit changement qui satisfait la demande. Évite les abstractions spéculatives. ${SHARED_BOUNDARIES}`,
full: `Agis comme un dev senior paresseux appliquant YAGNI. Uniquement le plus petit changement fonctionnel. Pas d'abstractions non demandées, pas de généralisation prématurée, pas de couches en plus, pas d'échafaudage défensif que la demande n'a pas exigé. Réutilise le code existant plutôt que d'en ajouter. ${SHARED_BOUNDARIES}`,
ultra: `Discipline du diff minimal. Touche le moins de lignes possible pour que ça marche. Zéro nouveau fichier, classe ou config sauf stricte nécessité. Inline plutôt qu'abstrait. Pas d'extras « tant qu'on y est ». ${SHARED_BOUNDARIES}`,
},
it: {
lite: `Scrivi la modifica più piccola che soddisfa la richiesta. Evita astrazioni speculative. ${SHARED_BOUNDARIES}`,
full: `Agisci come un dev senior pigro che applica YAGNI. Solo la modifica funzionante più piccola. Niente astrazioni non richieste, niente generalizzazione prematura, niente strati extra, niente impalcature difensive che la richiesta non ha chiesto. Riusa il codice esistente invece di aggiungerne di nuovo. ${SHARED_BOUNDARIES}`,
ultra: `Disciplina del diff minimo. Tocca il minor numero di righe che lo fa funzionare. Zero nuovi file, classi o config se non strettamente necessari. Inline invece che astratto. Niente extra "già che ci siamo". ${SHARED_BOUNDARIES}`,
},
ru: {
lite: `Пиши наименьшее изменение, которое закрывает запрос. Без спекулятивных абстракций. ${SHARED_BOUNDARIES}`,
full: `Действуй как ленивый сеньор с YAGNI. Только наименьшее работающее изменение. Без незапрошенных абстракций, без преждевременного обобщения, без лишних слоёв, без защитных лесов, которых запрос не требовал. Переиспользуй существующий код вместо добавления нового. ${SHARED_BOUNDARIES}`,
ultra: `Дисциплина минимального diff. Трогай как можно меньше строк. Ноль новых файлов, классов или конфигов без строгой необходимости. Inline вместо абстракции. Без довесков «раз уж мы здесь». ${SHARED_BOUNDARIES}`,
},
zh: {
lite: `写出满足需求的最小改动。跳过投机性的抽象。${SHARED_BOUNDARIES}`,
full: `像一名践行 YAGNI 的懒惰资深开发者。只做最小的可用改动。不写未被要求的抽象,不做过早的泛化,不加多余的层,不搭需求没要的防御性脚手架。优先复用现有代码而不是新增代码。${SHARED_BOUNDARIES}`,
ultra: `最小 diff 纪律。只动让它能工作的最少行数。除非绝对必要,零新文件、新类、新配置。内联优于抽象。不做"顺手再改点"的额外事。${SHARED_BOUNDARIES}`,
},
},
},
// Ponytail (lazy-senior-dev mode) — integrated into the output-style registry
@@ -152,36 +116,6 @@ export const OUTPUT_STYLE_CATALOG: Record<string, OutputStyle> = {
full: `# Ponytail — dev senior malas\n\nKamu adalah senior developer yang malas. Malas = efisien, bukan ceroboh. Kode terbaik adalah kode yang tidak pernah ditulis.\n\nSebelum menulis kode, berhenti di anak tangga pertama yang tepat:\n1. Apakah ini perlu? (YAGNI)\n2. Sudah ada di codebase? Pakai ulang.\n3. Stdlib melakukan ini? Pakai.\n4. Fitur platform atau dep terinstal mencakup? Pakai.\n5. Bisa satu baris? Buat satu baris.\n6. Baru tulis minimum yang bekerja.\n\nPerbaiki bug = akar masalah, bukan gejala. Grep semua pemanggil fungsi yang disentuh; perbaiki fungsi bersama sekali — satu guard di sana lebih kecil daripada satu guard per pemanggil.\n\nAturan:\n- Tanpa abstraksi yang tidak diminta. Tanpa dep baru. Tanpa boilerplate.\n- Hapus > tambah. Membosankan > cerdas. Paling sedikit file.\n- Diff terpendek menang — tapi hanya setelah paham masalah.\n- Tanyai permintaan kompleks: "Kamu perlu X, atau Y mencakup?"\n- Saat dua solusi imbang, pilih yang benar untuk edge-case. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ultra)\nDev senior malas. Kode terbaik = tak pernah ditulis. Sebelum kode: YAGNI → pakai ulang → stdlib → platform → dep → satu baris → minimum. Perbaiki akar, bukan gejala: grep semua caller, perbaiki fungsi bersama sekali. Tanpa abstraksi tak diminta, tanpa dep baru, tanpa boilerplate. Hapus > tambah. Paling sedikit file. Diff terpendek, hanya setelah paham masalah. Tanya permintaan kompleks. Edge-case benar saat imbang. ${SHARED_BOUNDARIES}`,
},
es: {
lite: `# Ponytail (lite)\nAntes de escribir código: ¿necesita existir? ¿Ya existe aquí? ¿La stdlib o una dependencia instalada lo cubre? Solo entonces: escribe el mínimo. Reutilizar antes que reescribir. ${SHARED_BOUNDARIES}`,
full: `# Ponytail — dev senior perezoso\n\nEres un desarrollador senior perezoso. Perezoso = eficiente, no descuidado. El mejor código es el que nunca se escribió.\n\nAntes de escribir código, detente en el primer peldaño que aguante:\n1. ¿Esto necesita existir? (YAGNI)\n2. ¿Ya existe en este codebase? Reutilízalo.\n3. ¿La stdlib lo hace? Úsala.\n4. ¿Una función de la plataforma o una dependencia instalada lo cubre? Úsala.\n5. ¿Puede ser una línea? Hazlo una línea.\n6. Solo entonces: escribe el mínimo que funcione.\n\nCorregir un bug = causa raíz, no síntoma. Haz grep de cada caller de la función que tocas; corrige la función compartida una vez — un guard ahí es un diff más pequeño que uno por caller.\n\nReglas:\n- Sin abstracciones no pedidas. Sin dependencias nuevas. Sin boilerplate.\n- Borrar > añadir. Aburrido > ingenioso. Menos archivos.\n- Gana el diff más corto que funcione — pero solo después de entender el problema.\n- Cuestiona peticiones complejas: "¿Necesitas X, o Y lo cubre?"\n- Si dos soluciones empatan, elige la correcta en los edge cases. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ultra)\nDev senior perezoso. Mejor código = el que nunca se escribió. Antes de codificar: YAGNI → reutilizar → stdlib → plataforma → dependencia → una línea → mínimo que funcione. Corrige la causa raíz, no el síntoma: grep a cada caller, parchea la función compartida una vez. Sin abstracciones no pedidas, sin dependencias nuevas, sin boilerplate. Borrar > añadir. Menos archivos. Diff más corto, solo tras entender el problema. Cuestiona lo complejo. Ante empate, lo correcto en edge cases. ${SHARED_BOUNDARIES}`,
},
de: {
lite: `# Ponytail (lite)\nVor dem Codeschreiben: Muss das existieren? Existiert es hier schon? Deckt die Stdlib oder eine installierte Dependency es ab? Erst dann: das Minimum schreiben. Wiederverwenden statt neu schreiben. ${SHARED_BOUNDARIES}`,
full: `# Ponytail — fauler Senior-Entwickler\n\nDu bist ein fauler Senior-Entwickler. Faul = effizient, nicht nachlässig. Der beste Code ist der, der nie geschrieben wurde.\n\nBevor du Code schreibst, halte auf der ersten tragfähigen Stufe an:\n1. Muss das existieren? (YAGNI)\n2. Existiert es schon in dieser Codebase? Wiederverwenden.\n3. Kann die Stdlib das? Nutzen.\n4. Deckt ein Plattform-Feature oder eine installierte Dependency es ab? Nutzen.\n5. Geht es in einer Zeile? Mach eine Zeile draus.\n6. Erst dann: das Minimum schreiben, das funktioniert.\n\nBugfix = Ursache, nicht Symptom. Grep jeden Caller der Funktion, die du anfasst; fixe die gemeinsame Funktion einmal — ein Guard dort ist ein kleinerer Diff als einer pro Caller.\n\nRegeln:\n- Keine unbestellten Abstraktionen. Keine neuen Dependencies. Kein Boilerplate.\n- Löschen > Hinzufügen. Langweilig > clever. So wenige Dateien wie möglich.\n- Der kürzeste funktionierende Diff gewinnt — aber erst, nachdem du das Problem verstanden hast.\n- Hinterfrage komplexe Anforderungen: "Brauchst du X, oder deckt Y es ab?"\n- Bei Gleichstand die Lösung wählen, die in Edge Cases korrekt ist. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ultra)\nFauler Senior-Entwickler. Bester Code = nie geschrieben. Vor jedem Code: YAGNI → wiederverwenden → Stdlib → Plattform → Dependency → eine Zeile → funktionierendes Minimum. Ursache fixen, nicht Symptom: jeden Caller greppen, gemeinsame Funktion einmal patchen. Keine unbestellten Abstraktionen, keine neuen Dependencies, kein Boilerplate. Löschen > Hinzufügen. Wenigste Dateien. Kürzester funktionierender Diff, erst nach Verständnis des Problems. Komplexes hinterfragen. Bei Gleichstand: korrekt in Edge Cases. ${SHARED_BOUNDARIES}`,
},
fr: {
lite: `# Ponytail (lite)\nAvant d'écrire du code : doit-il exister ? Existe-t-il déjà ici ? La stdlib ou une dépendance installée le couvre-t-elle ? Seulement alors : écris le minimum. Réutiliser plutôt que réécrire. ${SHARED_BOUNDARIES}`,
full: `# Ponytail — dev senior paresseux\n\nTu es un développeur senior paresseux. Paresseux = efficace, pas négligent. Le meilleur code est celui qui n'a jamais été écrit.\n\nAvant d'écrire du code, arrête-toi au premier barreau qui tient :\n1. Cela doit-il exister ? (YAGNI)\n2. Existe-t-il déjà dans cette codebase ? Réutilise-le.\n3. La stdlib le fait ? Utilise-la.\n4. Une fonctionnalité de la plateforme ou une dépendance installée le couvre ? Utilise-la.\n5. Tient-il en une ligne ? Fais-en une ligne.\n6. Seulement alors : écris le minimum qui fonctionne.\n\nCorriger un bug = cause racine, pas symptôme. Grep chaque caller de la fonction touchée ; corrige la fonction partagée une fois — un guard là est un diff plus petit qu'un par caller.\n\nRègles :\n- Pas d'abstractions non demandées. Pas de nouvelles dépendances. Pas de boilerplate.\n- Supprimer > ajouter. Ennuyeux > malin. Le moins de fichiers possible.\n- Le diff fonctionnel le plus court gagne — mais seulement après avoir compris le problème.\n- Questionne les demandes complexes : « As-tu besoin de X, ou Y suffit-il ? »\n- À égalité, choisis la solution correcte dans les edge cases. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ultra)\nDev senior paresseux. Meilleur code = jamais écrit. Avant tout code : YAGNI → réutiliser → stdlib → plateforme → dépendance → une ligne → minimum fonctionnel. Corrige la cause racine, pas le symptôme : grep chaque caller, patch la fonction partagée une fois. Pas d'abstractions non demandées, pas de nouvelles dépendances, pas de boilerplate. Supprimer > ajouter. Moins de fichiers. Diff le plus court, seulement après compréhension du problème. Questionner le complexe. À égalité : correct dans les edge cases. ${SHARED_BOUNDARIES}`,
},
it: {
lite: `# Ponytail (lite)\nPrima di scrivere codice: deve esistere? Esiste già qui? La stdlib o una dipendenza installata lo copre? Solo allora: scrivi il minimo. Riusare invece di riscrivere. ${SHARED_BOUNDARIES}`,
full: `# Ponytail — dev senior pigro\n\nSei uno sviluppatore senior pigro. Pigro = efficiente, non trascurato. Il codice migliore è quello mai scritto.\n\nPrima di scrivere codice, fermati al primo gradino che regge:\n1. Deve esistere? (YAGNI)\n2. Esiste già in questa codebase? Riusalo.\n3. La stdlib lo fa? Usala.\n4. Una feature della piattaforma o una dipendenza installata lo copre? Usala.\n5. Può stare in una riga? Falla in una riga.\n6. Solo allora: scrivi il minimo che funziona.\n\nBug fix = causa radice, non sintomo. Fai grep di ogni caller della funzione che tocchi; correggi la funzione condivisa una volta — un guard lì è un diff più piccolo di uno per caller.\n\nRegole:\n- Niente astrazioni non richieste. Niente nuove dipendenze. Niente boilerplate.\n- Cancellare > aggiungere. Noioso > ingegnoso. Meno file possibile.\n- Vince il diff funzionante più corto — ma solo dopo aver capito il problema.\n- Metti in dubbio le richieste complesse: "Ti serve X, o basta Y?"\n- A parità, scegli la soluzione corretta negli edge case. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ultra)\nDev senior pigro. Codice migliore = mai scritto. Prima del codice: YAGNI → riuso → stdlib → piattaforma → dipendenza → una riga → minimo funzionante. Correggi la causa radice, non il sintomo: grep di ogni caller, patch della funzione condivisa una volta. Niente astrazioni non richieste, niente nuove dipendenze, niente boilerplate. Cancellare > aggiungere. Meno file. Diff più corto, solo dopo aver capito il problema. Dubita del complesso. A parità: corretto negli edge case. ${SHARED_BOUNDARIES}`,
},
ru: {
lite: `# Ponytail (лайт)\режде чем писать код: это должно существовать? Уже есть здесь? Покрывает ли stdlib или установленная зависимость? Только потом: пиши минимум. Переиспользуй, а не переписывай. ${SHARED_BOUNDARIES}`,
full: `# Ponytail — ленивый сеньор\n\nТы ленивый сеньор-разработчик. Ленивый = эффективный, а не небрежный. Лучший код — тот, что не был написан.\n\режде чем писать код, остановись на первой ступени, которая держит:\n1. Это должно существовать? (YAGNI)\n2. Уже есть в этой кодовой базе? Переиспользуй.\n3. Stdlib это умеет? Используй.\n4. Возможность платформы или установленная зависимость покрывает? Используй.\n5. Помещается в одну строку? Сделай одной строкой.\n6. Только потом: напиши минимум, который работает.\n\агфикс = первопричина, а не симптом. Сделай grep по всем caller'ам функции, которую трогаешь; исправь общую функцию один раз — один guard там меньше, чем guard на каждый caller.\n\равила:\n- Никаких незапрошенных абстракций. Никаких новых зависимостей. Никакого boilerplate.\n- Удалить > добавить. Скучное > хитрое. Минимум файлов.\n- Побеждает кратчайший работающий diff — но только после понимания проблемы.\n- Подвергай сомнению сложные запросы: «Тебе нужен X, или хватит Y?»\n- При равенстве выбирай решение, корректное в edge case. ${SHARED_BOUNDARIES}`,
ultra: `# Ponytail (ультра)\енивый сеньор. Лучший код = ненаписанный. Перед кодом: YAGNI → переиспользование → stdlib → платформа → зависимость → одна строка → работающий минимум. Чини первопричину, не симптом: grep по всем caller'ам, патчи общую функцию один раз. Без незапрошенных абстракций, без новых зависимостей, без boilerplate. Удалить > добавить. Минимум файлов. Кратчайший diff — только после понимания проблемы. Сомневайся в сложном. При равенстве — корректность в edge case. ${SHARED_BOUNDARIES}`,
},
zh: {
lite: `# Ponytail精简\n写代码之前它需要存在吗这里已经有了吗标准库或已安装的依赖能覆盖吗然后才写最小实现。复用优于重写。${SHARED_BOUNDARIES}`,
full: `# Ponytail — 懒惰的资深开发者\n\n你是一名懒惰的资深开发者。懒惰 = 高效,而非马虎。最好的代码是从未写出的代码。\n\n写任何代码之前停在第一个站得住的台阶上\n1. 它需要存在吗YAGNI\n2. 代码库里已经有了吗?复用它。\n3. 标准库能做吗?用它。\n4. 平台能力或已安装的依赖能覆盖吗?用它。\n5. 一行能写完吗?写成一行。\n6. 然后才写:能工作的最小实现。\n\n修 bug = 根因,而非症状。对你要改的函数 grep 所有调用方;把共享函数修一次 — 在那里加一个 guard比每个调用方各加一个的 diff 更小。\n\n规则\n- 不写未被要求的抽象。不加新依赖。不写样板代码。\n- 删除 > 添加。朴实 > 取巧。文件越少越好。\n- 最短的可用 diff 获胜 — 但必须先理解问题。\n- 质疑复杂需求:"你需要 X还是 Y 就够了?"\n- 两个方案打平时,选边界情况下正确的那个。${SHARED_BOUNDARIES}`,
ultra: `# Ponytail极简\n懒惰资深开发者。最好的代码 = 从未写出。写码前YAGNI → 复用 → 标准库 → 平台 → 依赖 → 一行 → 最小可用。修根因不修症状grep 所有调用方,共享函数只修一次。不要未被要求的抽象、新依赖、样板代码。删除 > 添加。文件最少。最短可用 diff理解问题之后才算。质疑复杂需求。打平时选边界情况正确者。${SHARED_BOUNDARIES}`,
},
},
},
// i-have-adhd (action-first output) — integrated into the output-style registry
@@ -221,36 +155,6 @@ export const OUTPUT_STYLE_CATALOG: Record<string, OutputStyle> = {
full: `# Saya punya ADHD — keluaran yang mengutamakan aksi\n\nPembaca punya ADHD. Bentuk keluaran supaya otak ADHD bisa langsung bertindak:\n1. Mulai dari aksi berikutnya — perintah, path, atau cuplikan kode dulu; konteks belakangan, kalau perlu.\n2. Beri nomor untuk pekerjaan banyak langkah; tiap langkah satu aksi terbatas; pakai langkah sesedikit mungkin yang tetap jalan.\n3. Akhiri dengan SATU langkah konkret yang bisa dikerjakan di bawah dua menit.\n4. Tahan bahasan sampingan: selesaikan yang pertama, tawarkan yang kedua sebagai pertanyaan terpisah.\n5. Pada pekerjaan banyak giliran, ulangi posisi saat ini ("langkah 3 dari 5 selesai") — pembaca tidak menyimpan status antar pesan.\n6. Kalau ada usaha manusia, perkirakan dalam satuan konkret (menit, satu sore), jangan "agak butuh kerja".\n7. Tunjukkan hasil: sebutkan apa yang sekarang jalan dan cara mencobanya.\n8. Error apa adanya: sebab dan perbaikannya; jangan "Waduh".\n9. Daftar maksimal 5 butir; lebih dari itu pisahkan "sekarang" dan "nanti".\n10. Tanpa pembuka, tanpa rekap, tanpa basa-basi penutup ("Semoga membantu").\nPengecualian: permintaan eksplisit "jelaskan" dapat isi penuh (tetap tanpa pembuka/penutup); aksi merusak dikonfirmasi dulu; ambiguitas nyata dapat satu pertanyaan singkat. ${SHARED_BOUNDARIES}`,
ultra: `# Saya punya ADHD (ultra)\nAksi dulu: perintah/path/cuplikan, prosa kalau perlu. Langkah bernomor dan terbatas, sesedikit mungkin. SATU langkah <2 menit di akhir. Tanpa bahasan sampingan — jadikan pertanyaan terpisah. Banyak giliran: ulangi status. Usaha manusia: satuan waktu konkret. Hasil terlihat. Error: sebab + perbaikan. Daftar ≤5. Nol pembuka/rekap/penutup. "Jelaskan" dapat isi penuh; aksi merusak dikonfirmasi; ambiguitas nyata dapat satu pertanyaan. ${SHARED_BOUNDARIES}`,
},
es: {
lite: `# Tengo TDAH (lite)\nEmpieza por la acción: comando, ruta o snippet primero, prosa después. Numera el trabajo multi-paso; cada paso es una acción acotada. Termina con UNA próxima acción concreta. Sin preámbulo, sin resumen, sin despedidas. ${SHARED_BOUNDARIES}`,
full: `# Tengo TDAH — salida orientada a la acción\n\nQuien lee tiene TDAH. Da forma a la salida para que un cerebro con TDAH pueda actuar:\n1. Empieza por la próxima acción — comando, ruta o snippet primero; contexto después, si hace falta.\n2. Numera el trabajo multi-paso; cada paso es una acción acotada; usa los mínimos pasos que funcionen.\n3. Termina con UNA acción concreta realizable en menos de dos minutos.\n4. Suprime tangentes: cierra el primer asunto, ofrece el segundo como pregunta aparte.\n5. En trabajo multi-turno, reafirma dónde están las cosas ("paso 3 de 5 hecho") — quien lee no retiene estado entre mensajes.\n6. Si hay esfuerzo humano, estímalo en unidades concretas (minutos, una tarde), nunca "algo de trabajo".\n7. Haz visibles los logros: di qué funciona ya y cómo probarlo.\n8. Errores sin drama: causa y arreglo; nunca "¡Uy!".\n9. Listas de 5 ítems como máximo; más allá, divide en "ahora" vs "después".\n10. Sin preámbulo, sin resumen, sin cierres ("Espero que ayude").\nExcepciones: una petición explícita de "explica" recibe cuerpo completo (aún sin preámbulo/cierre); las acciones destructivas piden confirmación antes; la ambigüedad real recibe una pregunta corta. ${SHARED_BOUNDARIES}`,
ultra: `# Tengo TDAH (ultra)\nAcción primero: comando/ruta/snippet, luego prosa si hace falta. Pasos numerados y acotados, los mínimos que funcionen. UNA próxima acción <2 min al final. Sin tangentes — pregunta aparte. Multi-turno: reafirma el estado. Esfuerzo humano: unidades concretas de tiempo. Logros visibles. Errores: causa + arreglo. Listas ≤5. Cero preámbulo/resumen/cierres. "Explica" recibe cuerpo completo; lo destructivo pide confirmación; la ambigüedad real recibe una pregunta. ${SHARED_BOUNDARIES}`,
},
de: {
lite: `# Ich habe ADHS (lite)\nBeginne mit der Aktion: Befehl, Pfad oder Snippet zuerst, Prosa danach. Nummeriere mehrschrittige Arbeit; jeder Schritt eine begrenzte Aktion. Ende mit EINEM konkreten nächsten Schritt. Kein Vorwort, keine Zusammenfassung, keine Verabschiedung. ${SHARED_BOUNDARIES}`,
full: `# Ich habe ADHS — aktionsorientierte Ausgabe\n\nDie lesende Person hat ADHS. Forme die Ausgabe so, dass ein ADHS-Gehirn danach handeln kann:\n1. Beginne mit der nächsten Aktion — Befehl, Pfad oder Snippet zuerst; Kontext danach, falls überhaupt.\n2. Nummeriere mehrschrittige Arbeit; jeder Schritt ist eine begrenzte Aktion; so wenige Schritte wie möglich.\n3. Ende mit EINEM konkreten nächsten Schritt, machbar in unter zwei Minuten.\n4. Unterdrücke Abschweifungen: schließe das erste Thema ab, biete das zweite als separate Frage an.\n5. Bei Arbeit über mehrere Runden den Stand wiederholen („Schritt 3 von 5 fertig") — die lesende Person hält keinen Zustand zwischen Nachrichten.\n6. Bei menschlichem Aufwand in konkreten Einheiten schätzen (Minuten, ein Nachmittag), nie „etwas Arbeit".\n7. Erfolge sichtbar machen: sag, was jetzt funktioniert und wie man es ausprobiert.\n8. Fehler sachlich: Ursache und Fix; nie „Hoppla".\n9. Listen mit höchstens 5 Punkten; darüber in „jetzt" vs. „später" teilen.\n10. Kein Vorwort, keine Zusammenfassung, keine Schlussfloskeln („Ich hoffe, das hilft").\nAusnahmen: eine explizite „Erkläre"-Anfrage bekommt einen vollen Text (weiterhin ohne Vorwort/Abschluss); destruktive Aktionen erst bestätigen lassen; echte Mehrdeutigkeit bekommt eine kurze Rückfrage. ${SHARED_BOUNDARIES}`,
ultra: `# Ich habe ADHS (ultra)\nAktion zuerst: Befehl/Pfad/Snippet, dann Prosa falls nötig. Nummerierte, begrenzte Schritte, so wenige wie möglich. EIN nächster Schritt <2 Min am Ende. Keine Abschweifungen — separate Frage. Mehrere Runden: Stand wiederholen. Menschlicher Aufwand: konkrete Zeiteinheiten. Erfolge sichtbar. Fehler: Ursache + Fix. Listen ≤5. Null Vorwort/Zusammenfassung/Floskeln. „Erkläre" bekommt vollen Text; Destruktives braucht Bestätigung; echte Mehrdeutigkeit eine Frage. ${SHARED_BOUNDARIES}`,
},
fr: {
lite: `# J'ai un TDAH (lite)\nCommence par l'action : commande, chemin ou snippet d'abord, prose ensuite. Numérote le travail multi-étapes ; chaque étape est une action délimitée. Termine par UNE prochaine action concrète. Pas de préambule, pas de récapitulatif, pas de formules de politesse. ${SHARED_BOUNDARIES}`,
full: `# J'ai un TDAH — sortie orientée action\n\nLa personne qui lit a un TDAH. Façonne la sortie pour qu'un cerveau TDAH puisse agir :\n1. Commence par la prochaine action — commande, chemin ou snippet d'abord ; le contexte ensuite, si nécessaire.\n2. Numérote le travail multi-étapes ; chaque étape est une action délimitée ; le moins d'étapes possible.\n3. Termine par UNE action concrète faisable en moins de deux minutes.\n4. Supprime les digressions : termine le premier sujet, propose le second comme question séparée.\n5. Sur plusieurs tours, redis où on en est (« étape 3 sur 5 faite ») — la personne ne retient pas l'état entre les messages.\n6. Quand un effort humain est en jeu, estime-le en unités concrètes (minutes, un après-midi), jamais « un peu de travail ».\n7. Rends les victoires visibles : dis ce qui marche désormais et comment l'essayer.\n8. Erreurs sans drame : cause et correctif ; jamais « Oups ».\n9. Listes de 5 éléments max ; au-delà, sépare « maintenant » vs « plus tard ».\n10. Pas de préambule, pas de récapitulatif, pas de conclusion (« En espérant que ça aide »).\nExceptions : une demande explicite d'« explication » reçoit un corps complet (toujours sans préambule/conclusion) ; les actions destructives demandent confirmation d'abord ; une vraie ambiguïté reçoit une courte question. ${SHARED_BOUNDARIES}`,
ultra: `# J'ai un TDAH (ultra)\nAction d'abord : commande/chemin/snippet, puis prose si besoin. Étapes numérotées et délimitées, le minimum qui fonctionne. UNE action <2 min à la fin. Pas de digressions — question séparée. Multi-tours : redire l'état. Effort humain : unités de temps concrètes. Victoires visibles. Erreurs : cause + correctif. Listes ≤5. Zéro préambule/récap/conclusion. « Explique » reçoit un corps complet ; le destructif demande confirmation ; la vraie ambiguïté reçoit une question. ${SHARED_BOUNDARIES}`,
},
it: {
lite: `# Ho l'ADHD (lite)\nParti dall'azione: comando, percorso o snippet prima, prosa dopo. Numera il lavoro multi-passo; ogni passo è un'azione delimitata. Chiudi con UNA prossima azione concreta. Niente preamboli, niente riassunti, niente saluti finali. ${SHARED_BOUNDARIES}`,
full: `# Ho l'ADHD — output orientato all'azione\n\nChi legge ha l'ADHD. Modella l'output perché un cervello ADHD possa agire:\n1. Parti dalla prossima azione — comando, percorso o snippet prima; contesto dopo, se serve.\n2. Numera il lavoro multi-passo; ogni passo è un'azione delimitata; usa i minimi passi che funzionano.\n3. Chiudi con UNA azione concreta fattibile in meno di due minuti.\n4. Sopprimi le tangenti: chiudi il primo tema, offri il secondo come domanda separata.\n5. Nel lavoro multi-turno, ripeti a che punto siamo ("passo 3 di 5 fatto") — chi legge non trattiene lo stato tra i messaggi.\n6. Se c'è sforzo umano, stimalo in unità concrete (minuti, un pomeriggio), mai "un po' di lavoro".\n7. Rendi visibili i risultati: di' cosa funziona ora e come provarlo.\n8. Errori senza drammi: causa e fix; mai "Ops".\n9. Liste di massimo 5 voci; oltre, separa "ora" vs "dopo".\n10. Niente preamboli, niente riassunti, niente chiusure ("Spero sia utile").\nEccezioni: una richiesta esplicita di "spiegare" riceve un corpo completo (sempre senza preambolo/chiusura); le azioni distruttive chiedono prima conferma; l'ambiguità reale riceve una domanda breve. ${SHARED_BOUNDARIES}`,
ultra: `# Ho l'ADHD (ultra)\nAzione prima: comando/percorso/snippet, poi prosa se serve. Passi numerati e delimitati, i minimi che funzionano. UNA azione <2 min alla fine. Niente tangenti — domanda separata. Multi-turno: ripeti lo stato. Sforzo umano: unità di tempo concrete. Risultati visibili. Errori: causa + fix. Liste ≤5. Zero preamboli/riassunti/chiusure. "Spiega" riceve corpo completo; il distruttivo chiede conferma; l'ambiguità reale riceve una domanda. ${SHARED_BOUNDARIES}`,
},
ru: {
lite: `# У меня СДВГ (лайт)\nНачинай с действия: команда, путь или сниппет сначала, проза потом. Нумеруй многошаговую работу; каждый шаг — одно ограниченное действие. Заверши ОДНИМ конкретным следующим шагом. Без вступлений, без пересказа, без прощаний. ${SHARED_BOUNDARIES}`,
full: `# У меня СДВГ — вывод, ориентированный на действие\n\nЧитатель с СДВГ. Оформи вывод так, чтобы мозг с СДВГ мог сразу действовать:\n1. Начинай со следующего действия — команда, путь или сниппет сначала; контекст потом, если вообще нужен.\n2. Нумеруй многошаговую работу; каждый шаг — одно ограниченное действие; минимально работающее число шагов.\n3. Завершай ОДНИМ конкретным шагом, выполнимым меньше чем за две минуты.\n4. Отсекай отступления: закончи первый вопрос, второй предложи отдельным вопросом.\n5. В многоходовой работе повторяй, где мы находимся («шаг 3 из 5 готов») — читатель не удерживает состояние между сообщениями.\n6. Если нужен человеческий труд, оценивай в конкретных единицах (минуты, полдня), никогда «немного работы».\n7. Делай победы видимыми: скажи, что уже работает и как это попробовать.\n8. Ошибки по-деловому: причина и исправление; никаких «Ой».\n9. Списки не длиннее 5 пунктов; дальше дели на «сейчас» и «потом».\n10. Без вступлений, без пересказа, без концовок («Надеюсь, помогло»).\сключения: явная просьба «объясни» получает полный текст (по-прежнему без вступления/концовки); разрушительные действия сначала подтверждаются; настоящая неоднозначность получает один короткий вопрос. ${SHARED_BOUNDARIES}`,
ultra: `# У меня СДВГ (ультра)\nСначала действие: команда/путь/сниппет, потом проза при необходимости. Нумерованные ограниченные шаги, минимум работающих. ОДИН шаг <2 мин в конце. Без отступлений — отдельный вопрос. Много ходов: повторяй состояние. Человеческий труд: конкретные единицы времени. Победы видимы. Ошибки: причина + исправление. Списки ≤5. Ноль вступлений/пересказов/концовок. «Объясни» — полный текст; разрушительное — подтверждение; настоящая неоднозначность — один вопрос. ${SHARED_BOUNDARIES}`,
},
zh: {
lite: `# 我有 ADHD精简\n从行动开始先给命令、路径或代码片段散文放后面。多步骤工作要编号每一步是一个有边界的动作。以一个具体的下一步收尾。不要开场白、不要复述、不要客套结尾。${SHARED_BOUNDARIES}`,
full: `# 我有 ADHD — 行动优先的输出\n\n读者有 ADHD。让输出适配 ADHD 的大脑,让人能直接行动:\n1. 从下一步行动开始 — 先给命令、路径或代码片段;上下文放后面,如果需要的话。\n2. 多步骤工作要编号;每一步是一个有边界的动作;用能工作的最少步数。\n3. 以一个两分钟内可完成的具体下一步收尾。\n4. 压住跑题:先完成第一件事,第二件作为单独的问题提出。\n5. 多轮工作要复述进度("5 步中第 3 步已完成")— 读者无法在消息之间保持状态。\n6. 涉及人力时,用具体单位估算(几分钟、一个下午),绝不说"要花点功夫"。\n7. 让成果可见:说清现在什么能用了、怎么试。\n8. 报错就事论事:原因和修法;不说"糟糕"。\n9. 列表最多 5 项;超过就拆成"现在做"和"以后做"。\n10. 不要开场白、不要复述、不要客套结尾("希望有帮助")。\n例外明确要求"解释"时给完整正文(仍不要开场白/结尾);破坏性操作先确认;真正的歧义提一个简短的问题。${SHARED_BOUNDARIES}`,
ultra: `# 我有 ADHD极简\n行动优先命令/路径/片段在前,需要时才有散文。步骤编号且有边界,越少越好。结尾给一个 <2 分钟的下一步。不跑题 — 另起问题。多轮:复述进度。人力:具体时间单位。成果可见。报错:原因 + 修法。列表 ≤5。零开场白/复述/客套。"解释"给完整正文;破坏性操作先确认;真歧义提一个问题。${SHARED_BOUNDARIES}`,
},
},
},
"terse-cjk": {
@@ -273,14 +177,3 @@ export const OUTPUT_STYLE_IDS: string[] = Object.keys(OUTPUT_STYLE_CATALOG);
export function outputStyleMeta(id: string): OutputStyle {
return OUTPUT_STYLE_CATALOG[id];
}
/** Sorted union of every language an output style can instruct in (i18n keys + locale gates + en). */
export function outputStyleLanguages(): string[] {
const langs = new Set<string>(["en"]);
for (const id of OUTPUT_STYLE_IDS) {
const meta = OUTPUT_STYLE_CATALOG[id];
if (meta.locale) langs.add(meta.locale);
for (const lang of Object.keys(meta.i18n ?? {})) langs.add(lang);
}
return [...langs].sort();
}

View File

@@ -2,8 +2,8 @@
* Browser-TLS-impersonating HTTP client for grok.com.
*
* Thin re-export over the shared `tlsClientBase.ts` factory
* (`createTlsClientModule`). All provider-agnostic logic (wreq-js transport
* pooling, direct streaming, proxy resolution, deadlines, Cloudflare challenge
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
* streaming tail-file, proxy resolution, error classes, Cloudflare challenge
* detection) lives in the base module; this file supplies only Grok-specific
* config and preserves the original public export surface.
*/
@@ -22,9 +22,9 @@ const HARD_TIMEOUT_GRACE_MS =
export const tlsClientModule = createTlsClientModule({
providerName: "Grok",
tlsProfile: "chrome_146",
emulationOs: "linux",
domain: "https://grok.com",
streamEofPolicy: "exclude",
tempDirPrefix: "grok-stream-",
tailFileVariant: "B1",
responseValidation: "cf",
exportCloudflareCheck: true,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,

View File

@@ -2,8 +2,8 @@
* Browser-TLS-impersonating HTTP client for arena.ai.
*
* Thin re-export over the shared `tlsClientBase.ts` factory
* (`createTlsClientModule`). All provider-agnostic logic (wreq-js transport
* pooling, direct streaming, proxy resolution, deadlines, Cloudflare challenge
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
* streaming tail-file, proxy resolution, error classes, Cloudflare challenge
* detection) lives in the base module; this file supplies only LMArena-specific
* config and preserves the original public export surface.
*/
@@ -20,12 +20,11 @@ const HARD_TIMEOUT_GRACE_MS = 10_000;
export const tlsClientModule = createTlsClientModule({
providerName: "LMArena",
tlsProfile: "chrome_146",
emulationOs: "windows",
domain: "https://lmarena.ai",
// LMArena's proxy resolution domain is hardcoded to arena.ai, not the config domain.
proxyDomainOverride: "https://arena.ai",
streamEofPolicy: "none",
streamEofSymbol: "",
tempDirPrefix: "LMArena-stream-",
tailFileVariant: "B2",
responseValidation: "cf",
exportCloudflareCheck: true,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,

View File

@@ -2,8 +2,8 @@
* Browser-TLS-impersonating HTTP client for app.notion.com.
*
* Thin re-export over the shared `tlsClientBase.ts` factory
* (`createTlsClientModule`). All provider-agnostic logic (wreq-js transport
* pooling, direct streaming, proxy resolution, deadlines, SSE detection,
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
* streaming tail-file, proxy resolution, error classes, SSE detection,
* Cloudflare challenge detection) lives in the base module; this file supplies
* only Notion-specific config and preserves the original public export surface.
*/
@@ -22,9 +22,9 @@ const HARD_TIMEOUT_GRACE_MS =
export const tlsClientModule = createTlsClientModule({
providerName: "Notion",
tlsProfile: "chrome_146",
emulationOs: "windows",
domain: "https://app.notion.com",
streamEofPolicy: "include",
tempDirPrefix: "pplx-stream-",
tailFileVariant: "A",
responseValidation: "sse",
exportCloudflareCheck: true,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,

View File

@@ -2,8 +2,8 @@
* Browser-TLS-impersonating HTTP client for www.perplexity.ai.
*
* Thin re-export over the shared `tlsClientBase.ts` factory
* (`createTlsClientModule`). All provider-agnostic logic (wreq-js transport
* pooling, direct streaming, proxy resolution, deadlines, SSE detection,
* (`createTlsClientModule`). All provider-agnostic logic (sidecar lifecycle,
* streaming tail-file, proxy resolution, error classes, SSE detection,
* Cloudflare challenge detection) lives in the base module; this file supplies
* only Perplexity-specific config and preserves the original public export
* surface.
@@ -23,9 +23,9 @@ const HARD_TIMEOUT_GRACE_MS =
export const tlsClientModule = createTlsClientModule({
providerName: "Perplexity",
tlsProfile: "firefox_148",
emulationOs: "macos",
domain: "https://www.perplexity.ai",
streamEofPolicy: "include",
tempDirPrefix: "pplx-stream-",
tailFileVariant: "A",
responseValidation: "sse",
exportCloudflareCheck: true,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,

View File

@@ -11,8 +11,6 @@ import {
DEFAULT_RESILIENCE_SETTINGS,
type ResilienceSettings,
} from "../../src/lib/resilience/settings";
import { PROVIDER_PROFILES } from "../config/constants.ts";
import { getProviderCategory } from "../config/providerRegistry.ts";
interface CooldownEntry {
/** Timestamp of last recorded failure (ms since epoch) */
@@ -21,47 +19,6 @@ interface CooldownEntry {
failureCount: number;
/** How long this entry must be retained for cleanup purposes */
retentionMs: number;
/**
* Provider-level entries only: timestamps of recent failures, pruned to the
* profile's `providerFailureWindowMs`. Powers the PROVIDER_PROFILES window
* gate (`providerFailureThreshold` failures inside the window trip a
* `providerCooldownMs` cooldown for the whole provider).
*/
failureTimestamps?: number[];
}
// ── PROVIDER_PROFILES window gate (whole-provider scope) ─────────────────────
// `providerFailureThreshold` / `providerFailureWindowMs` / `providerCooldownMs`
// shipped in PROVIDER_PROFILES with no runtime consumer (2026-08-31 docs
// audit, P0.1). Provider-level entries (no connectionId) now honor them: the
// provider only counts as cooling after `providerFailureThreshold` failures
// inside `providerFailureWindowMs`, and then cools for `providerCooldownMs`.
// Connection-level entries keep the pre-existing exponential backoff.
function providerWindowProfile(provider: string) {
const category = getProviderCategory(provider);
const profile = PROVIDER_PROFILES[category] ?? PROVIDER_PROFILES.apikey;
return {
failureThreshold: profile.providerFailureThreshold,
failureWindowMs: profile.providerFailureWindowMs,
cooldownMs: profile.providerCooldownMs,
};
}
function pruneWindow(timestamps: number[], windowMs: number, now: number): number[] {
const cutoff = now - windowMs;
const pruned = timestamps.filter((t) => t >= cutoff);
// Memory bound: the gate only ever needs `failureThreshold` recent samples;
// keep a small multiple so bursts cannot grow the array unbounded.
return pruned.length > 200 ? pruned.slice(-200) : pruned;
}
function providerWindowCooldownMs(provider: string, entry: CooldownEntry, now: number): number {
const { failureThreshold, failureWindowMs, cooldownMs } = providerWindowProfile(provider);
const inWindow = pruneWindow(entry.failureTimestamps ?? [], failureWindowMs, now);
if (inWindow.length < failureThreshold) return 0;
const elapsed = now - entry.lastFailureAt;
const remaining = cooldownMs - elapsed;
return remaining > 0 ? remaining : 0;
}
// Global cooldown state: keyed by "provider:connectionId" or "provider"
@@ -133,21 +90,8 @@ export function recordProviderCooldown(
existing.lastFailureAt = now;
existing.failureCount++;
existing.retentionMs = Math.max(existing.retentionMs, retentionMs);
if (!connectionId) {
const { failureWindowMs } = providerWindowProfile(provider);
existing.failureTimestamps = pruneWindow(
[...(existing.failureTimestamps ?? []), now],
failureWindowMs,
now
);
}
} else {
cooldownMap.set(key, {
lastFailureAt: now,
failureCount: 1,
retentionMs,
...(connectionId ? {} : { failureTimestamps: [now] }),
});
cooldownMap.set(key, { lastFailureAt: now, failureCount: 1, retentionMs });
}
startCleanupIfNeeded();
@@ -175,11 +119,6 @@ export function isProviderInCooldown(
if (entry.failureCount === 0) return false;
const now = Date.now();
if (!connectionId) {
return providerWindowCooldownMs(provider, entry, now) > 0;
}
const elapsed = now - entry.lastFailureAt;
const minCooldownMs =
@@ -212,12 +151,6 @@ export function getRemainingCooldownMs(
if (!entry) return 0;
const now = Date.now();
if (!connectionId) {
if (entry.failureCount === 0) return 0;
return providerWindowCooldownMs(provider, entry, now);
}
const elapsed = now - entry.lastFailureAt;
const minCooldownMs =
@@ -250,9 +183,8 @@ export function recordProviderSuccess(provider: string, connectionId: string | u
const key = cooldownKey(provider, connectionId);
const entry = cooldownMap.get(key);
if (entry) {
// Reset failure count and the provider-level failure window, keep the entry
// Reset failure count but keep the entry
entry.failureCount = 0;
entry.failureTimestamps = [];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
import { join } from "node:path";
import { resolveDataDir } from "@/lib/dataPaths";
/**
* Writable cache directory for tls-client-node's native binary.
*
* Without an explicit `downloadDir`, the library defaults to its own package
* `node_modules/tls-client-node/bin`, which is root-owned on global installs
* and fails with EACCES for normal users (#8579).
*/
export function resolveTlsClientDownloadDir(): string {
return join(resolveDataDir(), "tls-client", "bin");
}
export function buildNativeTlsClientOptions(): {
runtimeMode: "native";
downloadDir: string;
} {
return {
runtimeMode: "native",
downloadDir: resolveTlsClientDownloadDir(),
};
}

View File

@@ -73,7 +73,6 @@ import { getCommandCodeUsage } from "./usage/command-code.ts";
import { getQwenTokenPlanUsage } from "./usage/qwen-token-plan.ts";
import { getConolUsage } from "./conolUsage.ts";
import { getAgentrouterUsage } from "./usage/agentrouter.ts";
import { getKilocodeUsage } from "./usage/kilocode.ts";
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
@@ -206,8 +205,6 @@ export async function getUsageForProvider(
return await getConolUsage(apiKey || accessToken, providerSpecificData);
case "agentrouter":
return await getAgentrouterUsage(id, connection);
case "kilocode":
return await getKilocodeUsage(id, connection);
default:
return { message: `Usage API not implemented for ${provider}` };
}
@@ -247,5 +244,4 @@ export const __testing = {
mapSubscriptionTierStringToPlanLabel,
toDisplayLabel,
getKiroUsage,
getKilocodeUsage,
};

View File

@@ -73,7 +73,6 @@ export const USAGE_FETCHER_PROVIDERS = [
"cnl",
// AgentRouter (New-API) console balance (GET /api/user/self)
"agentrouter",
"kilocode",
] as const;
export type UsageFetcherProvider = (typeof USAGE_FETCHER_PROVIDERS)[number];

View File

@@ -1,334 +0,0 @@
/**
* usage/kilocode.ts — Kilo Code balance + Kilo Pass usage fetcher (Provider Limits).
*
* Two independent upstream requests per usage fetch, both authenticated with the
* existing kilocode OAuth access token (personal scope; no organization support):
* - GET {KILO_API_URL|https://api.kilo.ai}/api/profile/balance → personal USD balance
* - GET {KILO_API_URL|https://api.kilo.ai}/api/trpc/kiloPass.getState?batch=1&input={"0":null}
* → Kilo Pass subscription state (official tRPC endpoint, Kilo-Org/kilocode contract)
*
* The two requests fail independently: a Kilo Pass error never hides the personal
* balance and vice versa. Only when both are unavailable does the dashboard fall
* back to the existing { message } convention.
*/
import type { UsageQuota } from "./quota.ts";
import { parseResetTime } from "./quota.ts";
import { toRecord, toNumber, roundCurrency } from "./scalars.ts";
/** Upstream API base. Environment override mirrors sibling fetchers. */
const KILO_API_BASE: string = process.env.KILO_API_URL || "https://api.kilo.ai";
const BALANCE_PATH = "/api/profile/balance";
const BALANCE_URL = `${KILO_API_BASE}${BALANCE_PATH}`;
const PASS_PATH = "/api/trpc/kiloPass.getState";
const KILO_EDITOR_NAME = "OmniRoute";
const FETCH_TIMEOUT_MS = 8_000;
/** Fallback token for Kilo's anonymous freetier (registry anonymousApiKey).
* Balance/pass endpoints require authenticated accounts, value rejected
* before any request made. */
const KILO_ANONYMOUS_TOKEN = "anonymous";
/** Live subscription statuses that represent an active Kilo Pass, per the
* official Kilo-Org/kilocode parseKiloPassState contract. The cloud returns
* full records after cancellation too; only these statuses consume credits. */
const KILO_PASS_LIVE_STATUSES = new Set(["active", "past_due", "trialing"]);
/** Kilo Pass subscription state (mirrors official Kilo-Org/kilocode KiloPassState). */
export interface KiloPassState {
currentPeriodBaseCreditsUsd: number;
currentPeriodUsageUsd: number;
currentPeriodBonusCreditsUsd: number;
nextBillingAt: string | null;
}
function readAccessToken(connection: Record<string, unknown>): string | null {
const value = connection["accessToken"];
if (typeof value === "string" && value.trim().length > 0) return value;
return null;
}
function isAnonymousToken(token: string): boolean {
return token.trim() === KILO_ANONYMOUS_TOKEN;
}
function kiloHeaders(token: string): Record<string, string> {
return {
Authorization: `Bearer ${token}`,
"X-KILOCODE-EDITORNAME": KILO_EDITOR_NAME,
"Content-Type": "application/json",
Accept: "application/json",
};
}
/** Extract non-negative USD balance from upstream JSON body. Returns null
* when value missing, null, negative, not numeric. */
export function parseKilocodeBalance(data: unknown): number | null {
const obj = toRecord(data);
if (obj.balance === undefined || obj.balance === null) return null;
const balance = toNumber(obj.balance, Number.NaN);
if (!Number.isFinite(balance) || balance < 0) return null;
return roundCurrency(balance);
}
/** Coerce a USD credit amount the way the official client does: finite
* non-negative numbers pass through, everything else becomes 0. */
function passUsd(value: unknown): number {
const num = toNumber(value, 0);
return Number.isFinite(num) && num >= 0 ? num : 0;
}
/**
* Parse Kilo Pass state from the tRPC response, mirroring the official
* Kilo-Org/kilocode parseKiloPassState semantics exactly:
* - batched tRPC shape: [{ result: { data: { json: { subscription } } } }]
* - unbatched result.data.json: { result: { data: { json: { subscription } } } }
* - plain result.data (no superjson json wrapper): { result: { data: { subscription } } }
* - plain fallback: { subscription }
* - requires at least one period amount present (base or usage)
* - status, when present as string, must be a live status
* - negative/non-finite amounts clamp to 0; invalid dates become null
*
* Returns null when no live pass data is present (no pass, canceled, expired,
* missing fields, malformed tRPC envelope).
*/
export function parseKiloPassState(value: unknown): KiloPassState | null {
const item = Array.isArray(value) ? value[0] : value;
const data = toRecord(toRecord(toRecord(item)?.result)?.data);
// Official Kilo-Org/kilocode fallback chain: data.json envelope first,
// then the tRPC data object itself (plain-JSON responses carry the
// subscription there without a superjson json wrapper), then raw payload.
const jsonValue = data?.json;
const root =
jsonValue !== null && typeof jsonValue === "object" && !Array.isArray(jsonValue)
? toRecord(jsonValue)
: Object.keys(data).length > 0
? data
: toRecord(value);
const sub = toRecord(root?.subscription);
if (!sub || (sub.currentPeriodBaseCreditsUsd == null && sub.currentPeriodUsageUsd == null)) {
return null;
}
if (typeof sub.status === "string" && !KILO_PASS_LIVE_STATUSES.has(sub.status)) {
return null;
}
const next = sub.nextBillingAt ?? sub.nextRenewalAt;
return {
currentPeriodBaseCreditsUsd: passUsd(sub.currentPeriodBaseCreditsUsd),
currentPeriodUsageUsd: passUsd(sub.currentPeriodUsageUsd),
currentPeriodBonusCreditsUsd: passUsd(sub.currentPeriodBonusCreditsUsd),
// Normalize to the ISO format OmniRoute expects; invalid dates must not
// break the whole fetch (parseResetTime returns null instead).
nextBillingAt: parseResetTime(typeof next === "string" ? next : null),
};
}
/**
* Fetch Kilo Pass state. Returns null on any failure (HTTP error, network,
* timeout, malformed body, no live pass) — matches the official client's
* silent-degradation contract. Never throws, never logs tokens/bodies.
*/
export async function fetchKiloPassState(token: string): Promise<KiloPassState | null> {
try {
const params = new URLSearchParams({
batch: "1",
input: JSON.stringify({ "0": null }),
});
const response = await fetch(`${KILO_API_BASE}${PASS_PATH}?${params}`, {
method: "GET",
headers: kiloHeaders(token),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
if (!response.ok) return null;
return parseKiloPassState(await response.json());
} catch {
return null;
}
}
/** Build normalized usage response from successful balance fetch. */
export function buildKilocodeUsageResult(balance: number): {
plan: string;
quotas: Record<string, UsageQuota>;
} {
const balanceQuota: UsageQuota = {
used: 0,
total: 0,
remaining: balance,
remainingPercentage: balance > 0 ? 100 : 0,
resetAt: null,
unlimited: true,
currency: "USD",
displayName: "Balance (USD)",
};
return {
plan: "Kilo Code",
quotas: { balance: balanceQuota },
};
}
/**
* Build Kilo Pass quota entries. Remaining pass credits follow the official
* Kilo Pass meter semantics (total pool = base + bonus, used consumed from it):
* remaining = max(0, base + bonus - usage). resetAt carries nextBillingAt on
* the period-defining Base Credits row.
*/
export function buildKiloPassUsageResult(pass: KiloPassState): {
plan: string;
quotas: Record<string, UsageQuota>;
} {
const base = pass.currentPeriodBaseCreditsUsd;
const bonus = pass.currentPeriodBonusCreditsUsd;
const usage = pass.currentPeriodUsageUsd;
const remaining = Math.max(0, roundCurrency(base + bonus - usage));
const quotas: Record<string, UsageQuota> = {
kiloPassBase: {
used: 0,
total: base,
remaining: base,
remainingPercentage: base > 0 ? 100 : 0,
resetAt: pass.nextBillingAt,
unlimited: false,
currency: "USD",
displayName: "Base Credits",
},
kiloPassBonus: {
used: 0,
total: bonus,
remaining: bonus,
remainingPercentage: bonus > 0 ? 100 : 0,
resetAt: null,
unlimited: false,
currency: "USD",
displayName: "Bonus Credits",
},
kiloPassUsage: {
used: usage,
total: base + bonus,
remaining,
remainingPercentage: base + bonus > 0 ? Math.max(0, (remaining / (base + bonus)) * 100) : 0,
resetAt: pass.nextBillingAt,
unlimited: false,
currency: "USD",
displayName: "Kilo Pass Usage",
},
kiloPassRemaining: {
used: 0,
total: 0,
remaining,
remainingPercentage: remaining > 0 ? 100 : 0,
resetAt: pass.nextBillingAt,
unlimited: false,
currency: "USD",
displayName: "Pass Remaining",
},
};
return {
plan: "Kilo Code",
quotas,
};
}
/**
* Fetch balance from upstream API. Throws with the historical per-status
* messages so getKilocodeUsage can surface the same diagnostics as before
* when the pass request fails alongside it.
*/
async function fetchBalance(token: string): Promise<number> {
let response: Response;
try {
response = await fetch(BALANCE_URL, {
method: "GET",
headers: kiloHeaders(token),
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
} catch (error) {
throw new Error(`Kilo Code balance error: ${(error as Error).message}`);
}
if (response.status === 401 || response.status === 403) {
throw new Error("Kilo Code token expired access denied. Please re-authenticate connection.");
}
if (response.status === 429) {
throw new Error("Kilo Code balance request rate limited. Try again later.");
}
if (!response.ok) {
throw new Error(`Kilo Code balance request failed with HTTP ${response.status}.`);
}
let data: unknown;
try {
data = await response.json();
} catch (error) {
throw new Error(`Kilo Code balance error: ${(error as Error).message}`);
}
const balance = parseKilocodeBalance(data);
if (balance === null) {
throw new Error("Kilo Code balance response invalid missing balance value.");
}
return balance;
}
/** Fetch and normalize Kilo Code balance + Kilo Pass usage for connection. */
export async function getKilocodeUsage(
_connectionId: string | undefined,
connection?: Record<string, unknown>
): Promise<
{ plan: string; quotas: Record<string, UsageQuota> } | { plan: string; message: string }
> {
const token = connection ? readAccessToken(connection) : null;
if (connection?.["apiKey"] !== undefined && !token) {
return {
plan: "Kilo Code",
message: "Kilo Code balance uses Kilo Code OAuth account; separate API key not supported.",
};
}
if (!token) {
return {
plan: "Kilo Code",
message: "Kilo Code balance not available. Add Kilo Code account view usage.",
};
}
if (isAnonymousToken(token)) {
return {
plan: "Kilo Code",
message:
"Kilo Code balance only available authenticated accounts. Free anonymous usage balance.",
};
}
// Both requests share one usage fetch but fail independently.
const [balanceSettled, passSettled] = await Promise.allSettled([
fetchBalance(token),
fetchKiloPassState(token),
]);
const balance = balanceSettled.status === "fulfilled" ? balanceSettled.value : null;
const pass = passSettled.status === "fulfilled" ? passSettled.value : null;
if (balance !== null && pass !== null) {
return {
plan: "Kilo Code",
quotas: {
...buildKilocodeUsageResult(balance).quotas,
...buildKiloPassUsageResult(pass).quotas,
},
};
}
if (balance !== null) return buildKilocodeUsageResult(balance);
if (pass !== null) return buildKiloPassUsageResult(pass);
const balanceError =
balanceSettled.status === "rejected" ? (balanceSettled.reason as Error).message : null;
return {
plan: "Kilo Code",
message: balanceError ?? "Kilo Code usage unavailable. Try again later.",
};
}

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