mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
Merge branch 'release/v3.8.51' into fix/claude-oauth-sticky-refresh
This commit is contained in:
14
.env.example
14
.env.example
@@ -2748,6 +2748,13 @@ APP_LOG_TO_FILE=true
|
||||
# tokens (accessToken / refreshToken / providerSpecificData). Default OFF —
|
||||
# only non-credential metadata is synced. See docs/security/SOCKET_DEV_FINDINGS.md §5.
|
||||
# OMNIROUTE_CLOUD_SYNC_SECRETS=false
|
||||
#
|
||||
# Set to "true" to reject an UNSIGNED Cloud sync response when no local secret
|
||||
# is configured (#13679). Default OFF keeps v3.8.x back-compat for peers that
|
||||
# have not rotated in a shared secret yet; v3.9 flips the default to enforced.
|
||||
# A signature that IS present is always verified, and always rejected when
|
||||
# OMNIROUTE_CLOUD_SYNC_SECRET is unset, regardless of this flag.
|
||||
# OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=false
|
||||
|
||||
# ─── Zed import legacy compat (v3.8.6) ──────────────────────────────────────
|
||||
# Set to "true" to fall back to the v3.8.5 one-step "import everything from
|
||||
@@ -3043,6 +3050,13 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
|
||||
# CHROME_PATH=/usr/bin/chromium
|
||||
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
# CDP_PROXY_TOKEN required by docker/chatgpt-web-codex-browser/cdp-proxy.mjs (#13679):
|
||||
# when set, every request to the CDP proxy sidecar must present it as an
|
||||
# `X-Omni-Cdp-Token` header. Left unset, the proxy keeps forwarding requests
|
||||
# unauthenticated (network isolation via docker-compose.yml's dedicated
|
||||
# `chatgpt-web-codex-net` is the default mitigation). Generate with:
|
||||
# `openssl rand -hex 32`
|
||||
# CDP_PROXY_TOKEN=
|
||||
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
|
||||
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex v2
|
||||
|
||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -511,9 +511,11 @@ jobs:
|
||||
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
||||
# Real-translation ratchet: a leaf copied verbatim from en.json passes key
|
||||
# parity above but is still English to the user (es shipped 55% English).
|
||||
# Advisory in PR-0; flipped to blocking once the backlog is retranslated (PR-4).
|
||||
- name: i18n real-translation ratio (advisory)
|
||||
run: node scripts/i18n/check-translation-ratio.mjs --warn
|
||||
# Blocking since PR-4 retranslated the verbatim-English backlog: the share of
|
||||
# untranslated leaves per locale may only fall (ratchet baseline in
|
||||
# config/quality/i18n-translation-baseline.json; `npm run i18n:check-ratio:update`).
|
||||
- name: i18n real-translation ratio
|
||||
run: node scripts/i18n/check-translation-ratio.mjs
|
||||
# #8463: a rewritten English value used to leave its 39 translations behind
|
||||
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
|
||||
# Key parity above cannot see it — a stale translation counts as covered.
|
||||
@@ -531,6 +533,15 @@ jobs:
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
||||
run: node scripts/i18n/check-new-key-coverage.mjs
|
||||
# Absolute complement of the two gates above: every locale must carry exactly the key
|
||||
# set of en.json, whatever the age of the key. A locale batch is generated from the
|
||||
# en.json of the day the branch is cut and translates for days while the base keeps
|
||||
# adding keys — the batch PR adds no key itself, so the new-key gate stays silent and
|
||||
# 43 absent keys out of ~13,000 still read 99.7 % coverage. Incident 2026-09-15:
|
||||
# batch 1 (#13044) landed 43 keys short in nine locales, batch 2 (#13660) 10 keys short
|
||||
# in eight. Fix is `sync-ui-keys --locale=<codes> --translate-markers`.
|
||||
- name: i18n key completeness (every locale carries every en.json key)
|
||||
run: node scripts/i18n/check-key-completeness.mjs
|
||||
|
||||
# #8038: cheap glossary/protected-terms consistency gate —
|
||||
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
|
||||
|
||||
42
.github/workflows/release-acceptance.yml
vendored
Normal file
42
.github/workflows/release-acceptance.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Release acceptance
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-acceptance-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
acceptance:
|
||||
name: Release acceptance
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Emit shadow acceptance report
|
||||
run: |
|
||||
node scripts/quality/validate-release-acceptance.mjs \
|
||||
--plan tests/fixtures/release-acceptance/plan-lint.json \
|
||||
--manifests tests/fixtures/release-acceptance/shadow-manifests \
|
||||
--out release-acceptance-report.json
|
||||
continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: release-acceptance-report
|
||||
path: release-acceptance-report.json
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
@@ -97,4 +97,11 @@
|
||||
# credential; the generic-api-key rule flags the long hyphenated string.
|
||||
'''omniroute-cheaperinference-sponsor-banner-dismissed-v\d+''',
|
||||
'''SunbreakWebUI1''',
|
||||
# Uzbek dashboard catalog (#13727, src/i18n/messages/uz.json `outputTokenDesc`):
|
||||
# "Yakunlash/javob tokenlari" = "completion/response tokens". The rule reads the
|
||||
# `...TokenDesc` key as a token assignment and the translated words as its value.
|
||||
'''Yakunlash/javob''',
|
||||
# Feature-flag id from #13439 (src/shared/constants/featureFlagDefinitions.ts):
|
||||
# `key: "PROTECTED_PRIORITY_INFRA_502_ENABLED"` is a flag name, not a credential.
|
||||
'''PROTECTED_PRIORITY_INFRA_502_ENABLED''',
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts tests/issue-13000-cold-start-combo-limit.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -4631,9 +4631,21 @@ export function buildStaticProviderEntry(
|
||||
.map((m) => m.max_output_tokens)
|
||||
.filter((v): v is number => typeof v === "number" && v > 0);
|
||||
|
||||
if (contextValues.length > 0 && outputValues.length > 0) {
|
||||
// Prefer the server-computed aggregate (accounts for explicit
|
||||
// context_length overrides and members outside memberEntries, e.g.
|
||||
// not yet resolved in /v1/models) over the raw Math.min(member)
|
||||
// lower bound. Mirrors mapComboToModelV2's limit.context logic
|
||||
// (#13000) so the static catalog and the dynamic hook agree.
|
||||
const preferredContext =
|
||||
typeof combo.computed_context_length === "number" && combo.computed_context_length > 0
|
||||
? combo.computed_context_length
|
||||
: contextValues.length > 0
|
||||
? Math.min(...contextValues)
|
||||
: undefined;
|
||||
|
||||
if (preferredContext !== undefined && outputValues.length > 0) {
|
||||
entry.limit = {
|
||||
context: Math.min(...contextValues),
|
||||
context: preferredContext,
|
||||
output: Math.min(...outputValues),
|
||||
};
|
||||
}
|
||||
@@ -5511,6 +5523,32 @@ export function createOmniRouteConfigHook(
|
||||
|
||||
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
|
||||
|
||||
// Snapshot backfill for computed_context_length: a live /api/combos
|
||||
// response can come back without this field (server hasn't finished
|
||||
// recomputing it yet, e.g. just after a restart) even though the
|
||||
// combo's members and identity are otherwise unchanged. When that
|
||||
// happens, prefer the last-known-good value from the warm disk
|
||||
// snapshot over the Math.min(member) fallback in
|
||||
// mapComboToModelV2() — never overwrite any other combo field
|
||||
// (models/name/etc.) with stale data, only this one derived number.
|
||||
if (warmSnapshot) {
|
||||
const snapshotComboById = new Map(warmSnapshot.rawCombos.map((c) => [c.id, c]));
|
||||
for (const combo of localRawCombos) {
|
||||
const hasLive =
|
||||
typeof combo.computed_context_length === "number" &&
|
||||
combo.computed_context_length > 0;
|
||||
if (hasLive) continue;
|
||||
const stale = snapshotComboById.get(combo.id);
|
||||
if (
|
||||
stale &&
|
||||
typeof stale.computed_context_length === "number" &&
|
||||
stale.computed_context_length > 0
|
||||
) {
|
||||
combo.computed_context_length = stale.computed_context_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk-cache fallback (cold first run, no warm snapshot): when the
|
||||
// live fetch returned no models AND features.diskCache !== false,
|
||||
// hydrate from the last-known-good snapshot so OC still surfaces a
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Repro for #13000: combo context limits fall back to Math.min(member)
|
||||
* instead of using computed_context_length after cold start — no disk
|
||||
* snapshot fallback.
|
||||
*
|
||||
* Scenario (mirrors the report): a warm disk snapshot holds the combo with
|
||||
* its correct server-computed `computed_context_length` (245000, from all 6
|
||||
* members). After a restart, the live refresh's combos fetch returns the
|
||||
* SAME combo but without `computed_context_length` (e.g. the value hasn't
|
||||
* propagated yet), and the live models fetch only resolves 2 of the 6
|
||||
* members (the rest not yet in /v1/models). The background refresh then
|
||||
* republishes the provider block built from this degraded live data,
|
||||
* downgrading a previously-known-good 245000 limit to Math.min(163840,
|
||||
* 1_000_000) = 163840 — exactly the member-minimum described in the issue.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import type { Config } from "@opencode-ai/plugin";
|
||||
|
||||
import {
|
||||
createOmniRouteConfigHook,
|
||||
_resetInflightRefresh,
|
||||
type OmniRouteAutoCombosFetcher,
|
||||
type OmniRouteCombosFetcher,
|
||||
type OmniRouteCompressionMetaFetcher,
|
||||
type OmniRouteEnrichmentFetcher,
|
||||
type OmniRouteFetchCache,
|
||||
type OmniRouteModelsFetcher,
|
||||
type OmniRouteProvidersFetcher,
|
||||
type OmniRouteRawCombo,
|
||||
type OmniRouteRawModelEntry,
|
||||
type OmniRouteReadAuthJson,
|
||||
type OmniRouteStaticProviderEntry,
|
||||
type OmniRouteDiskSnapshotReader,
|
||||
type OmniRouteDiskSnapshotWriter,
|
||||
} from "../src/index.js";
|
||||
|
||||
test.beforeEach(() => {
|
||||
_resetInflightRefresh();
|
||||
});
|
||||
|
||||
function stubReadAuthJson(value: Record<string, unknown>): OmniRouteReadAuthJson {
|
||||
return async () => value as never;
|
||||
}
|
||||
|
||||
function authStub() {
|
||||
return stubReadAuthJson({
|
||||
"opencode-omniroute": {
|
||||
type: "api",
|
||||
key: "sk-test",
|
||||
baseURL: "https://or.example.com/v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function makeInput(): Config {
|
||||
return { provider: {} } as unknown as Config;
|
||||
}
|
||||
|
||||
// The two members resolvable in the degraded live /v1/models response.
|
||||
const MEMBER_DEEPSEEK: OmniRouteRawModelEntry = {
|
||||
id: "deepseek-v4-pro",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 163_840,
|
||||
max_output_tokens: 64_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const MEMBER_GLM: OmniRouteRawModelEntry = {
|
||||
id: "glm-5.2",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 1_000_000,
|
||||
max_output_tokens: 16_384,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
// The other member that IS present once the server is fully warm.
|
||||
const MEMBER_GLM_53_HIGH: OmniRouteRawModelEntry = {
|
||||
id: "GLM-5.3-high",
|
||||
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
|
||||
context_length: 245_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
};
|
||||
|
||||
const COMBO_MODELS: OmniRouteRawCombo["models"] = [
|
||||
{ kind: "model", model: "deepseek-v4-pro", weight: 25 },
|
||||
{ kind: "model", model: "glm-5.2", weight: 25 },
|
||||
{ kind: "model", model: "GLM-5.3-high", weight: 50 },
|
||||
];
|
||||
|
||||
test("issue #13000: warm combo limit (245000) survives a degraded post-restart refresh instead of downgrading to Math.min(member)", async () => {
|
||||
const warmSnapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
|
||||
rawModels: [MEMBER_DEEPSEEK, MEMBER_GLM, MEMBER_GLM_53_HIGH],
|
||||
rawCombos: [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
computed_context_length: 245_000,
|
||||
},
|
||||
],
|
||||
rawAutoCombos: [],
|
||||
rawEnrichment: new Map(),
|
||||
rawCompressionCombos: [],
|
||||
rawConnections: [],
|
||||
};
|
||||
|
||||
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
|
||||
const combosFetcher: OmniRouteCombosFetcher = async () => [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
// computed_context_length intentionally omitted.
|
||||
},
|
||||
];
|
||||
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
|
||||
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
|
||||
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
|
||||
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
|
||||
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => warmSnapshot;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
// Let the detached background refresh (degraded live data) complete and
|
||||
// republish the block.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
const comboModelAfter = entryAfter.models["orchestrator"];
|
||||
assert.ok(comboModelAfter, "combo model still published after refresh");
|
||||
|
||||
assert.equal(
|
||||
comboModelAfter.limit.context,
|
||||
245_000,
|
||||
`expected the combo limit to stay at the known-good 245000, but got ${comboModelAfter.limit.context} ` +
|
||||
`(Math.min(member) fallback — the exact bug described in #13000)`
|
||||
);
|
||||
});
|
||||
|
||||
test("issue #13000 (control): no warm snapshot exists — Math.min(member) fallback is still used (expected, documented behavior)", async () => {
|
||||
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
|
||||
const combosFetcher: OmniRouteCombosFetcher = async () => [
|
||||
{
|
||||
id: "orchestrator",
|
||||
name: "orchestrator",
|
||||
models: COMBO_MODELS,
|
||||
// computed_context_length intentionally omitted.
|
||||
},
|
||||
];
|
||||
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
|
||||
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
|
||||
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
|
||||
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
|
||||
|
||||
// No prior snapshot on disk.
|
||||
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
|
||||
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
|
||||
|
||||
const sharedCache: OmniRouteFetchCache = new Map();
|
||||
|
||||
const hook = createOmniRouteConfigHook(
|
||||
{ providerId: "omniroute", modelCacheTtl: 60_000 },
|
||||
{
|
||||
readAuthJson: authStub(),
|
||||
fetcher,
|
||||
combosFetcher,
|
||||
autoCombosFetcher,
|
||||
enrichmentFetcher,
|
||||
compressionMetaFetcher,
|
||||
providersFetcher,
|
||||
diskSnapshotReader,
|
||||
diskSnapshotWriter,
|
||||
cache: sharedCache,
|
||||
}
|
||||
);
|
||||
|
||||
const input = makeInput();
|
||||
await hook(input);
|
||||
|
||||
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
|
||||
"opencode-omniroute"
|
||||
];
|
||||
const comboModelAfter = entryAfter.models["orchestrator"];
|
||||
assert.ok(comboModelAfter, "combo model published on cold first run");
|
||||
|
||||
// No snapshot to backfill from — Math.min(163840, 1_000_000) = 163840.
|
||||
assert.equal(
|
||||
comboModelAfter.limit.context,
|
||||
163_840,
|
||||
"pure cold start with no snapshot must keep using the Math.min(member) fallback"
|
||||
);
|
||||
});
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 358 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 359 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (176 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (177 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
|
||||
16
README.md
16
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 358 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 358 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 359 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **443 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **446 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.07B in the first month with signup credits, from 34 documented recurring pool keys covering 443 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.07B in the first month with signup credits, from 34 documented recurring pool keys covering 446 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
|
||||
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
|
||||
>
|
||||
@@ -233,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 358 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 358 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 52 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 358 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -672,7 +672,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
</div>
|
||||
|
||||
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **443 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **443 per-model rows**, **34 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1268,7 +1268,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 176 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 177 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
@@ -1331,7 +1331,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>16-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 34 documented recurring pools / 443 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 34 documented recurring pools / 446 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -9,6 +9,8 @@ function truncate(v, len = 60) {
|
||||
return s.length > len ? s.slice(0, len - 1) + "…" : s;
|
||||
}
|
||||
|
||||
const VALID_MCP_TRANSPORTS = ["stdio", "sse", "streamable-http"];
|
||||
|
||||
const mcpToolSchema = [
|
||||
{ key: "name", header: "Tool", width: 36 },
|
||||
{
|
||||
@@ -43,6 +45,25 @@ export function registerMcp(program) {
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("enable")
|
||||
.description(t("mcp.enable.description"))
|
||||
.option("--transport <transport>", t("mcp.enable.transport"))
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runMcpEnableCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
mcp
|
||||
.command("disable")
|
||||
.description(t("mcp.disable.description"))
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runMcpDisableCommand({ ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
|
||||
// 5.1 — mcp call + mcp scopes
|
||||
mcp
|
||||
.command("call <tool> [argsJson]")
|
||||
@@ -61,10 +82,15 @@ export function registerMcp(program) {
|
||||
? JSON.parse(argsPositional)
|
||||
: {};
|
||||
|
||||
const exitCode = await runMcpCallCommand(tool, args, {
|
||||
...opts,
|
||||
stream: opts.stream,
|
||||
}, globalOpts);
|
||||
const exitCode = await runMcpCallCommand(
|
||||
tool,
|
||||
args,
|
||||
{
|
||||
...opts,
|
||||
stream: opts.stream,
|
||||
},
|
||||
globalOpts
|
||||
);
|
||||
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
@@ -127,7 +153,9 @@ async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } =
|
||||
|
||||
if (!initRes.ok) {
|
||||
const text = await initRes.text().catch(() => "");
|
||||
process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n`);
|
||||
process.stderr.write(
|
||||
`MCP initialize failed: HTTP ${initRes.status}${text ? ` — ${text}` : ""}\n`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -227,6 +255,7 @@ export async function runMcpStatusCommand(opts = {}) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.log(t("mcp.stopped"));
|
||||
console.log(t("mcp.stoppedHint"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -240,6 +269,9 @@ export async function runMcpStatusCommand(opts = {}) {
|
||||
const transport = status.transport || "stdio";
|
||||
const online = status.online ?? status.running;
|
||||
console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped"));
|
||||
if (!online && status.enabled === false) {
|
||||
console.log(t("mcp.stoppedHint"));
|
||||
}
|
||||
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
|
||||
if (status.scopes?.length) {
|
||||
console.log(" Scopes:");
|
||||
@@ -270,10 +302,76 @@ export async function runMcpRestartCommand(opts = {}) {
|
||||
console.log(t("mcp.restarted"));
|
||||
return 0;
|
||||
}
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
const body = await res.json().catch(() => null);
|
||||
const message = body?.error || `HTTP ${res.status}`;
|
||||
console.error(t("common.error", { message }));
|
||||
return 1;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMcpEnableCommand(opts = {}) {
|
||||
const serverUp = await isServerUp();
|
||||
if (!serverUp) {
|
||||
console.error(t("common.serverOffline"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.transport && !VALID_MCP_TRANSPORTS.includes(opts.transport)) {
|
||||
console.error(
|
||||
t("common.error", {
|
||||
message: `Invalid transport '${opts.transport}'. Valid: ${VALID_MCP_TRANSPORTS.join(", ")}`,
|
||||
})
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = { mcpEnabled: true };
|
||||
if (opts.transport) body.mcpTransport = opts.transport;
|
||||
|
||||
const res = await apiFetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
body,
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(t("mcp.enabled"));
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMcpDisableCommand(opts = {}) {
|
||||
const serverUp = await isServerUp();
|
||||
if (!serverUp) {
|
||||
console.error(t("common.serverOffline"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiFetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
body: { mcpEnabled: false },
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
}
|
||||
console.log(t("mcp.disabled"));
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"max_restarts": "Max crash restarts within 30s before giving up (default: 2)",
|
||||
"tray": "Start in the system tray (desktop only, opt-in)",
|
||||
"no_tray": "Disable system tray icon",
|
||||
"ready_timeout": "Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)",
|
||||
"tls_cert": "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -347,6 +348,16 @@
|
||||
"running": "MCP server running ({transport})",
|
||||
"stopped": "MCP server stopped.",
|
||||
"restarted": "MCP server restarted.",
|
||||
"stoppedHint": "Run `omniroute mcp enable` to turn it on.",
|
||||
"enabled": "MCP server enabled.",
|
||||
"disabled": "MCP server disabled.",
|
||||
"enable": {
|
||||
"description": "Enable the MCP server",
|
||||
"transport": "Transport to use: stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "Disable the MCP server"
|
||||
},
|
||||
"call": {
|
||||
"description": "Invoke an MCP tool directly",
|
||||
"args": "JSON arguments object (inline)",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"max_restarts": "30 秒内的最大崩溃重启次数(默认:2)",
|
||||
"tray": "显示系统托盘图标(仅桌面,选择加入)",
|
||||
"no_tray": "禁用系统托盘图标",
|
||||
"ready_timeout": "就绪探测超时(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS,默认 60000)",
|
||||
"tls_cert": "用于提供 HTTPS 服务的 TLS 证书(PEM)路径(也可用 OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "用于提供 HTTPS 服务的 TLS 私钥(PEM)路径(也可用 OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -345,6 +346,16 @@
|
||||
"running": "MCP 服务器正在运行({transport})",
|
||||
"stopped": "MCP 服务器已停止。",
|
||||
"restarted": "MCP 服务器已重启。",
|
||||
"stoppedHint": "运行 `omniroute mcp enable` 以启用它。",
|
||||
"enabled": "MCP 服务器已启用。",
|
||||
"disabled": "MCP 服务器已禁用。",
|
||||
"enable": {
|
||||
"description": "启用 MCP 服务器",
|
||||
"transport": "要使用的传输方式:stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "禁用 MCP 服务器"
|
||||
},
|
||||
"call": {
|
||||
"description": "直接调用 MCP 工具",
|
||||
"args": "JSON 参数对象(内联)",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"max_restarts": "30 秒內的最大崩潰重啟次數(預設:2)",
|
||||
"tray": "顯示系統托盤圖示(僅桌面,選擇加入)",
|
||||
"no_tray": "停用系統托盤圖示",
|
||||
"ready_timeout": "就緒探測逾時(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS,預設 60000)",
|
||||
"tls_cert": "用於提供 HTTPS 服務的 TLS 憑證(PEM)路徑(也可用 OMNIROUTE_TLS_CERT)",
|
||||
"tls_key": "用於提供 HTTPS 服務的 TLS 私鑰(PEM)路徑(也可用 OMNIROUTE_TLS_KEY)"
|
||||
},
|
||||
@@ -345,6 +346,16 @@
|
||||
"running": "MCP 伺服器正在執行({transport})",
|
||||
"stopped": "MCP 伺服器已停止。",
|
||||
"restarted": "MCP 伺服器已重啟。",
|
||||
"stoppedHint": "執行 `omniroute mcp enable` 以啟用它。",
|
||||
"enabled": "MCP 伺服器已啟用。",
|
||||
"disabled": "MCP 伺服器已停用。",
|
||||
"enable": {
|
||||
"description": "啟用 MCP 伺服器",
|
||||
"transport": "要使用的傳輸方式:stdio|sse|streamable-http"
|
||||
},
|
||||
"disable": {
|
||||
"description": "停用 MCP 伺服器"
|
||||
},
|
||||
"call": {
|
||||
"description": "直接呼叫 MCP 工具",
|
||||
"args": "JSON 引數物件(內聯)",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { stopProcessGracefully } from "../../../src/shared/platform/windowsProce
|
||||
import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
isFatalStartupDiagnostic,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
@@ -55,12 +56,14 @@ export class ServerSupervisor {
|
||||
this.child = null;
|
||||
this.isShuttingDown = false;
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
this.fatalStartupDiagnosticPrinted = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now();
|
||||
this.crashLog = [];
|
||||
this.instrumentationFailureHintPrinted = false;
|
||||
this.fatalStartupDiagnosticPrinted = false;
|
||||
|
||||
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
|
||||
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
|
||||
@@ -99,6 +102,15 @@ export class ServerSupervisor {
|
||||
)
|
||||
);
|
||||
}
|
||||
// #13314: surface any `[STARTUP] Fatal:`-guarded boot diagnostic
|
||||
// immediately, even without --log — otherwise it is only buffered and
|
||||
// reaches the operator on exit/crash, which never happens when the
|
||||
// HTTP listener still comes up after the fatal failure (every route
|
||||
// then 500s with zero visible diagnostic anywhere).
|
||||
if (!this.fatalStartupDiagnosticPrinted && isFatalStartupDiagnostic(text)) {
|
||||
this.fatalStartupDiagnosticPrinted = true;
|
||||
process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.child.stdout) {
|
||||
|
||||
BIN
bin/cli/tray/icon.ico
Normal file
BIN
bin/cli/tray/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 286 B |
@@ -105,6 +105,27 @@ export function isFatalInstrumentationHookFailure(text) {
|
||||
return /Unsupported platform:\s*android/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect any fatal boot-time diagnostic guarded by the `[STARTUP] Fatal:`
|
||||
* prefix (`src/instrumentation-node.ts::ensureDbReadyForBoot()`,
|
||||
* `src/instrumentation.ts::register()`, and any future guard using the same
|
||||
* marker). #13314: in the default `omniroute serve` mode (no `--log`),
|
||||
* `ServerSupervisor` only buffers stdout/stderr and flushes it to the real
|
||||
* console on exit/crash/readiness-timeout — so if the HTTP listener still
|
||||
* comes up after a fatal boot diagnostic was already printed (e.g. the
|
||||
* better-sqlite3 / node:sqlite driver cascade failing hard), the operator
|
||||
* sees "OmniRoute is running!" with zero visible diagnostic anywhere, and
|
||||
* every route 500s. This generalizes the #10028 Android/Termux carve-out to
|
||||
* every `[STARTUP] Fatal:` guard, not just that one platform-specific string.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFatalStartupDiagnostic(text) {
|
||||
if (!text) return false;
|
||||
return /^\[STARTUP\] Fatal:/m.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Operator-facing hint when that instrumentation failure shows up in child
|
||||
* output — defense in depth if prep was skipped or a future Next.js probe
|
||||
|
||||
1
changelog.d/features/13827-i18n-key-completeness-gate.md
Normal file
1
changelog.d/features/13827-i18n-key-completeness-gate.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(i18n):** new blocking gate `i18n:check-keys` (`scripts/i18n/check-key-completeness.mjs`) — every locale catalog must carry exactly the key set of `en.json`, whatever the age of the key; the percentage and new-key gates let batch 1 (#13044) ship 43 keys short and batch 2 (#13660) 10 keys short. The i18n guide now documents the post-merge re-sync and the retranslation flow. (#13827)
|
||||
1
changelog.d/features/agnes-cn-provider.md
Normal file
1
changelog.d/features/agnes-cn-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): **Added Agnes AI (China) as `agnes-cn` pointed at `https://api.agnes-ai.cn/v1`. Keys issued for `apihub.agnes-ai.com` stay on the existing `agnes` card. Live `/v1/models` on that host lists `agnes-3.0-flash` (same id as intl); the CN seed matches 2.0/2.5/3.0 and not retired 1.5.**
|
||||
1
changelog.d/features/xai-oauth-live-models.md
Normal file
1
changelog.d/features/xai-oauth-live-models.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** share the existing `api.x.ai/v1/models` discovery config with `xai-oauth` so SuperGrok OAuth connections pick up new Grok ids without a registry seed edit.
|
||||
1
changelog.d/fixes/12370-responses-function-call-name.md
Normal file
1
changelog.d/fixes/12370-responses-function-call-name.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(api): restore the `name` field on non-streaming `/v1/responses` `function_call` output items — a plain (non-namespace) tool call's identity restore was blindly applying the `_toolNameMap` alias-table fallback as a `{namespace, name}` object, silently blanking `name` to `undefined` (dropped entirely by JSON.stringify) and leaving Codex unable to dispatch the call, so it re-narrated its intent in a loop instead (#12370)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(db):** give `conversation_turn_nodes` its own independent retention knob (`retention.conversationTurnNodes`, default 30 days — matching `callLogs` so upgrading changes nothing until an operator overrides it) instead of sharing `callLogs`, and sweep orphaned `agentic_conversations` after the nodes expire (#12453).
|
||||
1
changelog.d/fixes/12491-codex-wreq-standalone-runtime.md
Normal file
1
changelog.d/fixes/12491-codex-wreq-standalone-runtime.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** Codex WebSocket transport (including the app-server) no longer fails to load in the Next.js standalone Docker runtime — the wreq-js loader now resolves its module name dynamically instead of a literal Turbopack could rewrite to an unreachable build-time symlink (#12491) — thanks @marshalfevzi
|
||||
1
changelog.d/fixes/12692-xai-legacy-function-call.md
Normal file
1
changelog.d/fixes/12692-xai-legacy-function-call.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** xAI requests no longer silently drop an assistant tool call sent in the legacy OpenAI `function_call` shape (instead of `tool_calls[]`) — the call is now translated into the xAI request the same way modern tool calls are (#12692) — thanks @soroush5
|
||||
1
changelog.d/fixes/12700-xai-usage-total.md
Normal file
1
changelog.d/fixes/12700-xai-usage-total.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** xAI responses no longer report `total_tokens`/`totalTokenCount` as `0` when upstream usage uses the legacy `prompt_tokens`/`completion_tokens` names instead of `input_tokens`/`output_tokens` (#12700) — thanks @soroush5
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(resilience):** a recoverable direct-fetch response-start timeout (`DIRECT_RESPONSE_START_TIMEOUT`) could, in a narrow timer/promise-settlement race, escape as an `unhandledRejection` → `uncaughtException` and kill the server process — even though `proxyFetch` already retries this exact condition on a fresh socket. Guarded the timer callback so it can no longer fire against an already-settled attempt, and extended the process-level crash guard (already used by the WS/API-bridge servers) to recognize and swallow this code if it ever escapes anyway. Also installs that same guard in the production server entrypoint (`dist/server-ws.mjs`), which never had it even though the dev server already did ([#12861](https://github.com/diegosouzapw/OmniRoute/issues/12861)) — thanks @insoln
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** correct Magnific API key validation, which reported every valid key as invalid due to a GET probe against a POST-only endpoint (#12927) — thanks @hubo1989
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** GitLab Duo Retest and chat requests now fall back to the public Code Suggestions endpoint for ANY `direct_access` 403 (not only the "direct connections are disabled" tenant-config message), and surface the real upstream error body instead of a generic "Access denied" when both endpoints reject the token (#12958) — thanks @Rahulsharma0810
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stop misclassifying a truncated Anthropic-compatible `max_tokens` probe response (`content:[{type:"text",text:""}]`) as an empty upstream response (#12968) — thanks @pranay-gpt
|
||||
@@ -0,0 +1 @@
|
||||
- fix(providers): stop `@omniroute/opencode-plugin` combo context limits from downgrading to the raw `Math.min(member)` lower bound after a restart — the static catalog now honors the server-computed `computed_context_length` (mirroring the dynamic hook), and a background refresh with a degraded `/api/combos` response backfills the field from the last-known-good disk snapshot instead of overwriting it (#13000) — thanks @morpheus9393
|
||||
1
changelog.d/fixes/13012-cli-mcp-restart-and-enable.md
Normal file
1
changelog.d/fixes/13012-cli-mcp-restart-and-enable.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(cli):** `omniroute mcp restart` no longer 404s — the missing `POST /api/mcp/restart` route now exists — and new `omniroute mcp enable`/`mcp disable [--transport]` subcommands give the CLI a way to turn the MCP server on without the dashboard ([#13012](https://github.com/diegosouzapw/OmniRoute/issues/13012)) — thanks @ricardusx
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(skills):** repair nested malformed skill-tool schemas (bare property maps, boolean `required: true`) for OpenAI-compatible providers, not just the schema root (#13022) — thanks @ftevxk
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(routing):** round-robin combos now show up in Combo Studio's Live dashboard — they were completing successfully but never publishing the attempt/success/failure events the dashboard listens for (#13089) — thanks @adityadwi21
|
||||
1
changelog.d/fixes/13122-responses-custom-tool-choice.md
Normal file
1
changelog.d/fixes/13122-responses-custom-tool-choice.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stop rejecting a Responses API `tool_choice.type: "custom"` (e.g. Codex CLI forcing `functions__exec`) with a 400 `unsupported_feature` error (#13122) — thanks @phamtienduceng-eng
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** classify a missing Playwright Chromium install on the Z.ai web transport as an actionable 503 host/config cooldown instead of a generic 502 that trips the provider circuit breaker (#13232) — thanks @oleksandr1811
|
||||
1
changelog.d/fixes/13234-embed-lan-keyed-auth.md
Normal file
1
changelog.d/fixes/13234-embed-lan-keyed-auth.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(embeddings):** LAN/CGNAT OpenAI-compatible embeddings nodes with a stored API key now send `Authorization: Bearer` on the outbound request, matching dashboard Check. Keyless LAN nodes stay no-auth ([#6925](https://github.com/diegosouzapw/OmniRoute/issues/6925)) ([#13234](https://github.com/diegosouzapw/OmniRoute/issues/13234))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(db):** defer `process.exit(0)` on graceful shutdown by one macrotask, avoiding a Windows-only libuv abort when the sql.js fallback driver has a statement in flight (#13306) — thanks @anhtahaylove
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(cli):** `omniroute serve` now surfaces a fatal `[STARTUP] Fatal: ...` boot diagnostic (e.g. a DB driver init failure) to the console immediately, even without `--log`, instead of only when the process later crashes or restarts (#13314) — thanks @Orion1943
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(memory):** stop FTS5 rewrite on access-count updates; rebuild the index on cleanup so leftover tombstones shrink (#13326).
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** stop zed-hosted `claude-haiku-4-5` extended-thinking requests from inflating `max_tokens` past the model's real 64000 output cap (#13364) — thanks @ThiagoMafra-Integrare
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** gemini-web no longer drops the system instruction on single-turn requests or the tool contract when a client system message is present, and switches to an atomic composer insert so embedded newlines can't submit the message early (#13380) — thanks @formilw
|
||||
1
changelog.d/fixes/13389-catalog-cache-backoff-reset.md
Normal file
1
changelog.d/fixes/13389-catalog-cache-backoff-reset.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** stop routine connection-backoff auto-recovery from busting the entire `/v1/models` response cache, which was causing intermittent 75-120s/502 responses on deployments routing many providers (#13389) — thanks @RaviTharuma
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(compression):** stop lite compression from dropping a `role:"tool"` message when it is byte-identical to the previous message, which orphaned a `tool_call_id` and triggered upstream 400 errors on parallel tool calls (#13429) — thanks @tolgaaksoy
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** frame post-keepalive `/v1/responses` stream errors with a top-level `type` field so Responses clients (Codex) surface the real upstream error instead of reporting "stream disconnected before completion" (#13431) — thanks @andrea-kingautomation
|
||||
1
changelog.d/fixes/13432-incremental-auto-vacuum-drift.md
Normal file
1
changelog.d/fixes/13432-incremental-auto-vacuum-drift.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(db):** reconcile `auto_vacuum` drift between the configured INCREMENTAL mode and the live SQLite pragma — detected at startup and reconciled out-of-request by the vacuum scheduler, which now also runs a bounded `PRAGMA incremental_vacuum` reclaim instead of an unconditional full `VACUUM` once INCREMENTAL is actually in effect (#13432) — thanks @tolgaaksoy
|
||||
1
changelog.d/fixes/13445-groq-tls-fingerprint.md
Normal file
1
changelog.d/fixes/13445-groq-tls-fingerprint.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(network):** skip Chrome TLS impersonation for Groq (`api.groq.com`); Cloudflare 1010s that JA3 while native undici reaches the API ([#13445](https://github.com/diegosouzapw/OmniRoute/pull/13445)) (#13225)
|
||||
1
changelog.d/fixes/13452-provider-node-baseurl-ignored.md
Normal file
1
changelog.d/fixes/13452-provider-node-baseurl-ignored.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stop an unhydrated `openai-compatible-*`/`anthropic-compatible-*` connection from silently routing chat requests (and its stored credential) to the real OpenAI/Anthropic API instead of the operator's configured provider-node endpoint (#13452) — thanks @DenXio101
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(resilience):** background OAuth token refresh (proactive health-check sweep and the shared refresh helper behind `refreshAccessToken`/`refreshClaudeOAuthToken`/etc.) now fails closed like the interactive chat path when a connection's assigned proxy pool is entirely dead, instead of silently sending the refresh-token exchange out direct or via a stray `HTTPS_PROXY` (#13470) — thanks @elielsousa-pathbit
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** forward Anthropic prompt-cache-creation tokens through the `/v1/responses` usage hop so cache-write counts stop logging as zero (#13472) — thanks @fidelix
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stop the streaming PII sanitizer from splicing OpenRouter metadata (`provider`, `native_finish_reason`, `reasoning_details[].format`) into the answer text buffer (#13488) — thanks @Xore
|
||||
1
changelog.d/fixes/13535-windows-tray-icon-contrast.md
Normal file
1
changelog.d/fixes/13535-windows-tray-icon-contrast.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(cli):** redraw the CLI/Electron system tray icon with a dark outline and ship a native multi-res `icon.ico` so it is no longer a pure-white, nearly invisible glyph on the Windows light-theme taskbar and hidden-icons flyout (#13535) — thanks @ProphetOfDoom-PoD
|
||||
1
changelog.d/fixes/13544-audio-transcription-call-log.md
Normal file
1
changelog.d/fixes/13544-audio-transcription-call-log.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(api):** `/v1/audio/transcriptions`, `/v1/audio/translations` and `/v1/audio/speech` requests now show up in Dashboard → Request Logs — the three routes never called the shared call-log pipeline, so every successful (and failed) transcription/translation/speech request was silently dropped from `call_logs` ([#13544](https://github.com/diegosouzapw/OmniRoute/issues/13544)) — thanks @delafu
|
||||
1
changelog.d/fixes/13558-minimax-m3-reasoning-leak.md
Normal file
1
changelog.d/fixes/13558-minimax-m3-reasoning-leak.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** MiniMax-M3's inline `<think>...</think>` reasoning no longer leaks into `message.content`/`delta.content` on the `minimax`/`minimax-cn` routes — it is now stripped and surfaced as `reasoning_content`, in both streaming and non-streaming responses (#13558) — thanks @pan17
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(api):** `PATCH /api/settings` now persists `hideAutoCombos` and `hideNoThinkVariants` instead of silently dropping them (#13562) — thanks @texastoland
|
||||
@@ -0,0 +1,3 @@
|
||||
- **fix(providers):** Antigravity error responses and logs now surface the real upstream
|
||||
message (e.g. Gemini field-path rejections) instead of the generic "Antigravity upstream
|
||||
error (400)" placeholder (#13591) — thanks @afonsoft
|
||||
1
changelog.d/fixes/13597-calllogs-worker-error-detail.md
Normal file
1
changelog.d/fixes/13597-calllogs-worker-error-detail.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(usage):** the call-logs artifact worker's failure warning now includes the underlying error's message/code instead of the generic "detail omitted" — a crashed or non-zero-exit worker was previously undiagnosable in the logs (#13597) — thanks @afonsoft
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(providers):** echo back `reasoning_content` on `bai` DeepSeek thinking-mode follow-up turns, fixing the upstream 400 "reasoning_content must be passed back" (#13599) — thanks @afonsoft
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(i18n):** drop the second copy of `featureFlagProxySkipRecentlyFailedDescription` that the 2026-09-15 batch merges left in 59 dashboard catalogs (a scripted keep-both conflict resolution concatenated the key both PRs carried; `JSON.parse` silently kept the last copy) and the duplicated `ERROR_TYPE_CONTRACT` import in `src/lib/db/callLogStats.ts` (TS2300); adds `tests/unit/i18n-catalogs-no-duplicate-keys.test.ts`, a raw-text guard that fails on any key declared twice in one object of `src/i18n/messages/*.json` or `bin/cli/locales/*.json` ([#13602](https://github.com/diegosouzapw/OmniRoute/pull/13602), [#13641](https://github.com/diegosouzapw/OmniRoute/pull/13641))
|
||||
1
changelog.d/fixes/13609-mistral-401-ambiguous-auth.md
Normal file
1
changelog.d/fixes/13609-mistral-401-ambiguous-auth.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** opt-in `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` flag (default off): a bare Mistral 401 with no explicit auth signal (identical for a revoked key and an exhausted quota) cools the connection down instead of parking it as expired, at most 3 times per hour per connection before it parks, so a revoked key still converges; the ambiguity check is now one implementation shared by the connection test and the runtime ([#13609](https://github.com/diegosouzapw/OmniRoute/pull/13609)) — thanks @maxmad64bis
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stream reasoning deltas from combo targets incrementally instead of buffering them into a single burst, and stop rejecting reasoning-only streams as an empty completion (#13620) — thanks @NaNomicon
|
||||
2
changelog.d/fixes/13628-grok46-default-effort.md
Normal file
2
changelog.d/fixes/13628-grok46-default-effort.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- fix(providers): restore grok-4.6/4.5 default reasoning effort so requests without an explicit effort keep reasoning enabled (#13628)
|
||||
- fix(registry): declare supportedThinkingEfforts on claude-opus-5 and claude-fable-5 across the anthropic/claude/claude-web/ghe-copilot/github registries (#13628)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(tests):** add `dist/httpClientAbortGuard.mjs` to the expected missing-paths list in `tests/unit/pack-artifact-policy.test.ts` — [#13636](https://github.com/diegosouzapw/OmniRoute/pull/13636) registered the file in `PACK_ARTIFACT_REQUIRED_PATHS` without updating the assertion, leaving the test red on the release tip for every PR that runs it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** Kiro translator no longer re-prepends the full relocated tool-documentation block onto every subsequent turn of a multi-turn conversation; it now stays anchored to the turn that originally carried it. (#13652) — thanks @KelvinKSPS
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** opt-in `OPENCODE_RATE_LIMITED_429_EARLY_STOP` flag (default off): an opencode 429 classified as a real rate limit (parseable `Retry-After`, or a body naming a rate/usage limit) stops the cross-account wave and returns that upstream 429 unchanged — body, `Retry-After` and quota headers intact, so the opencode quota error rules still apply; unclassified 429s keep rotating, and with the flag off every 429 rotates as before (#9611) ([#13657](https://github.com/diegosouzapw/OmniRoute/pull/13657)) — thanks @maxmad64bis
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(docker):** isolate the ChatGPT Web (Codex) CDP proxy sidecar onto its own Compose network, add an opt-in `CDP_PROXY_TOKEN` auth gate to `cdp-proxy.mjs`, and stop the VNC browser-login CDP bridge from starting when no token is configured (#13679)
|
||||
1
changelog.d/fixes/13679-cloudsync-hmac-fail-open.md
Normal file
1
changelog.d/fixes/13679-cloudsync-hmac-fail-open.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(auth):** `verifyCloudSignature()` no longer accepts an unverifiable `X-Cloud-Sig` when `OMNIROUTE_CLOUD_SYNC_SECRET` is unset — a forged/garbage signature is rejected outright, and the new opt-in `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true` flag rejects unsigned Cloud-sync payloads too (default stays legacy pass-through for v3.8.x; the default flips in v3.9) ([#13679](https://github.com/diegosouzapw/OmniRoute/issues/13679))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(security):** removed the copy-pasteable placeholder `JWT_SECRET`/`API_KEY_SECRET`/`INITIAL_PASSWORD` values from the Podman Quadlet deploy manifest, and blocked remote dashboard logins with the well-known default `INITIAL_PASSWORD=CHANGEME` (#13679)
|
||||
1
changelog.d/fixes/13679-selfloop-bearer-random.md
Normal file
1
changelog.d/fixes/13679-selfloop-bearer-random.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(security):** the internal self-loop admission-bypass bearer is now a random per-process secret instead of the checked-in literal `"sk_omniroute"` when no `OMNIROUTE_API_KEY`/`ROUTER_API_KEY` is configured (#13679)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(db):** `DELETE /v1/batches/delete-completed` now caps the work it does per request and reports `hasMore` so a caller can resume, and the sweep no longer deletes a file that another batch still references (#13680, #13681)
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(i18n):** retranslate the English strings that had been copied verbatim into the locale catalogs (Spanish alone carried 7,142) and turn the real-translation ratio gate into a blocking ratchet. (#13782)
|
||||
1
changelog.d/fixes/13795-opencode-429-proxy-dedup.md
Normal file
1
changelog.d/fixes/13795-opencode-429-proxy-dedup.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(sse):** stop re-sending a request to an already-refused route after a 429 — each refused route is tried once per request ([#13795](https://github.com/diegosouzapw/OmniRoute/pull/13795)) — thanks @maxmad64bis
|
||||
1
changelog.d/fixes/agnes-cn-thinking-effort-tiers.md
Normal file
1
changelog.d/fixes/agnes-cn-thinking-effort-tiers.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): **declare Agnes CN chat models' live `reasoning_effort` vocabulary** so the catalog and sanitizer stop inventing tiers the CN API rejects. Probes on api.agnes-ai.cn (2026-09-14) match the international endpoint: 2.0/2.5 accept `none/low/medium/high/max`, 3.0 also accepts `minimal` and `xhigh`; `off`/`ultra` clamp off the wire and Hermes' default `xhigh` clamps to `max` on 2.x.
|
||||
1
changelog.d/fixes/agnes-thinking-effort-tiers.md
Normal file
1
changelog.d/fixes/agnes-thinking-effort-tiers.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): **declare Agnes chat models' live `reasoning_effort` vocabulary so catalog/builder/sanitizer stop inventing aliases the API 400s.** 2.0/2.5 accept `none/low/medium/high/max`; 3.0 also accepts `minimal` and `xhigh`. `off`/`ultra` still clamp off the wire.
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(ci):** the advisory `forgotten-sibling-tests` step no longer fails "Fast Quality Gates" when a PR touches a hub module — the cross-product of consumers × candidate tests reached millions of rows and rendering them exceeded V8's maximum string length, so the throw hit `main()`'s catch and exited 1. The report now lists at most 200 rows per section (and 5 000 per array in the JSON artifact) while the header keeps the exact totals
|
||||
1
changelog.d/fixes/claude-assistant-prefill.md
Normal file
1
changelog.d/fixes/claude-assistant-prefill.md
Normal file
@@ -0,0 +1 @@
|
||||
- Strip a trailing text-only assistant turn before official Claude OAuth dispatch. Claude returns 400 `This model does not support assistant message prefill` for that shape; the shared strip only covered Mistral.
|
||||
1
changelog.d/fixes/codex-reasoning-object-whitelist.md
Normal file
1
changelog.d/fixes/codex-reasoning-object-whitelist.md
Normal file
@@ -0,0 +1 @@
|
||||
- Fixed Codex executor forwarding client `reasoning` sub-fields (`enabled`, `max_tokens`, `exclude`) that the Codex Responses API rejects with HTTP 400, taking down every combo target with a deterministic client error. The reasoning object is now whitelisted to `effort`/`summary`, and `enabled: false` maps to effort `none` when no more specific effort was requested.
|
||||
1
changelog.d/fixes/combo-probe-no-thinking.md
Normal file
1
changelog.d/fixes/combo-probe-no-thinking.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(combos):** Gemini combo probes send `reasoning_effort: none` so thinking does not eat the health-check budget; truncated `finish_reason: length` responses are no longer rewritten as empty-content 502s
|
||||
1
changelog.d/fixes/gemini-38-think-level.md
Normal file
1
changelog.d/fixes/gemini-38-think-level.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(gemini):** send Gemini 3.8 `thinkingLevel` instead of a numeric `thinkingBudget`, and omit `includeThoughts` unless the client asked, so hidden thoughts stop eating `maxOutputTokens`
|
||||
1
changelog.d/fixes/release-acceptance-shadow.md
Normal file
1
changelog.d/fixes/release-acceptance-shadow.md
Normal file
@@ -0,0 +1 @@
|
||||
- Add a shadow release-acceptance report next to release-green.json. It does not close #12732 and is not a Mergify required check.
|
||||
1
changelog.d/fixes/sensenova-deepseek-v4-effort-clamp.md
Normal file
1
changelog.d/fixes/sensenova-deepseek-v4-effort-clamp.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** clamp SenseNova DeepSeek V4 Flash `reasoning_effort` `xhigh`/`max` to `high` (upstream lists `xhigh` then 400s it)
|
||||
@@ -0,0 +1 @@
|
||||
- **chore(skills):** regenerate the `omni-settings` agent skill after the pool egress-observation route landed (#13581), clearing the `check:agent-skills-sync` base-red (#12732)
|
||||
@@ -0,0 +1 @@
|
||||
- **test(call-logs):** the early-keepalive merge and video-bridge redaction tests pass a `traceId` (defaulting to `pendingRequestId`) now that #13546 keys each attempt's call-log row on it, the dashboard `request.failed` redaction probe reads the persisted row by `traceId`, and the keepalive test polls against a 30s wall-clock deadline like the video-bridge test instead of a 2.4s try count ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(db):** drop the duplicated `ERROR_TYPE_CONTRACT` import in `src/lib/db/callLogStats.ts` left by the #13641 merge; the TS2300 duplicate-identifier error failed the API-route and dashboard typecheck gates on every PR ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(cli):** add the `serve.ready_timeout` string to the `en`, `zh-CN` and `zh-TW` CLI catalogs; `--ready-timeout` shipped calling `t("serve.ready_timeout")` without a catalog entry, which the CLI i18n key-coverage and parity tests report ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(ci):** re-freeze `tests/unit/image-generation-handler.test.ts` (2133→2235, #13748) and `tests/unit/batch_api.test.ts` (1345→1348, #13749) at their merged size; PR-mode `check:file-size` does not relax `testFrozen` against the base, so that regression coverage turned the gate red on every PR into the release line ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(ci):** allowlist the Uzbek `outputTokenDesc` translation ("Yakunlash/javob tokenlari") and the `PROTECTED_PRIORITY_INFRA_502_ENABLED` feature-flag id (#13439) in `.gitleaks.toml`; the `generic-api-key` rule reads the `...TokenDesc` key and the flag `key:` as token assignments, which the secrets ratchet reported as new findings ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **test(models):** the custom Jina specialty-model catalog test expects the `jina-ai/` prefix again: custom rows keep the connection provider id, only synced rows resolve through the `jina` alias, and #13403 had switched the custom assertion to `jina/` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **chore(build):** ship `httpClientAbortGuard.mjs` in the published tarball — the #13636 crash guard was a new `server-ws.mjs` import missing from both pack-artifact allowlists (#12732)
|
||||
@@ -0,0 +1 @@
|
||||
- **test(settings):** the #6540 paid-target tests now use `gemini/gemini-3.1-pro-preview` as the paid fixture and assert the fixtures still classify as paid/free/unknown; the old Together target became "unknown" once #13407 removed Together's one-time signup credit from the free catalog, so the three save-time blocking tests read a correct 200 as a missing guard ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(ci):** register `noauth-model-lockout`, `local-token-budget-429-skips-cooldown`, `free-badge-provider-gate` (#13645) and `daily-reset-tz-threading` (#13440) in `stryker.conf.json` `tap.testFiles`; they cover `accountFallback.ts`/`auth.ts`/`comboPredicates.ts`/`rrState.ts`, so the strict `mutation-test-coverage` gate failed Fast Quality Gates on every PR into the release line ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
|
||||
@@ -0,0 +1 @@
|
||||
- **chore(ci):** register the #13609 ambiguous-401 regression test in the mutation-coverage config, clearing the second `check:agent-skills-sync`/`mutation-test-coverage` base-red (#12732)
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"open-sse/config/providers/registry/claude/index.ts": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 7
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"open-sse/config/providers/registry/vertex/index.ts": {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"_rebaseline_2026_09_15_13572_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/base.ts->1754. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_15_13445_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/utils/proxyFetch.ts->1296. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_15_13643_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/codex.ts->1528. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_15_13344_conversation_turn_nodes_retention_field": "PR #13344 rework: src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx 1596->1597 (+1 checker count on the pure tip, matching the existing frozen 1597; this rework's own edit only changes the literal default shown in the retentionFields row from 1 to 30, no line added/removed). The one added line is the PR's own retentionFields row (conversationTurnNodes) exposing the new independent retention.conversationTurnNodes knob (src/types/databaseSettings.ts) added by the same PR. Irreducible: one row per existing retention setting in this table. Covered by tests/unit/db-cleanup-conversation-nodes-12453.test.ts.",
|
||||
"_rebaseline_2026_09_15_13609_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/services/accountFallback.ts->2507. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_15_13602_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): src/sse/handlers/chatHelpers.ts->1214. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_15_13580_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): src/sse/handlers/chatHelpers.ts->1202. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.",
|
||||
"_rebaseline_2026_09_13_13581_pool_egress_observation": "PR #13581 own growth: src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx 1475->1477 (+2 = the PoolEgressObservation import and its one-line mount under the pool members label). The observation itself lives outside the frozen file, all under cap: PoolEgressObservation.tsx, the dedicated GET /api/settings/proxies/pool/egress-observation route, src/lib/proxyPoolEgressObservation.ts and getPoolEgressObservation in src/lib/db/proxyLogs.ts. Only the mount point is irreducible. Covered by tests/unit/proxy-pool-egress-observation.test.ts, tests/unit/proxy-pool-egress-observation-route.test.ts and tests/unit/ui/PoolEgressObservation.test.tsx.",
|
||||
@@ -234,12 +239,13 @@
|
||||
"_rebaseline_2026_07_22_8213_combo_config_cooldown_wait_tests": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: tests/unit/combo-config.test.ts 880->940 (+60, entirely this PR's diff — testFrozen add covering isComboCooldownWaitEligible (gating cooldown-wait to auto/quota-share strategies with the feature enabled) and resolveComboTargetTimeoutMsForCombo (raising the per-target timeout floor to cover the cooldown-wait budget + buffer for eligible strategies, fixing the 120s default cutting off a 130s wait early and returning a synthetic 524)). Covered by the new assertions themselves.",
|
||||
"_rebaseline_2026_07_23_8122_codex_image_edits": "#8122 (@xiaoyaner0201) own growth: tests/unit/image-generation-handler.test.ts 2019->2029 (+10) — new coverage for Codex reference image edits (POST /v1/images/edits) plus the sanitizeImageProviderError/redactSensitiveErrorText hardening it introduces. Test-only growth at the existing handler test file.",
|
||||
"_rebaseline_2026_07_25_8510_adobe_firefly_reference_images_tests": "#8510 (artickc, feat/adobe-firefly-reference-images) own test growth: tests/unit/adobe-firefly.test.ts 711->871 (+159, entirely this PR's diff — new referenceBlobs upload/dispatch coverage for handleAdobeFireflyImageGeneration, resolveAdobeSourceImageIds, and the storage-upload wire contract). Route-level /v1/images/edits coverage (credentials/rate-limit/4-ref-cap branches added to route.ts) lives in the new tests/unit/8510-adobe-firefly-edits-route.test.ts instead of growing this file further.",
|
||||
"_rebaseline_2026_09_15_13748_13749_merged_test_growth_basereds": "Base-red drain (#12732): two security fixes merged on 2026-09-15 each grew a frozen test file with their own regression coverage, and PR-mode check:file-size does not relax testFrozen against the base, so every PR into release/v3.8.51 went red on file-size. Recorded against the merged state: tests/unit/image-generation-handler.test.ts 2133->2235 (#13748 public-only guard on client-supplied image URLs); tests/unit/batch_api.test.ts 1345->1348 (#13749 API-key ownership on files and batches). No cap is raised beyond the merged LOC.",
|
||||
"_rebaseline_2026_08_24_video_bridge_fu01_fu03_fu04_result_cache_tests": "PRs #11362 (FU-01 cache hardening) + #11382 (FU-03 visual dedup policy identity) + #11383 (FU-04 focused analysis mode) own test growth: videoBridgeResultCache.test.ts <1000->1040, +40 (sum of three stacked PRs boarded together in the same merge-batch, each adding its own cache-identity assertions on the shared result-cache seam). Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive).",
|
||||
"_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').",
|
||||
"_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.",
|
||||
"tests/integration/chat-pipeline.test.ts": 1736,
|
||||
"tests/unit/account-fallback-service.test.ts": 2056,
|
||||
"tests/unit/batch_api.test.ts": 1345,
|
||||
"tests/unit/batch_api.test.ts": 1348,
|
||||
"tests/unit/cc-compatible-provider.test.ts": 1225,
|
||||
"tests/unit/chatcore-translation-paths.test.ts": 3447,
|
||||
"tests/unit/chatgpt-web.test.ts": 4911,
|
||||
@@ -248,7 +254,7 @@
|
||||
"tests/unit/executor-codex.test.ts": 1465,
|
||||
"tests/unit/executor-default-base.test.ts": 1632,
|
||||
"tests/unit/grok-web.test.ts": 2985,
|
||||
"tests/unit/image-generation-handler.test.ts": 2133,
|
||||
"tests/unit/image-generation-handler.test.ts": 2235,
|
||||
"tests/unit/models-catalog-route.test.ts": 1653,
|
||||
"tests/unit/perplexity-web.test.ts": 1384,
|
||||
"tests/unit/provider-models-route.test.ts": 1783,
|
||||
@@ -345,6 +351,7 @@
|
||||
"_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)",
|
||||
"frozen": {
|
||||
"src/sse/handlers/chatHelpers.ts": 1214,
|
||||
"_rebaseline_2026_09_15_13609_mistral_ambiguous_401": "PR #13609 rework (maxmad64bis, bare Mistral 401 soft lockout behind MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT, default off). open-sse/services/accountFallback.ts 2469->2501 (+32): +14 are the change itself (shared-predicate + flag imports, the documented ambiguousAuth field on the checkFallbackError return type, and the flag-gated 401 branch formatted normally instead of the PR's 139-char squeezed configuredRule line); +18 are the lint-staged prettier pass normalizing lines that were already unformatted on the release tip (multi-import, ISO_RETRY_RE, two regex arrays, persistAntigravityFamilyCooldownIfQuota call, applyErrorState guard, trailing commas) — pure formatting, no logic. src/sse/services/auth.ts 3556->3557 (+1): markAccountUnavailable passes connectionId to resolveTerminalConnectionStatus so the soft-strike bound is per connection. The predicate and strike tracker live in the leaf open-sse/services/accountFallback/mistralAmbiguousAuth.ts (under cap). Covered by tests/unit/provider-401-ambiguous-runtime.test.ts (flag off/on, end-to-end through markAccountUnavailable).",
|
||||
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
|
||||
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
|
||||
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
|
||||
@@ -445,9 +452,9 @@
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
|
||||
"open-sse/executors/antigravity.ts": 1665,
|
||||
"open-sse/executors/base.ts": 1753,
|
||||
"open-sse/executors/base.ts": 1754,
|
||||
"open-sse/executors/chatgpt-web.ts": 5056,
|
||||
"open-sse/executors/codex.ts": 1505,
|
||||
"open-sse/executors/codex.ts": 1528,
|
||||
"open-sse/executors/cursor.ts": 1759,
|
||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||
"open-sse/handlers/chatCore.ts": 6146,
|
||||
@@ -455,13 +462,13 @@
|
||||
"open-sse/handlers/search.ts": 1789,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1621,
|
||||
"open-sse/mcp-server/server.ts": 1572,
|
||||
"open-sse/services/accountFallback.ts": 2493,
|
||||
"open-sse/services/accountFallback.ts": 2507,
|
||||
"open-sse/services/adobeFireflyBrowserLogin.ts": 1401,
|
||||
"open-sse/services/combo.ts": 4080,
|
||||
"open-sse/services/combo/executeTargetAttempt.ts": 1228,
|
||||
"open-sse/translator/response/openai-responses.ts": 1466,
|
||||
"open-sse/utils/cursorAgentProtobuf.ts": 1547,
|
||||
"open-sse/utils/proxyFetch.ts": 1275,
|
||||
"open-sse/utils/proxyFetch.ts": 1296,
|
||||
"open-sse/utils/stream.ts": 3098,
|
||||
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": 4398,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1335,
|
||||
@@ -476,7 +483,7 @@
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1477,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1271,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1607,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1597,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1598,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2152,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2432,
|
||||
"src/app/api/providers/[id]/test/route.ts": 1252,
|
||||
@@ -491,12 +498,13 @@
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1502,
|
||||
"src/shared/services/cliRuntime.ts": 1296,
|
||||
"src/sse/handlers/chat.ts": 2490,
|
||||
"src/sse/services/auth.ts": 3556,
|
||||
"src/sse/services/auth.ts": 3557,
|
||||
"tests/unit/account-fallback-service.test.ts": 2453,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 4656,
|
||||
"open-sse/services/autoCombo/virtualFactory.ts": 1230,
|
||||
"open-sse/services/combo/roundRobinCombo.ts": 1213
|
||||
},
|
||||
"_rebaseline_2026_09_15_roundrobin_dashboard_events": "Fix #13089 (Combo Studio Live dashboard shows an empty backlog for round-robin combos): open-sse/services/combo/roundRobinCombo.ts 1205->1213. Round-robin is the only combo strategy that bypasses handleComboChat/executeTargetAttempt.ts, the path that publishes the combo.target.attempt/succeeded/failed EventBus events the Live dashboard listens for — so round-robin completions never showed up. The new call-site wiring (createRRDashboardEvents(...) instantiated once per target, one-line .attempt()/.succeeded()/.failed() calls at the 6 existing dispatch/outcome points) is the emitter logic actually extracted into a new module, open-sse/services/combo/rrDashboardEvents.ts — this is the minimum irreducible footprint for wiring 6 required call sites into 6 fixed control-flow points of the frozen file. Covered by tests/unit/issue-13089-roundrobin-live-ws-events.test.ts (2 tests: success + failure paths).",
|
||||
"_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).",
|
||||
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
|
||||
|
||||
@@ -2,70 +2,70 @@
|
||||
"_comment": "Catraca de tradução real (valor idêntico ao en.json, placeholder ou ausente, fora do allowlist untranslatable-keys.json) em % por locale. Só pode cair. Atualize via `npm run i18n:check-ratio:update` quando um locale melhora. Valores medidos, nunca chutados.",
|
||||
"slack": 0.5,
|
||||
"locales": {
|
||||
"am": 1.8,
|
||||
"ar": 4,
|
||||
"az": 48.8,
|
||||
"bg": 24.7,
|
||||
"bn": 44.1,
|
||||
"cs": 22.7,
|
||||
"da": 27.9,
|
||||
"de": 26,
|
||||
"el": 3.2,
|
||||
"es": 56.2,
|
||||
"et": 2.4,
|
||||
"fa": 43.8,
|
||||
"fi": 25.1,
|
||||
"fr": 25.9,
|
||||
"ga": 2.7,
|
||||
"gu": 43.5,
|
||||
"ha": 2.8,
|
||||
"he": 25.4,
|
||||
"hi": 25.8,
|
||||
"hr": 3.5,
|
||||
"hu": 27,
|
||||
"hy": 1.6,
|
||||
"id": 27.2,
|
||||
"ig": 2.8,
|
||||
"it": 26.9,
|
||||
"ja": 25.6,
|
||||
"ka": 1.7,
|
||||
"km": 2.1,
|
||||
"kn": 2,
|
||||
"ko": 26.8,
|
||||
"lt": 2.1,
|
||||
"lv": 2.6,
|
||||
"ml": 1.9,
|
||||
"mr": 44,
|
||||
"ms": 26.8,
|
||||
"mt": 3.5,
|
||||
"my": 3.5,
|
||||
"ne": 1.8,
|
||||
"nl": 28.6,
|
||||
"no": 28.4,
|
||||
"or": 1.8,
|
||||
"pa": 1.8,
|
||||
"phi": 33.4,
|
||||
"pl": 10.5,
|
||||
"pt": 5.5,
|
||||
"pt-BR": 16.9,
|
||||
"ro": 28.4,
|
||||
"ru": 20.3,
|
||||
"si": 1.9,
|
||||
"sk": 27.3,
|
||||
"sl": 2.4,
|
||||
"sr": 2.8,
|
||||
"sv": 27.6,
|
||||
"sw": 44,
|
||||
"ta": 43.7,
|
||||
"te": 43.2,
|
||||
"th": 25.4,
|
||||
"tr": 22.4,
|
||||
"uk-UA": 23.4,
|
||||
"ur": 43.6,
|
||||
"uz": 2.7,
|
||||
"vi": 4.9,
|
||||
"yo": 2.1,
|
||||
"zh-CN": 3.7,
|
||||
"zh-TW": 4.3
|
||||
"am": 1.4,
|
||||
"ar": 0.9,
|
||||
"az": 2.2,
|
||||
"bg": 1.2,
|
||||
"bn": 1.2,
|
||||
"cs": 2.1,
|
||||
"da": 3.6,
|
||||
"de": 3,
|
||||
"el": 1.5,
|
||||
"es": 2.1,
|
||||
"et": 1.8,
|
||||
"fa": 1.1,
|
||||
"fi": 1.5,
|
||||
"fr": 3.4,
|
||||
"ga": 1.5,
|
||||
"gu": 1.2,
|
||||
"ha": 1.5,
|
||||
"he": 1.1,
|
||||
"hi": 1,
|
||||
"hr": 2.2,
|
||||
"hu": 1.6,
|
||||
"hy": 1.3,
|
||||
"id": 2.5,
|
||||
"ig": 1.6,
|
||||
"it": 2.6,
|
||||
"ja": 1.1,
|
||||
"ka": 1.4,
|
||||
"km": 1.4,
|
||||
"kn": 1.3,
|
||||
"ko": 1.3,
|
||||
"lt": 1.5,
|
||||
"lv": 1.6,
|
||||
"ml": 1.4,
|
||||
"mr": 1.3,
|
||||
"ms": 2.4,
|
||||
"mt": 2.3,
|
||||
"my": 1.5,
|
||||
"ne": 1.4,
|
||||
"nl": 3.8,
|
||||
"no": 2.5,
|
||||
"or": 1.4,
|
||||
"pa": 1.4,
|
||||
"phi": 3.2,
|
||||
"pl": 2.4,
|
||||
"pt": 2,
|
||||
"pt-BR": 2.5,
|
||||
"ro": 2.7,
|
||||
"ru": 1,
|
||||
"si": 1.4,
|
||||
"sk": 2,
|
||||
"sl": 1.8,
|
||||
"sr": 1.4,
|
||||
"sv": 2.7,
|
||||
"sw": 1.5,
|
||||
"ta": 1.3,
|
||||
"te": 1.2,
|
||||
"th": 1.1,
|
||||
"tr": 1.6,
|
||||
"uk-UA": 1.2,
|
||||
"ur": 1.2,
|
||||
"uz": 2.2,
|
||||
"vi": 1.5,
|
||||
"yo": 1.4,
|
||||
"zh-CN": 1,
|
||||
"zh-TW": 1.1
|
||||
}
|
||||
}
|
||||
|
||||
183
config/quality/release-acceptance.schema.json
Normal file
183
config/quality/release-acceptance.schema.json
Normal file
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"$id": "https://omniroute.local/quality/release-acceptance.schema.json",
|
||||
"title": "Release acceptance report",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"identity",
|
||||
"required_gates",
|
||||
"gates",
|
||||
"evidence_errors",
|
||||
"verdict",
|
||||
"artifact"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": { "type": "integer", "const": 1 },
|
||||
"identity": { "$ref": "#/$defs/identity" },
|
||||
"required_gates": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/gateInstanceKey" }
|
||||
},
|
||||
"gates": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/gateResult" }
|
||||
},
|
||||
"evidence_errors": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/evidenceError" }
|
||||
},
|
||||
"verdict": { "enum": ["VERIFIED", "FAILED", "UNVERIFIED"] },
|
||||
"artifact": {
|
||||
"anyOf": [
|
||||
{ "type": "null" },
|
||||
{ "$ref": "#/$defs/artifact" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "verdict": { "const": "VERIFIED" } }, "required": ["verdict"] },
|
||||
"then": { "properties": { "required_gates": { "minItems": 1 } } }
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"sha": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
},
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"gateInstanceKey": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["gate_id", "suite_id", "shard_index", "shard_total"],
|
||||
"properties": {
|
||||
"gate_id": { "type": "string", "minLength": 1 },
|
||||
"suite_id": { "type": ["string", "null"] },
|
||||
"shard_index": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"shard_total": { "type": ["integer", "null"], "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"repository",
|
||||
"run_id",
|
||||
"run_attempt",
|
||||
"workflow",
|
||||
"trigger",
|
||||
"scope",
|
||||
"requested_ref",
|
||||
"base_sha",
|
||||
"candidate_sha",
|
||||
"tested_sha"
|
||||
],
|
||||
"properties": {
|
||||
"repository": { "type": "string", "minLength": 1 },
|
||||
"run_id": { "type": "string", "minLength": 1 },
|
||||
"run_attempt": { "type": "integer", "minimum": 1 },
|
||||
"workflow": { "type": "string", "minLength": 1 },
|
||||
"trigger": { "type": "string", "minLength": 1 },
|
||||
"scope": { "enum": ["pr", "release", "scheduled"] },
|
||||
"requested_ref": { "type": "string", "minLength": 1 },
|
||||
"base_sha": { "$ref": "#/$defs/sha" },
|
||||
"candidate_sha": { "$ref": "#/$defs/sha" },
|
||||
"tested_sha": { "$ref": "#/$defs/sha" }
|
||||
}
|
||||
},
|
||||
"evidenceRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["artifact_id", "member", "algorithm", "digest"],
|
||||
"properties": {
|
||||
"artifact_id": { "type": "string", "minLength": 1 },
|
||||
"member": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^(?!/)(?!.*(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$))[^\\s]+$"
|
||||
},
|
||||
"algorithm": { "const": "sha256" },
|
||||
"digest": { "$ref": "#/$defs/digest" }
|
||||
}
|
||||
},
|
||||
"artifact": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["algorithm", "digest", "identity"],
|
||||
"properties": {
|
||||
"algorithm": { "const": "sha256" },
|
||||
"digest": { "$ref": "#/$defs/digest" },
|
||||
"identity": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
},
|
||||
"evidenceError": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["code", "gate", "detail"],
|
||||
"properties": {
|
||||
"code": { "type": "string", "minLength": 1 },
|
||||
"gate": { "$ref": "#/$defs/gateInstanceKey" },
|
||||
"detail": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"gateResult": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"gate_id",
|
||||
"suite_id",
|
||||
"shard_index",
|
||||
"shard_total",
|
||||
"tested_sha",
|
||||
"run_id",
|
||||
"run_attempt",
|
||||
"command_id",
|
||||
"gate_type",
|
||||
"status",
|
||||
"cause",
|
||||
"exit_code",
|
||||
"duration_ms",
|
||||
"evidence"
|
||||
],
|
||||
"properties": {
|
||||
"gate_id": { "type": "string", "minLength": 1 },
|
||||
"suite_id": { "type": ["string", "null"] },
|
||||
"shard_index": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"shard_total": { "type": ["integer", "null"], "minimum": 1 },
|
||||
"tested_sha": { "$ref": "#/$defs/sha" },
|
||||
"run_id": { "type": "string", "minLength": 1 },
|
||||
"run_attempt": { "type": "integer", "minimum": 1 },
|
||||
"command_id": { "type": "string", "minLength": 1 },
|
||||
"gate_type": { "enum": ["static", "test", "artifact"] },
|
||||
"status": { "enum": ["PASS", "FAIL", "INFRA_ERROR", "SKIPPED"] },
|
||||
"reason": { "type": "string", "minLength": 1 },
|
||||
"cause": {
|
||||
"anyOf": [
|
||||
{ "type": "null" },
|
||||
{ "$ref": "#/$defs/gateInstanceKey" }
|
||||
]
|
||||
},
|
||||
"exit_code": { "type": ["integer", "null"] },
|
||||
"duration_ms": { "type": "integer", "minimum": 0 },
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/evidenceRef" }
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "status": { "const": "SKIPPED" } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": { "required": ["reason"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,28 @@ cp contrib/podman/*.network ~/.config/containers/systemd/omniroute/
|
||||
cp contrib/podman/*.volume ~/.config/containers/systemd/omniroute/
|
||||
```
|
||||
|
||||
### 3. Mount the project .env for secrets
|
||||
### 3. Generate secrets before first start
|
||||
|
||||
`omniroute.container` no longer ships `JWT_SECRET` / `API_KEY_SECRET` /
|
||||
`INITIAL_PASSWORD` values — earlier versions shipped copy-pasteable
|
||||
placeholders (`change-me-to-a-random-base64-string`,
|
||||
`change-me-to-a-random-hex-string`) that an operator could forget to
|
||||
rotate, leaving the deployment with a public, guessable secret/password
|
||||
(#13679). Generate real ones and put them in your project `.env`:
|
||||
|
||||
```bash
|
||||
echo "JWT_SECRET=$(openssl rand -base64 48)" >> .env
|
||||
echo "API_KEY_SECRET=$(openssl rand -hex 32)" >> .env
|
||||
echo "INITIAL_PASSWORD=$(openssl rand -hex 24)" >> .env
|
||||
```
|
||||
|
||||
If you skip this: `JWT_SECRET`/`API_KEY_SECRET` are auto-generated and
|
||||
persisted on first boot, and the dashboard requires setup from `localhost`
|
||||
before it accepts any password — safer than a literal default, but a real
|
||||
`INITIAL_PASSWORD` is still recommended so a non-interactive first boot has
|
||||
a known credential to log in with.
|
||||
|
||||
### 4. Mount the project .env for secrets
|
||||
|
||||
Edit `~/.config/containers/systemd/omniroute/omniroute.container` and
|
||||
uncomment/replace the `EnvironmentFile` line with the absolute path to
|
||||
@@ -54,7 +75,7 @@ Make sure `CONTAINER_HOST=podman` is set in that `.env`.
|
||||
|
||||
Alternatively, edit the env vars directly in the `.container` file.
|
||||
|
||||
### 4. Reload systemd and start
|
||||
### 5. Reload systemd and start
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
@@ -62,7 +83,7 @@ systemctl --user start omniroute-redis
|
||||
systemctl --user start omniroute
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
### 6. Verify
|
||||
|
||||
```bash
|
||||
systemctl --user status omniroute
|
||||
|
||||
@@ -28,14 +28,23 @@ Environment=DASHBOARD_PORT=20128
|
||||
Environment=API_PORT=20129
|
||||
Environment=API_HOST=0.0.0.0
|
||||
Environment=REDIS_URL=redis://redis:6379
|
||||
Environment=JWT_SECRET=change-me-to-a-random-base64-string
|
||||
Environment=API_KEY_SECRET=change-me-to-a-random-base64-string
|
||||
Environment=INITIAL_PASSWORD=change-me-to-a-random-hex-string
|
||||
Environment=NODE_ENV=production
|
||||
Environment=REQUIRE_API_KEY=true
|
||||
|
||||
# Load additional secrets (API keys, OAuth creds) from the project .env:
|
||||
# JWT_SECRET, API_KEY_SECRET and INITIAL_PASSWORD are deliberately NOT set here.
|
||||
# This unit used to ship copy-pasteable "replace-me" placeholder literals — an
|
||||
# operator who forgot to replace them ran production with a public, guessable
|
||||
# secret and dashboard password (#13679). Generate real values and load them from
|
||||
# your project .env before the FIRST start — see "Generate secrets before first
|
||||
# start" in contrib/podman/README.md — by uncommenting and pointing this at your
|
||||
# project .env:
|
||||
# EnvironmentFile=%h/code/docker/OmniRoute/.env
|
||||
#
|
||||
# If left unset: JWT_SECRET and API_KEY_SECRET are auto-generated and persisted
|
||||
# on first boot, and the dashboard requires setup from localhost before it
|
||||
# accepts any password (see managementPassword.ts / apiAuth.ts) — safer than a
|
||||
# known-literal default either way, but a real INITIAL_PASSWORD is still
|
||||
# recommended for non-interactive first boots.
|
||||
|
||||
HealthCmd=node /app/healthcheck.mjs
|
||||
HealthInterval=30s
|
||||
|
||||
@@ -142,11 +142,24 @@ services:
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
# SECURITY (#13679): joins BOTH `default` (to keep reaching redis and the
|
||||
# other sidecars) AND the dedicated `chatgpt-web-codex-net` (the one
|
||||
# legitimate consumer of the CDP proxy below).
|
||||
networks:
|
||||
- default
|
||||
- chatgpt-web-codex-net
|
||||
profiles:
|
||||
- web
|
||||
|
||||
# Internal-only Chromium runtime for ChatGPT Web (Codex). No CDP or browser
|
||||
# UI port is published to the host.
|
||||
#
|
||||
# SECURITY (#13679): isolated onto its own `chatgpt-web-codex-net` network
|
||||
# instead of the shared implicit default bridge — its cdp-proxy.mjs
|
||||
# sidecar republishes Chromium's CDP on 0.0.0.0:9223, and CDP grants full
|
||||
# control over a live browser session. Without this isolation, any
|
||||
# compromised sibling container (redis, qdrant, bifrost, cliproxyapi,
|
||||
# codex-app-server, ...) on the default network could reach it.
|
||||
chatgpt-web-codex-browser:
|
||||
build:
|
||||
context: .
|
||||
@@ -154,8 +167,12 @@ services:
|
||||
image: omniroute:chatgpt-web-codex-browser
|
||||
restart: unless-stopped
|
||||
shm_size: "2gb"
|
||||
environment:
|
||||
- CDP_PROXY_TOKEN=${CDP_PROXY_TOKEN:-}
|
||||
volumes:
|
||||
- chatgpt-web-codex-browser-data:/browser-profile
|
||||
networks:
|
||||
- chatgpt-web-codex-net
|
||||
profiles:
|
||||
- web
|
||||
|
||||
@@ -370,7 +387,12 @@ services:
|
||||
# compose network by the omniroute app.
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "node", "-e", "require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -378,6 +400,13 @@ services:
|
||||
profiles:
|
||||
- codex-app-server
|
||||
|
||||
networks:
|
||||
# SECURITY (#13679): dedicated network for the unauthenticated-by-default
|
||||
# CDP proxy sidecar (docker/chatgpt-web-codex-browser/cdp-proxy.mjs) —
|
||||
# shared only with omniroute-web, not with redis/qdrant/bifrost/cliproxyapi/
|
||||
# codex-app-server or any other sibling on the implicit default network.
|
||||
chatgpt-web-codex-net: {}
|
||||
|
||||
volumes:
|
||||
chatgpt-web-codex-browser-data:
|
||||
name: omniroute-chatgpt-web-codex-browser-data
|
||||
|
||||
@@ -5,6 +5,31 @@ const listenPort = 9223;
|
||||
const upstreamHost = "127.0.0.1";
|
||||
const upstreamPort = 9222;
|
||||
|
||||
// SECURITY (#13679): this proxy republishes Chromium's loopback CDP onto
|
||||
// 0.0.0.0:9223 with no auth of its own — CDP grants full control over a
|
||||
// live browser session (Runtime.evaluate, cookie theft, etc). When the
|
||||
// operator sets CDP_PROXY_TOKEN, every request/WS-upgrade MUST present it as
|
||||
// an `X-Omni-Cdp-Token: <token>` header before a single byte is forwarded
|
||||
// upstream, mirroring the gate docker/vnc-browser/chromium/cdp-bridge.py
|
||||
// already has (#12571). Left unset, the proxy keeps its historical
|
||||
// zero-config behavior — the primary mitigation for the shared-bridge risk
|
||||
// is docker-compose.yml isolating this service onto its own network so no
|
||||
// unrelated sibling container can reach it at all.
|
||||
const TOKEN = process.env.CDP_PROXY_TOKEN || "";
|
||||
const TOKEN_HEADER = "x-omni-cdp-token";
|
||||
|
||||
if (!TOKEN) {
|
||||
console.error(
|
||||
"[cdp-proxy] WARNING: running without CDP_PROXY_TOKEN — every request is forwarded " +
|
||||
"unauthenticated. Set CDP_PROXY_TOKEN to require an X-Omni-Cdp-Token header (#13679)."
|
||||
);
|
||||
}
|
||||
|
||||
function hasValidToken(headers) {
|
||||
if (!TOKEN) return true;
|
||||
return headers[TOKEN_HEADER] === TOKEN;
|
||||
}
|
||||
|
||||
function proxyHeaders(headers) {
|
||||
const next = { ...headers, host: `${upstreamHost}:${upstreamPort}` };
|
||||
delete next.connection;
|
||||
@@ -13,6 +38,11 @@ function proxyHeaders(headers) {
|
||||
}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
if (!hasValidToken(request.headers)) {
|
||||
response.writeHead(403, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "missing or invalid X-Omni-Cdp-Token" }));
|
||||
return;
|
||||
}
|
||||
const upstream = http.request(
|
||||
{
|
||||
host: upstreamHost,
|
||||
@@ -48,6 +78,10 @@ const server = http.createServer((request, response) => {
|
||||
});
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
if (!hasValidToken(request.headers)) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const upstream = net.connect(upstreamPort, upstreamHost, () => {
|
||||
const upgradeHeaders = {
|
||||
...request.headers,
|
||||
@@ -69,4 +103,6 @@ server.on("upgrade", (request, socket, head) => {
|
||||
upstream.on("error", () => socket.destroy());
|
||||
});
|
||||
|
||||
server.listen(listenPort, "0.0.0.0");
|
||||
server.listen(listenPort, "0.0.0.0", () => {
|
||||
console.error(`[cdp-proxy] listening on 0.0.0.0:${listenPort}`);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,13 @@ if [[ "${PIXELFLUX_WAYLAND,,}" == "true" ]]; then
|
||||
echo "[svc-de] ${SOCKET_PATH} found launching de"
|
||||
cd $HOME
|
||||
# OmniRoute: bridge Chromium DevTools (127.0.0.1:9222) to 0.0.0.0:9223.
|
||||
( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) &
|
||||
# SECURITY (#13679): only start the bridge when CDP_BRIDGE_TOKEN is
|
||||
# configured — cdp-bridge.py already fails closed for every caller when
|
||||
# it is unset (#12571), so an unconfigured container gains nothing by
|
||||
# running an always-listening 0.0.0.0:9223 process anyway.
|
||||
if [ -n "${CDP_BRIDGE_TOKEN:-}" ]; then
|
||||
( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) &
|
||||
fi
|
||||
exec s6-setuidgid abc \
|
||||
/bin/bash /defaults/startwm_wayland.sh &
|
||||
PID=$!
|
||||
@@ -57,7 +63,13 @@ chmod 777 /tmp/selkies*
|
||||
# run
|
||||
cd $HOME
|
||||
# OmniRoute: bridge Chromium DevTools (127.0.0.1:9222) to 0.0.0.0:9223.
|
||||
( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) &
|
||||
# SECURITY (#13679): only start the bridge when CDP_BRIDGE_TOKEN is
|
||||
# configured — cdp-bridge.py already fails closed for every caller when it
|
||||
# is unset (#12571), so an unconfigured container gains nothing by running
|
||||
# an always-listening 0.0.0.0:9223 process anyway.
|
||||
if [ -n "${CDP_BRIDGE_TOKEN:-}" ]; then
|
||||
( sleep 8; python3 /usr/local/bin/cdp-bridge.py >/proc/1/fd/2 2>&1 ) &
|
||||
fi
|
||||
exec s6-setuidgid abc \
|
||||
/bin/bash /defaults/startwm.sh &
|
||||
PID=$!
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user