Compare commits

..

6 Commits

Author SHA1 Message Date
diegosouzapw
20c0a0b9f4 docs(changelog): point the batch-2 fragment at #13660 2026-09-14 13:46:36 -03:00
diegosouzapw
d0eb0f372b docs(i18n): re-sync the eight new llm.txt mirrors with the root file
The base merge moved the root llm.txt; docs-sync keeps every mirror body
byte-identical to the root minus its top heading, with the mirror heading and
language bar above the --- separator.
2026-09-14 13:45:52 -03:00
diegosouzapw
8dddc5202b Merge remote-tracking branch 'origin/release/v3.8.51' into feat/i18n-batch-sa 2026-09-14 13:44:31 -03:00
diegosouzapw
d7cbaac9fe feat(i18n): add 8 South and Southeast Asian locales (kn ml or pa ne si my km)
Batch 2 of the locale-expansion plan: Kannada, Malayalam, Odia, Punjabi,
Nepali, Sinhala, Burmese and Khmer across every surface — dashboard catalog,
docs mirrors, CLI catalog, README, locale index and the marketing site.
51 → 59 locales. The four Indian languages share the 🇮🇳 flag through
--flag-file=in.svg.

Real-translation ratio of the new catalogs: 1.7 % (or) to 3.4 % (my) of
leaves still identical to English, measured by i18n:check-ratio.
2026-09-14 13:37:35 -03:00
diegosouzapw
455aade660 fix(i18n): split oversized docs sections before translating
chunkMarkdown only cut on '## ' headings, so a single long section (README.md
carries a 16 KB one, USER_GUIDE.md a 20 KB one) became one oversized request.
On the slow fallback model that request could not finish inside the backend's
10-minute fetch timeout, and the three biggest docs of every new locale failed
with 'fetch failed' on every retry — Odia burned eight attempts on them.

A section still longer than maxChars is now split again on sub-headings and
paragraph boundaries, never inside a fenced code block; a block that is itself
larger than the limit stays whole rather than being cut mid-paragraph.
2026-09-11 16:08:49 -03:00
diegosouzapw
e89a4da2b0 fix(i18n): keep the ICU literal escape around angle placeholders when translating
English writes '<name>' — the single quotes are ICU's escape, so the span
renders as literal text. Every backend drops them, and the translated message
then parses as an unclosed ICU tag: the nine locales of the first batch each
shipped two such strings and only CI caught them.

translateString and translateBatch now restore the quoting, doubling an
apostrophe inside the span so it does not close the literal early (Estonian
OmniRoute'i, Irish d'eochair). Messages whose English mixes real markup with a
literal span are left untouched, since there is no safe way to tell the two
apart.
2026-09-10 12:39:15 -03:00
2090 changed files with 143966 additions and 476147 deletions

View File

@@ -737,12 +737,6 @@ NEXT_PUBLIC_CLOUD_URL=
ENABLE_SOCKS5_PROXY=true
NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Opt-in feature flag (default off; a dashboard DB override wins over this value): proxy pools
# and per-account rotation stop re-serving a member that just failed (TCP probe refused, or a
# 429 received through it) for a period that doubles on each repeat, up to a cap. No proxy
# status is written. "true" (or 1, yes) enables it; unset keeps plain selection.
# PROXY_SKIP_RECENTLY_FAILED=false
# Standard proxy variables (lowercase variants also supported).
# HTTP_PROXY=http://127.0.0.1:7890
# HTTPS_PROXY=http://127.0.0.1:7890
@@ -1146,22 +1140,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
#OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS=21600000
# Frequent wal_checkpoint(PASSIVE) cadence (ms). Set to 0 to disable. Default: 300000 (5m).
# Used by: src/lib/db/walMaintenance.ts.
#OMNIROUTE_WAL_PASSIVE_INTERVAL_MS=300000
# WAL size (MB) above which a PASSIVE tick escalates to wal_checkpoint(TRUNCATE). Default: 256.
# Used by: src/lib/db/walMaintenance.ts.
#OMNIROUTE_WAL_GUARD_MAX_MB=256
# Minimum rows a cleanup must delete before the post-cleanup VACUUM runs. Default: 1000.
# 0 always vacuums when rows were freed. Used by: src/lib/db/cleanup.ts.
#OMNIROUTE_VACUUM_MIN_DELETED_ROWS=1000
# Explicit path to sql-wasm.wasm for the sql.js fallback adapter. Default: auto-detect.
# Used by: src/lib/db/adapters/sqljsAdapter.ts.
#OMNIROUTE_SQLJS_WASM_PATH=
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
@@ -1692,8 +1670,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
# ── TLS client (wreq-js fingerprint proxy) ──
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
# TLS_FIRST_BYTE_WATCHDOG_MS=10000 # #12656: bounds time-to-first-byte on the wreq body (0 disables)
# OPENCODE_RESPONSES_STALL_ROTATION=false # #13484 feature flag (Settings → Feature Flags wins): rotate once when a streamed Responses reply stalls before its first byte
# RESPONSES_FIRST_BYTE_TIMEOUT_MS=15000 # #13484: OpenCode Responses first-byte window, only used when the OPENCODE_RESPONSES_STALL_ROTATION flag is on (0 disables)
# ── API Bridge (/v1 proxy server) ──
# API_BRIDGE_PROXY_TIMEOUT_MS=600000 # Proxy hop timeout (default: 10min)
@@ -1824,13 +1800,6 @@ APP_LOG_TO_FILE=true
# Override only to hand-tune for a known workload.
# HEAP_PRESSURE_THRESHOLD_MB=
# Exit the process after critical resource pressure persists, so a supervisor
# (systemd Restart=always, Docker restart policy) brings back a clean process.
# Accepts 1/true/yes/on. Default: false. Used by: open-sse/utils/resourcePressure.ts.
# OMNIROUTE_PRESSURE_SELF_RESTART=false
# How long (ms) critical pressure must persist before that exit fires. Default: 120000 (2m).
# OMNIROUTE_PRESSURE_SELF_RESTART_AFTER_MS=120000
# ── CLI helpers (bin/cli/) ──
# Override UI language for CLI output. Accepts BCP-47 locale (e.g. en, pt-BR).
# Falls back to LC_ALL / LC_MESSAGES / LANG / en if unset.
@@ -1848,10 +1817,6 @@ APP_LOG_TO_FILE=true
# Per-attempt HTTP timeout for CLI → server calls (milliseconds). Default: 30000.
# OMNIROUTE_HTTP_TIMEOUT_MS=30000
# How long `omniroute serve` waits for the health endpoint before printing the
# readiness-timeout warning (milliseconds). Also --ready-timeout. Default: 60000.
# OMNIROUTE_READY_TIMEOUT_MS=60000
# Set to 1 to print retry/backoff details to stderr during CLI commands.
# OMNIROUTE_VERBOSE=0
@@ -1997,12 +1962,6 @@ APP_LOG_TO_FILE=true
# Default: 8000 (8 seconds). On timeout, a last-good 200 is served when available.
# CATALOG_BUILD_TIMEOUT_MS=8000
# Age after which a connection's synced model list stops being authoritative for routing (#12849).
# A stale (or never-timestamped) synced catalog fails open to the provider registry.
# Used by: src/lib/db/models/activeSyncedCatalog.ts
# Default: 2592000000 (30 days)
# OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS=2592000000
# ── NanoBanana (Image Generation) ──
# Polling config for async image generation jobs.
# Used by: open-sse/handlers/imageGeneration.ts
@@ -2239,20 +2198,15 @@ APP_LOG_TO_FILE=true
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
# PROXY_HEALTH_AUTO_DEACTIVATE=false
# Opt-in feature flag (default off; a dashboard DB override wins over this value): show,
# under a proxy pool in the dashboard, how many observed egress IPs served its members over
# the last 24 h and how many connections used them (read-only, computed from the proxy log,
# never used for routing). "true" (or 1, yes) enables it.
# PROXY_POOL_EGRESS_OBSERVATION=false
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags.
# OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK=false
# Rate limit maximum wait time before failing a request (ms). Default: 30000 (30s)
# Rate limit maximum wait time before failing a request (ms). Default: 15000 (15s)
# Used by: open-sse/services/rateLimitManager.ts
# RATE_LIMIT_MAX_WAIT_MS=30000
# RATE_LIMIT_MAX_WAIT_MS=15000
# Limiter-managed execution backstop (Bottleneck `expiration`): bounds a job's
# post-dispatch execution, never queue wait. Must stay ABOVE upstream

View File

@@ -511,11 +511,9 @@ 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).
# 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
# 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
# #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.

View File

@@ -10,11 +10,7 @@ name: Radar Export
on:
workflow_dispatch: # o operador pode publicar sob demanda (de qualquer ref)
push:
# `main` e a release ativa (default branch) publicam no mesmo asset estável: o
# radar-server só consome o asset, então um merge de catálogo na release que ficasse
# à espera do cron semanal deixava o feed até 7 dias atrás do README (2026-09-14: a
# linha da Together removida em d6e62ae só saiu do feed com dispatch manual).
branches: [main, "release/**"]
branches: [main] # produção: só o catálogo do main clobra o asset estável
paths:
- open-sse/config/freeModelCatalog.data.ts
- open-sse/config/freeModelCatalog.ts
@@ -23,9 +19,7 @@ on:
- scripts/release/radar-export.mjs
- .github/workflows/radar-export.yml
schedule:
# Diário 03:17 UTC — antes do `radar-feed.timer` do servidor (04:23 UTC), para o ciclo
# do dia já enxergar o export do dia; também mantém geradoEm/proveniência frescos.
- cron: "17 3 * * *"
- cron: "17 6 * * 1" # semanal (segunda 06:17 UTC): mantém geradoEm/proveniência frescos
permissions:
contents: read

View File

@@ -97,11 +97,4 @@
# 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''',
]

File diff suppressed because it is too large Load Diff

View File

@@ -56,14 +56,8 @@ explicitly:
}
```
The token can also come from the `OMNIROUTE_MANAGEMENT_API_KEY` environment
variable (the option wins when both are set). Resolution order:
`managementReadToken` option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then the
`apiKey` fallback.
Left unset, `managementReadToken` falls back to `apiKey` for backwards
compatibility, and the plugin warns once at startup that the fallback is
active. When a gateway rejects that fallback, the catalog still
compatibility. When a gateway rejects that fallback, the catalog still
publishes — but with raw model ids instead of display names, no canonical
alias dedupe, no pricing and no combos. The plugin warns once per endpoint
when this happens, naming the endpoint and the consequence, so the degraded
@@ -71,25 +65,25 @@ catalog is never a mystery.
## Options
| Key | Default | Notes |
| -------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
| `managementReadToken` | option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
| `displayName` | `"OmniRoute"` | Provider display name |
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
| Key | Default | Notes |
| -------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
| `managementReadToken` | falls back to `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
| `displayName` | `"OmniRoute"` | Provider display name |
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
## Tool calling on Gemini models

View File

@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type {
OmniRouteEnrichmentEntry,
@@ -81,12 +81,6 @@ interface DiskSnapshotV2 {
*/
const MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;
// Suffix for the temp file each write publishes via rename. Monotone per
// process: two writes for one provider (for example across a credential
// rotation) must not share a temp name. Built after the empty-models and
// size-cap guards, so only real attempts consume a value.
let snapshotWriteCounter = 0;
function trimTrailingSlashes(value: string): string {
let i = value.length;
while (i > 0 && value.charCodeAt(i - 1) === 0x2f) i -= 1;
@@ -139,7 +133,7 @@ export async function readDiskSnapshot(
if (
!parsed ||
typeof parsed.v !== "number" ||
parsed.v !== SNAPSHOT_FORMAT_VERSION ||
parsed.v < SNAPSHOT_FORMAT_VERSION ||
typeof parsed.identityFingerprint !== "string" ||
parsed.identityFingerprint !== identityFingerprint
) {
@@ -185,14 +179,8 @@ export async function readDiskSnapshot(
export async function writeDiskSnapshot(
providerId: string,
snapshot: CatalogSnapshot,
identityFingerprint: string,
logger?: { warn: (message: string) => void }
identityFingerprint: string
): Promise<void> {
// Monotone per-process suffix: two writes for one provider (for example
// across a credential rotation) must not share a temp name. Declared here
// so the catch below can clean it up; assigned after the guards so only
// real attempts consume a counter value.
let tmp = "";
try {
if (snapshot.models.length === 0) return;
const file = diskSnapshotPath(providerId);
@@ -208,33 +196,14 @@ export async function writeDiskSnapshot(
writtenAt: Date.now(),
};
let payload = JSON.stringify(envelope);
if (
Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES &&
envelope.enrichment !== undefined
) {
if (payload.length > MAX_SNAPSHOT_BYTES && envelope.enrichment !== undefined) {
delete envelope.enrichment;
payload = JSON.stringify(envelope);
}
if (Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES) {
logger?.warn(
`[omniroute-v2] snapshot for ${providerId} exceeds the size cap, skipping disk write`
);
return;
}
tmp = `${file}.${process.pid}.${snapshotWriteCounter++}`;
await writeFile(tmp, payload, { encoding: "utf8", mode: 0o600 });
await rename(tmp, file);
} catch (err) {
if (payload.length > MAX_SNAPSHOT_BYTES) return;
await writeFile(file, payload, { encoding: "utf8", mode: 0o600 });
} catch {
// Best-effort: callers already hold the in-memory entry.
logger?.warn(
`[omniroute-v2] snapshot write failed for ${providerId}: ` +
`${err instanceof Error ? err.message : String(err)}, keeping the in-memory entry`
);
try {
await unlink(tmp);
} catch {
// Ignore: the temp file may not exist (mkdir failed first).
}
}
}

View File

@@ -31,14 +31,7 @@ import { assertContext } from "./compat.js";
import { type ApiKeyOrigin, resolveApiKey, warnIfMissing } from "./credentials.js";
import { createSourceErrorReporter } from "./enrichment-report.js";
import { sanitizeToolSchemasFor } from "./gemini-language.js";
import {
MANAGEMENT_TOKEN_ENV_VAR,
PLUGIN_ID,
parsePluginOptions,
resolveManagementReadToken,
resolveTimeouts,
type PluginOptions,
} from "./options.js";
import { PLUGIN_ID, parsePluginOptions, resolveTimeouts, type PluginOptions } from "./options.js";
/**
* A fetch result that says whether it succeeded. Returning a bare `[]` on
@@ -68,7 +61,7 @@ function toResolvedOptions(parsed: PluginOptions): ResolvedOptions {
providerId: parsed.providerId,
baseURL: parsed.baseURL,
apiKey: parsed.apiKey ?? process.env.OMNIROUTE_API_KEY ?? "",
managementReadToken: resolveManagementReadToken(parsed.managementReadToken),
managementReadToken: parsed.managementReadToken,
timeoutMs: parsed.timeoutMs,
timeouts: parsed.timeouts,
logLevel: parsed.logLevel,
@@ -100,16 +93,6 @@ export default define({
resolved.logLevel = parsed.logLevel;
resolved.startupDebug = parsed.startupDebug;
log.info(`[omniroute-v2] init providerId=${X}`);
// The inference key stands in below when no management token is set, and
// gateways usually reject that stand-in with 401/403. Say so once here,
// before any fetch, instead of letting the refusal surface per endpoint.
if (resolved.managementReadToken === undefined) {
log.warn(
`[omniroute-v2] no management token configured: management endpoints (/api/*) will reuse the inference key, ` +
`which gateways usually reject with 401/403. Set "managementReadToken" in the plugin options ` +
`or export ${MANAGEMENT_TOKEN_ENV_VAR}.`
);
}
// v1 parity port: in-memory TTL + disk snapshot. The memory key
// `baseURL::sha256(creds)` isolates credential tuples (prod vs
@@ -314,7 +297,7 @@ export default define({
};
if (models.length > 0) {
state.entries.set(cacheKey, snapshot);
await writeDiskSnapshot(X, snapshot, identityFingerprint, log);
await writeDiskSnapshot(X, snapshot, identityFingerprint);
}
void optional.then(
(parts) => upgradeWithOptional(snapshot, parts),
@@ -361,7 +344,7 @@ export default define({
if (unchanged) return;
state.entries.set(cacheKey, upgraded);
if (upgraded.models.length > 0) {
await writeDiskSnapshot(X, upgraded, identityFingerprint, log);
await writeDiskSnapshot(X, upgraded, identityFingerprint);
}
// Reload only when the optional tier actually moved: the catalog
// fingerprint covers ids alone, so without this the host would rebuild

View File

@@ -61,21 +61,6 @@ const pluginOptionsSchema = z
export type PluginOptions = z.infer<typeof pluginOptionsSchema>;
/** Environment source for the management token (option wins over this). */
export const MANAGEMENT_TOKEN_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
/**
* Resolve the management token: a non-empty option wins, then a non-empty
* environment value, else absent. Empty counts as absent on both inputs, the
* same rule the inference key follows; no trimming, the token is opaque.
*/
export function resolveManagementReadToken(optionValue: string | undefined): string | undefined {
if (optionValue !== undefined && optionValue.length > 0) return optionValue;
const fromEnv = process.env[MANAGEMENT_TOKEN_ENV_VAR];
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
return undefined;
}
/** Per-endpoint timeout defaults (v1 parity). `timeoutMs` is the global fallback. */
export const DEFAULT_TIMEOUT_MS = 10_000 as const;
/** Auto-combos keep the v1 5s budget; the field is resolved now for the P3 port. */

View File

@@ -1,251 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import {
diskSnapshotPath,
readDiskSnapshot,
writeDiskSnapshot,
type CatalogSnapshot,
} from "../src/cache.js";
function isolateDisk(): { dir: string; restore: () => void } {
const dir = mkdtempSync(join(tmpdir(), "omniroute-disk-atomic-"));
const prev = process.env.OPENCODE_DATA_DIR;
process.env.OPENCODE_DATA_DIR = dir;
return {
dir,
restore: () => {
if (prev === undefined) delete process.env.OPENCODE_DATA_DIR;
else process.env.OPENCODE_DATA_DIR = prev;
},
};
}
function makeSnapshot(models: string[] = ["m-a"]): CatalogSnapshot {
return {
models: models.map((id) => ({ id })),
combos: [],
autoCombos: [],
providers: [],
fetchedAt: Date.now(),
} as unknown as CatalogSnapshot;
}
function makeLogger() {
const messages: string[] = [];
return {
messages,
logger: { warn: (message: string) => void messages.push(message) },
};
}
// Entries next to the destination other than the destination itself: any
// leftover temp file after a successful write shows up here.
function strayEntries(file: string): string[] {
let entries: string[];
try {
entries = readdirSync(dirname(file));
} catch {
return [];
}
return entries.filter((entry) => entry !== file.split("/").pop());
}
// The writer names its temp file `${file}.${pid}.${counter}` with a
// module-monotone counter starting at 0, built after the empty-models and
// size-cap guards (an over-cap call consumes no counter value). Tests in this
// file run sequentially in one process, so the attempt table below predicts
// every temp path exactly:
// over-cap: no counter use | failed write A: 0, failed write B: 1 |
// interrupted overwrite A: 2, interrupted overwrite B: 3 | mkdir failure: 4 |
// truncated read: 5 | success: 6 | permissions: 7 | round-trip: 8, 9.
function predictedTmp(file: string, counter: number): string {
return `${file}.${process.pid}.${counter}`;
}
describe("disk snapshot atomic write, strict version, traced give-ups", () => {
it("ignores a newer snapshot version without throwing", async () => {
const disk = isolateDisk();
try {
const file = diskSnapshotPath("t1-future");
mkdirSync(dirname(file), { recursive: true });
// A writer from the future persists version 3; this reader must
// treat it as "no snapshot" instead of trusting unknown data.
writeFileSync(
file,
JSON.stringify({
v: 3,
identityFingerprint: "fp-1",
models: [{ id: "m-future" }],
combos: [],
writtenAt: Date.now(),
})
);
const back = await readDiskSnapshot("t1-future", "fp-1");
assert.equal(back, undefined);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("traces an over-cap write and leaves no destination behind", async () => {
const disk = isolateDisk();
try {
const { messages, logger } = makeLogger();
const bigId = `huge-${"x".repeat(33 * 1024 * 1024)}`;
await writeDiskSnapshot("t2-cap", makeSnapshot([bigId]), "fp-1", logger);
const file = diskSnapshotPath("t2-cap");
assert.equal(existsSync(file), false);
assert.deepEqual(strayEntries(file), []);
assert.match(messages.join("\n"), /exceeds|too large|size cap/i);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a failed write leaves no destination behind and is traced", async () => {
const disk = isolateDisk();
const file = diskSnapshotPath("t3a-fail");
const blocker = predictedTmp(file, 1);
try {
const { messages, logger } = makeLogger();
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-before"]), "fp-1", logger);
// Plant a directory at the next temp path: the write fails with
// EISDIR before any rename, deterministically, on every platform.
mkdirSync(dirname(file), { recursive: true });
mkdirSync(blocker, { recursive: true });
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-after"]), "fp-1", logger);
assert.equal(existsSync(file), true);
const back = await readDiskSnapshot("t3a-fail", "fp-1");
assert.deepEqual(
(back?.models ?? []).map((entry) => entry.id),
["m-before"]
);
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
} finally {
rmSync(blocker, { recursive: true, force: true });
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("an interrupted overwrite keeps the previous snapshot", async () => {
const disk = isolateDisk();
const file = diskSnapshotPath("t3b-keep");
const blocker = predictedTmp(file, 3);
try {
const { messages, logger } = makeLogger();
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-before"]), "fp-1", logger);
const before = readFileSync(file, "utf8");
mkdirSync(blocker, { recursive: true });
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-after"]), "fp-1", logger);
assert.equal(readFileSync(file, "utf8"), before);
const back = await readDiskSnapshot("t3b-keep", "fp-1");
assert.deepEqual(
(back?.models ?? []).map((entry) => entry.id),
["m-before"]
);
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
} finally {
rmSync(blocker, { recursive: true, force: true });
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a mkdir failure is traced and writes nothing", async () => {
const disk = isolateDisk();
try {
const { messages, logger } = makeLogger();
// A file planted at the plugins path makes mkdir fail
// deterministically (EEXIST on mkdir, ENOTDIR on direct writeFile).
writeFileSync(join(disk.dir, "plugins"), "blocker");
await writeDiskSnapshot("t3b-bis", makeSnapshot(["m-a"]), "fp-1", logger);
assert.equal(existsSync(diskSnapshotPath("t3b-bis")), false);
assert.match(messages.join("\n"), /failed|EEXIST|ENOTDIR|error/i);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a truncated file reads as no snapshot without throwing", async () => {
const disk = isolateDisk();
try {
const { logger } = makeLogger();
await writeDiskSnapshot("t4-truncated", makeSnapshot(["m-a"]), "fp-1", logger);
const file = diskSnapshotPath("t4-truncated");
const full = readFileSync(file, "utf8");
writeFileSync(file, full.slice(0, Math.floor(full.length / 2)));
const back = await readDiskSnapshot("t4-truncated", "fp-1");
assert.equal(back, undefined);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a successful write leaves no entry but the destination", async () => {
const disk = isolateDisk();
try {
await writeDiskSnapshot("t5-clean", makeSnapshot(["m-a"]), "fp-1");
const file = diskSnapshotPath("t5-clean");
assert.deepEqual(strayEntries(file), []);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("the replaced snapshot stays owner-only", async (t) => {
if (process.platform === "win32") {
t.skip("file mode semantics are POSIX-only");
return;
}
const disk = isolateDisk();
try {
await writeDiskSnapshot("t6-mode", makeSnapshot(["m-a"]), "fp-1");
const file = diskSnapshotPath("t6-mode");
assert.equal((statSync(file).mode & 0o077) === 0, true);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("round-trips a valid snapshot with and without a logger", async () => {
const disk = isolateDisk();
try {
const { logger } = makeLogger();
const snapshot = makeSnapshot(["m-a"]);
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1");
const plain = await readDiskSnapshot("t7-roundtrip", "fp-1");
assert.deepEqual(
(plain?.models ?? []).map((entry) => entry.id),
["m-a"]
);
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1", logger);
const logged = await readDiskSnapshot("t7-roundtrip", "fp-1", logger);
assert.deepEqual(
(logged?.models ?? []).map((entry) => entry.id),
["m-a"]
);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
});

View File

@@ -1,371 +0,0 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import plugin from "../src/index.js";
import { publishCatalog } from "../src/catalog.js";
import type { CatalogDraft } from "@opencode-ai/plugin/v2/promise";
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types";
const MODELS_URL = "https://gw.example.com/v1/models";
const COMBOS_URL = "https://gw.example.com/api/combos";
const PRICING_MODELS_URL = "https://gw.example.com/api/pricing/models";
const MGMT_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
const INFERENCE_ENV_VAR = "OMNIROUTE_API_KEY";
function okJson(body: unknown) {
return { ok: true, status: 200, statusText: "OK", json: async () => body };
}
interface Harness {
seen: Map<string, string>;
warns: string[];
restore: () => void;
}
function installHarness(combos: unknown[]): Harness {
const seen = new Map<string, string>();
const warns: string[] = [];
const origFetch = globalThis.fetch;
const origWarn = console.warn;
const origLog = console.log;
const origError = console.error;
console.warn = (...args: unknown[]) => {
warns.push(String(args[0]));
};
console.log = () => {};
console.error = (...args: unknown[]) => {
warns.push(String(args[0]));
};
globalThis.fetch = (async (url: unknown, init?: { headers?: Record<string, string> }) => {
const href = String(url);
seen.set(href, String(init?.headers?.Authorization ?? ""));
if (href.includes("/api/combos/auto")) return okJson({ combos: [] });
if (href.includes("/api/pricing/models")) {
return okJson({
providers: {
demo: {
id: "demo",
name: "Demo",
models: [{ id: "team-combo", name: "Team Combo" }],
},
},
});
}
if (href.includes("/api/pricing")) return okJson({});
if (href.includes("/api/free-tier/summary")) return okJson({ perModel: [] });
if (href.includes("/api/combos")) return okJson({ combos });
return okJson({ data: [{ id: "m1" }] });
}) as typeof fetch;
return {
seen,
warns,
restore() {
globalThis.fetch = origFetch;
console.warn = origWarn;
console.log = origLog;
console.error = origError;
},
};
}
async function withIsolatedEnv<T>(
mgmt: string | undefined,
inference: string | undefined,
fn: () => Promise<T>
): Promise<T> {
const prevMgmt = process.env[MGMT_ENV_VAR];
const prevInference = process.env[INFERENCE_ENV_VAR];
// Like tests/management-token.test.ts:176-180: a fresh OPENCODE_DATA_DIR
// per case keeps the real disk snapshot out of the run, so a filtered 'it'
// never gets a warm snapshot served without fetch.
const prevDataDir = process.env.OPENCODE_DATA_DIR;
process.env.OPENCODE_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-mgmt-env-"));
if (mgmt === undefined) delete process.env[MGMT_ENV_VAR];
else process.env[MGMT_ENV_VAR] = mgmt;
if (inference === undefined) delete process.env[INFERENCE_ENV_VAR];
else process.env[INFERENCE_ENV_VAR] = inference;
try {
return await fn();
} finally {
if (prevDataDir === undefined) delete process.env.OPENCODE_DATA_DIR;
else process.env.OPENCODE_DATA_DIR = prevDataDir;
if (prevMgmt === undefined) delete process.env[MGMT_ENV_VAR];
else process.env[MGMT_ENV_VAR] = prevMgmt;
if (prevInference === undefined) delete process.env[INFERENCE_ENV_VAR];
else process.env[INFERENCE_ENV_VAR] = prevInference;
}
}
function setupHarness(options: Record<string, unknown>) {
const catalogCallbacks: Array<(draft: unknown) => Promise<void>> = [];
const ctx = {
options,
catalog: {
transform: (cb: (draft: unknown) => Promise<void>) => {
catalogCallbacks.push(cb);
return Promise.resolve({ dispose: async () => {} });
},
},
integration: {
transform: () => Promise.resolve({ dispose: async () => {} }),
},
};
return { catalogCallbacks, ctx };
}
function stubDraft() {
const published = new Map<string, Record<string, unknown>>();
const draft = {
provider: { update: (_id: string, fn: (p: Record<string, unknown>) => void) => fn({}) },
model: {
update: (pid: string, mid: string, fn: (m: Record<string, unknown>) => void) => {
const key = pid + "/" + mid;
let entry = published.get(key);
if (entry === undefined) {
entry = { id: mid, providerID: pid };
published.set(key, entry);
}
fn(entry);
},
},
};
return { draft, published };
}
function fallbackWarns(warns: string[]): string[] {
return warns.filter((w) => w.includes("managementReadToken"));
}
async function runSetup(ctx: unknown): Promise<void> {
await (plugin as unknown as { setup: (ctx: unknown) => Promise<void> }).setup(ctx);
}
describe("plugin-v2 management token environment source", () => {
it("uses the managementReadToken option for /api/* while models keep apiKey", async () => {
await withIsolatedEnv(undefined, undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("reads the management token from the environment when the option is absent", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("prefers the option over the environment", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
} finally {
h.restore();
}
});
});
it("falls back to the inference key with a single early warning when neither is set", async () => {
await withIsolatedEnv(undefined, undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
});
await runSetup(ctx);
const atSetup = fallbackWarns(h.warns);
assert.equal(
atSetup.length,
1,
`expected exactly one early fallback warning, got: ${JSON.stringify(h.warns)}`
);
assert.match(atSetup[0] ?? "", /managementReadToken/);
assert.match(atSetup[0] ?? "", new RegExp(MGMT_ENV_VAR));
assert.ok(!(atSetup[0] ?? "").includes("chat-key"), "warning must not leak the key");
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
assert.equal(
fallbackWarns(h.warns).length,
1,
"the fallback warning stays a single setup-time notice"
);
} finally {
h.restore();
}
});
});
it("treats an empty option as absent so the environment wins", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
} finally {
h.restore();
}
});
});
it("treats an empty environment value as absent so the option wins", async () => {
await withIsolatedEnv("", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
} finally {
h.restore();
}
});
});
it("falls back with a warning when both the option and the environment are empty", async () => {
await withIsolatedEnv("", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "",
});
await runSetup(ctx);
assert.equal(fallbackWarns(h.warns).length, 1);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("enriches the catalog from the environment token alone", async () => {
const providers = new Map<string, ProviderV2Info>();
const models = new Map<string, ModelV2Info>();
const draft = {
provider: {
list: () => [],
get: (id: string) => providers.get(id) as never,
update: (id: string, fn: (p: ProviderV2Info) => void) => {
const p = (providers.get(id) ?? { id }) as ProviderV2Info;
fn(p);
providers.set(id, p);
},
remove: () => {},
},
model: {
get: () => undefined,
update: (pid: string, mid: string, fn: (m: ModelV2Info) => void) => {
const k = pid + "/" + mid;
const m = (models.get(k) ?? { id: mid, providerID: pid }) as ModelV2Info;
fn(m);
models.set(k, m);
},
remove: () => {},
default: { get: () => undefined, set: () => {} },
},
} as unknown as CatalogDraft;
let seenCombos = "";
let seenPricing = "";
const res = await withIsolatedEnv("mgmt-env-token", undefined, async () =>
publishCatalog(
draft,
{
providerId: "omniroute",
baseURL: "https://gw.example.com",
apiKey: "chat-key",
managementReadToken: process.env[MGMT_ENV_VAR],
timeoutMs: 1000,
modelCacheTtlMs: 300000,
usableOnly: false,
},
{
fetcher: async () => [{ id: "m1" }],
combosFetcher: async (_base, token) => {
seenCombos = token;
return [{ id: "team-combo", models: [{ kind: "model", model: "m1" }] }];
},
enrichmentFetcher: async (_base, token) => {
seenPricing = token;
// The process env is the source under test: the resolver output
// flows in through the option above, so report success only when
// the flow under test actually carried it.
if (token !== "mgmt-env-token") return new Map();
return new Map([["team-combo", { name: "Team Combo" }]]);
},
}
)
);
assert.deepEqual(res, { models: 1, combos: 1, autoCombos: 0 });
assert.equal(seenCombos, "mgmt-env-token");
assert.equal(seenPricing, "mgmt-env-token");
const entry = models.get("omniroute/team-combo");
assert.ok(entry, "expected the combo entry in the published catalog");
assert.equal(entry?.name, "Team Combo");
});
});

View File

@@ -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, 356 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 (173 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 |

View File

@@ -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 1595% 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 → 356 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 1595% tokens (~89% avg) — never hit limits. 356 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 **444 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.10B in the first month with signup credits, from 34 documented recurring pool keys covering 444 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)**.
>
@@ -133,7 +133,7 @@
</div>
<div align="center">
<b>🌐 In 66 languages</b>
<b>🌐 In 59 languages</b>
<br/><br/>
<a href="README.md"><img src="docs/assets/flags/us.svg" width="30" alt="English (en)" title="English (en)"></a>
<a href="docs/i18n/pt-BR/README.md"><img src="docs/assets/flags/br.svg" width="30" alt="Português — Brasil (pt-BR)" title="Português — Brasil (pt-BR)"></a>
@@ -194,13 +194,6 @@
<a href="docs/i18n/si/README.md"><img src="docs/assets/flags/lk.svg" width="30" alt="සිංහල (si)" title="සිංහල (si)"></a>
<a href="docs/i18n/my/README.md"><img src="docs/assets/flags/mm.svg" width="30" alt="မြန်မာ (my)" title="မြန်မာ (my)"></a>
<a href="docs/i18n/km/README.md"><img src="docs/assets/flags/kh.svg" width="30" alt="ខ្មែរ (km)" title="ខ្មែរ (km)"></a>
<a href="docs/i18n/ha/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Hausa (ha)" title="Hausa (ha)"></a>
<a href="docs/i18n/yo/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Yorùbá (yo)" title="Yorùbá (yo)"></a>
<a href="docs/i18n/ig/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Igbo (ig)" title="Igbo (ig)"></a>
<a href="docs/i18n/am/README.md"><img src="docs/assets/flags/et.svg" width="30" alt="አማርኛ (am)" title="አማርኛ (am)"></a>
<a href="docs/i18n/uz/README.md"><img src="docs/assets/flags/uz.svg" width="30" alt="Oʻzbekcha (uz)" title="Oʻzbekcha (uz)"></a>
<a href="docs/i18n/ka/README.md"><img src="docs/assets/flags/ge.svg" width="30" alt="ქართული (ka)" title="ქართული (ka)"></a>
<a href="docs/i18n/hy/README.md"><img src="docs/assets/flags/am.svg" width="30" alt="Հայերեն (hy)" title="Հայերեն (hy)"></a>
</div>
<br/>
@@ -233,7 +226,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 356 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 356 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."/>
<br/>
<br/>
@@ -486,7 +479,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: 356 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 &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -544,7 +537,7 @@ Pix copia-e-cola:
The main free-tier headline remains **~1.47B tokens/month** from the documented,
pool-deduplicated catalog above. Temporary provider signup credits can separately lift the first
month to **~2.07B**. Radar is an optional, signed catalog overlay for people who want fresher
month to **~2.10B**. Radar is an optional, signed catalog overlay for people who want fresher
free-model availability between OmniRoute releases; the community catalog and every existing free
feature remain free.
@@ -672,7 +665,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 **444 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).
<div align="center">
@@ -1268,7 +1261,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>&gt;=22.22.2 &lt;23 || &gt;=24.0.0 &lt;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, 173 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 +1324,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 / 444 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>

View File

@@ -13,7 +13,6 @@ const OMNIROUTE_ENV_VARS = [
"OMNIROUTE_API_KEY",
"OMNIROUTE_BASE_URL",
"OMNIROUTE_HTTP_TIMEOUT_MS",
"OMNIROUTE_READY_TIMEOUT_MS",
];
const ENV_DEFAULTS = {

View File

@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { platform, totalmem } from "node:os";
import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer, resolveReadyTimeoutMs } from "../utils/pid.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import {
ServerSupervisor,
detectMitmCrash,
@@ -58,11 +58,6 @@ export function registerServe(program) {
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
.option("--tray", t("serve.tray") || "Start in the system tray (desktop only)")
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
.option(
"--ready-timeout <ms>",
t("serve.ready_timeout") ||
"Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)"
)
.option(
"--tls-cert <path>",
t("serve.tls_cert") ||
@@ -457,8 +452,7 @@ async function runWithSupervisor(
});
if (!showLog) {
const readyTimeoutMs = resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout });
waitForServer(dashboardPort, readyTimeoutMs).then(async (up) => {
waitForServer(dashboardPort, 60000).then(async (up) => {
if (up) {
if (useTray) {
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
@@ -495,15 +489,10 @@ async function runWithSupervisor(
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
// plus whatever stdout/stderr the child buffered instead of going silent.
export function reportReadinessTimeout(dashboardPort, supervisor) {
const readyTimeoutMs = resolveReadyTimeoutMs();
const seconds = Math.round(readyTimeoutMs / 1000);
console.error(
`\n\x1b[33m⚠ Server did not respond within ${seconds}s.\x1b[0m It may still be starting, or may` +
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
` have failed silently.`
);
console.error(
` Tip: set OMNIROUTE_READY_TIMEOUT_MS=${readyTimeoutMs * 2} or --ready-timeout ${readyTimeoutMs * 2} for slower cold starts.`
);
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);

File diff suppressed because it is too large Load Diff

View File

@@ -256,7 +256,6 @@
"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)"
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -254,7 +254,6 @@
"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"
},

View File

@@ -254,7 +254,6 @@
"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"
},

View File

@@ -66,30 +66,13 @@ export function sleep(ms) {
// #2460: Default raised from 15s to 60s so Windows users (slower Next.js
// cold start due to filesystem watchers, antivirus, etc.) get a working
// "server ready" signal instead of a phantom timeout while the server is
// still booting. #13369: Made configurable via OMNIROUTE_READY_TIMEOUT_MS
// so operators on slow cold starts (e.g. 6+ min Windows boots) can raise
// the budget instead of hitting the warning on every start.
//
// TCP fallback marks the server as ready when the port
// still booting. TCP fallback marks the server as ready when the port
// has been listening for >= 3s consecutively AND the health route is
// actively rejecting/resetting connections fast (route not mounted yet,
// but the HTTP server is clearly alive and responsive) — never for a
// socket that merely accepts TCP and then hangs without ever completing
// a single request (#6800: that's a still-booting/CPU-bound process, not
// a "route not mounted" gap, and must NOT be reported as ready).
const DEFAULT_READY_TIMEOUT_MS = 60_000;
export function resolveReadyTimeoutMs(overrides = {}) {
if (typeof overrides.timeoutMs === "number" && overrides.timeoutMs > 0) {
return overrides.timeoutMs;
}
const envValue = Number.parseInt(
process.env.OMNIROUTE_READY_TIMEOUT_MS || "",
10
);
return Number.isFinite(envValue) && envValue > 0 ? envValue : DEFAULT_READY_TIMEOUT_MS;
}
export async function waitForServer(port, timeout = 60000) {
const start = Date.now();
let tcpListeningSince = null;

View File

@@ -1 +0,0 @@
- **feat(proxies):** proxy pools and opencode's per-account rotation stop re-serving a proxy that just failed (refused TCP probe, or a 429 through it) for a period that doubles on each repeat up to a cap, without writing any proxy status; with every candidate set aside the choice is unchanged. Opt-in via the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: selection unchanged) ([#13578](https://github.com/diegosouzapw/OmniRoute/pull/13578)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **feat(proxy-logs):** proxy log rows keep the HTTP status the provider actually returned (`upstream_status`, null when no response arrived), so a throttled egress IP (429), a refused one (403) and a provider outage (500) are no longer the same "error" line, and a 429 generated locally is no longer mistaken for one from the provider ([#13580](https://github.com/diegosouzapw/OmniRoute/pull/13580)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **feat(proxies):** the proxy pool editor shows, for the last 24 h, how many distinct egress IPs actually served the pool's members, how many connections went through them and the most seen behind one IP, read from the proxy log through a separate route so it can never break the pool screen; opt-in with the `PROXY_POOL_EGRESS_OBSERVATION` feature flag (default off) ([#13581](https://github.com/diegosouzapw/OmniRoute/pull/13581)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **feat(proxies):** a proxy pool stops re-serving a member the provider just refused through it and tries another member instead, reusing the existing skip cooldown; a later success through the member clears it. Opt-in with the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: pool selection unchanged) ([#13602](https://github.com/diegosouzapw/OmniRoute/pull/13602)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **feat(i18n):** 7 new locales — Hausa (`ha`), Yoruba (`yo`), Igbo (`ig`), Amharic (`am`), Uzbek (`uz`), Georgian (`ka`), Armenian (`hy`) — across the dashboard, docs mirrors, CLI, README and the site (66 locales, the full planned expansion from 43). (#13727)

View File

@@ -1 +0,0 @@
- **fix(api):** `/v1/files` and `/v1/batches` now enforce one ownership rule everywhere — a dashboard session is the instance operator, an API key acts on its own records only, and a record with no owner is denied to every non-session caller. Previously a file or batch whose `api_key_id` was null (a dashboard-session or anonymous upload, or a batch artifact inheriting one) could be read, downloaded, deleted, cancelled or used as a batch input by any other key or by an unauthenticated caller (GHSA-2jm2-mpx8-6523), and `GET /v1/files` / `GET /v1/batches` returned every tenant's records to an anonymous or invalid-bearer caller under the default `REQUIRE_API_KEY=false` (GHSA-m3hp-hq9g-fpmv) — both lists now fail closed with a `401`, and only a dashboard session without a key reads the whole instance. The same shared rule lets the dashboard cancel any batch, not just unowned ones. Behaviour change: the anonymous upload → batch → download flow no longer works without an API key, since a null owner cannot be attributed. Subsumes [#13683](https://github.com/diegosouzapw/OmniRoute/pull/13683) — thanks @hartmark

View File

@@ -1 +0,0 @@
- **fix(auth):** closed the JWT_SECRET bootstrap chain (GHSA-7pq4-8pvv-rx7r). The fresh-install bootstrap gate in `isAuthRequired()` now decides "loopback" from the trusted peer — the token-stamped real TCP peer the custom server writes, the pipeline's own locality verdict, or a real socket — and never from the client-controlled `Host` / `nextUrl.hostname` whenever a stamping server is in front (every supported runtime), so `Host: localhost` from a remote address no longer opens the window; the anonymous first-password write (`POST /api/settings/require-login`) is under the same loopback constraint instead of being open to every network peer, and `managementPolicy` hands its `peerContext` verdict down explicitly. `/api/settings/obsidian` (incl. `/webdav`, which mints reusable WebDAV Basic credentials for a caller-chosen root served before Next.js) joined `ALWAYS_PROTECTED_API_PATHS`, and `enableObsidianVaultSync()` refuses a vault that is, sits inside, or contains the data directory (realpath-resolved), so the WebDAV file service can no longer be pointed at `server.env` / `storage.sqlite`

View File

@@ -1,4 +0,0 @@
- **fix(sse):** the provider execution pipeline keeps the upstream error `code`/`type` again — both were lost when the non-streaming leg was extracted, so a config-class `422` (Antigravity missing project) degraded into a generic account cooldown instead of reaching the gate that recognises it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
- **fix(db):** a legacy `call_logs` table no longer aborts startup — the provider-stats index is created after the column healing runs, not before it, so an install predating `request_type` opens instead of failing with `no such column` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
- **fix(catalog):** a malformed row in the operator-writable custom-models blob no longer kills every `auto/*` pool ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
- **fix(tests):** realign the guards that the same day's merges left asserting the old behavior — the provider-count total after GreenPT and EURouter, the GLM stream buffer slot that `#12925` turned into a declared parameter, and the injection scan bound that `#13104` reshaped from a head-only window into head-plus-tail ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **fix(models):** a cold `GET /v1/models` on a large deployment no longer blocks the event loop for about a second at a time or overruns the 8s cold-build bound: since #12046 the built-in `auto/*` combos resolved catalog metadata for every target of every combo without memoizing or yielding, and they all draw on the same candidate pool, so 720 synced models took the build from ~4s to ~18s. Each distinct target is now resolved once per build, with a yield between misses ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **fix(combo):** a `quota-share` combo whose steps carry no weight now rotates across its targets again instead of sending every request to the first one — the resolver turns an unset weight into 0 and #10881 made 0 mean "disabled", so an all-unweighted combo had no quanta and fell back to definition order; an explicit 0 still disables a target next to weighted siblings ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- fix(nvidia): fail open when a synced model catalog goes stale instead of gating forever (#12849)

View File

@@ -1 +0,0 @@
- **fix(api):** `DELETE /v1/batches/delete-completed` now sweeps only the calling API key's own completed batches (batches with no owner stay out of a key-scoped sweep on purpose), with an explicit instance-wide mode reserved for authenticated dashboard sessions, a 401 for a presented key that is unknown, revoked, deactivated, banned or expired (never falling through to the session branch), audit logging of every sweep, a sanitized 500 on failure, an owner-scoped file half (a key-scoped sweep never nulls a file another tenant owns) and an atomic sweep — chunked in 200-batch transactions in instance mode — so a mid-way error never leaves a batch pointing at a nulled file (GHSA-wvxc-jp3v-5mg5) ([#12969](https://github.com/diegosouzapw/OmniRoute/pull/12969))

View File

@@ -1 +0,0 @@
- **fix(sse):** fail over once to a sibling connection on stream early EOF (the original `STREAM_EARLY_EOF` 502 is kept when no sibling can serve the request), gated behind `STREAM_EARLY_EOF_SIBLING_FAILOVER_ENABLED` (default off) ([#13153](https://github.com/diegosouzapw/OmniRoute/pull/13153)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(combos):** stop dropping live keys and persisting dead ones ([#13217](https://github.com/diegosouzapw/OmniRoute/pull/13217)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(db):** the WAL checkpoint busy counter reported by `/api/monitoring/health` now survives restarts — busy checkpoints are counted in memory and persisted from the next clean maintenance tick or at shutdown, never with a write while the database is contended ([#13218](https://github.com/diegosouzapw/OmniRoute/pull/13218)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **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

View File

@@ -1 +0,0 @@
- **fix(combos):** testing a combo aborts in-flight probes when the client disconnects instead of probing on after the dashboard navigates away ([#13279](https://github.com/diegosouzapw/OmniRoute/pull/13279)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(quota):** in-process routing and quota caches (quality tracker, saturation and rate-limit header caches, quota-fetcher cache, learned rate limits, account buckets) are now size-bounded through one shared `boundedMap` — caps sit far above normal deployments, evictions are logged once per minute per cache instead of per entry, and state whose loss would change routing (live saturated quota buckets, evaluator quality scores) is never evicted ([#13280](https://github.com/diegosouzapw/OmniRoute/pull/13280)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(call-logs):** call-log error types are now a versioned vocabulary (`ERROR_TYPE_CONTRACT v1`) with explicit `unknown` instead of ambiguous `null`, and free-text history reads back as `unclassified` ([#13281](https://github.com/diegosouzapw/OmniRoute/pull/13281)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(build):** the production build no longer breaks when a client component reaches a server-only module, and the client-bundle guard now discovers server-only modules instead of matching a fixed list ([#13436](https://github.com/diegosouzapw/OmniRoute/pull/13436)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(models):** return a retryable 503 with Retry-After instead of a 500 when the first catalog build outlasts its time bound ([#13438](https://github.com/diegosouzapw/OmniRoute/pull/13438)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(combo):** new opt-in flag `PROTECTED_PRIORITY_INFRA_502_ENABLED` (default off): when a priority target marked fallback-only-on-quota-exhaustion stops the combo because its provider circuit breaker is open or a predictive latency check rejected it — causes that are provably not quota — the response is 502 instead of a quota-looking 503; lockout, cooldown, unavailable, exhaustion, credential-gate and concurrency-cap stops keep 503 ([#13439](https://github.com/diegosouzapw/OmniRoute/pull/13439)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(resilience):** non-TPD daily-quota cooldowns honor the provider node's configured daily-reset clock (timezone + hour) instead of server midnight, on single-model and combo (priority and round-robin) paths; timezone edits apply without a restart ([#13440](https://github.com/diegosouzapw/OmniRoute/pull/13440)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(call-logs):** the call-log write point validates `error_type` against the versioned vocabulary with a Zod schema and stores `unknown` for any value outside it, so a classifier family that drifts from `ERROR_TYPE_CONTRACT` can never persist free text ([#13441](https://github.com/diegosouzapw/OmniRoute/pull/13441)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(providers):** Muse Spark 1.3 works on OpenCode Zen, OpenCode and OpenCode Go instead of failing with a 500, and gets its real 1M context window ([#13471](https://github.com/diegosouzapw/OmniRoute/pull/13471)) — thanks @maxmad64bis (with thanks to @bacnh85, @shermzy and @atakhadiviom for #12675, #12973 and #13111)

View File

@@ -1 +0,0 @@
- **fix(opencode):** opt-in `OPENCODE_RESPONSES_STALL_ROTATION` flag (default off): a streamed Responses reply that sends headers and then nothing is cut after `RESPONSES_FIRST_BYTE_TIMEOUT_MS` (default 15 s) instead of waiting for the stream readiness timeout — the account is cooled down and the request rotates to the next account once (proxied or proxy-less), a second stall fails fast; with the flag off nothing changes ([#13484](https://github.com/diegosouzapw/OmniRoute/pull/13484)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(sse):** opt-in `OPENCODE_USER_BLOCKED_ROTATION` flag (default off): an opencode 403 or 451 carrying a `user_blocked` refusal cools the refused account down and fails over to the next account at most once per request, cancelling the abandoned response body; with the flag off the refusal is returned unchanged ([#13498](https://github.com/diegosouzapw/OmniRoute/pull/13498)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(proxies):** a subscription refresh, a bulk re-import or an API update that omits the status no longer turns a disabled proxy back on, and a refresh no longer rewrites a manual proxy that shares a subscription node's address ([#13577](https://github.com/diegosouzapw/OmniRoute/pull/13577)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(api):** a partial update no longer resets the fields the client did not send: renaming a disabled reasoning routing rule keeps it disabled with its priority, description and tags, renaming a playground preset keeps its params, and renaming or re-importing a proxy keeps its address family ([#13582](https://github.com/diegosouzapw/OmniRoute/pull/13582)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **fix(proxy):** proxy credentials holding a literal `%` (e.g. `pa%ss`) no longer break the proxy — HTTP(S) proxies now receive a correctly built `Proxy-Authorization` header instead of undici throwing `URIError`, SOCKS5 proxies get the raw credential, and the proxy registry, subscription import and legacy settings parsers keep the value instead of dropping the entry; correctly percent-encoded credentials decode exactly as before ([#13605](https://github.com/diegosouzapw/OmniRoute/pull/13605)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(connection-cooldown):** skip connection cooldown for locally rejected token-budget 429s so a per-key limit never cools a healthy connection ([#13606](https://github.com/diegosouzapw/OmniRoute/pull/13606)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(opencode-plugin-v2):** write the catalog snapshot to a temp file and rename it into place, ignore newer snapshot versions, and warn when a write is skipped or fails ([#13607](https://github.com/diegosouzapw/OmniRoute/pull/13607)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(proxy-health):** a target-refused probe (401/403/429) can reset the consecutive-failure streak instead of staying neutral, behind the opt-in `PROXY_HEALTH_BLOCKED_RESETS_STREAK` feature flag (default off: refusals keep the #10654 neutral policy); a relayed 5xx stays inconclusive and a refusal never removes or disables a proxy ([#13608](https://github.com/diegosouzapw/OmniRoute/pull/13608)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **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

View File

@@ -1 +0,0 @@
- **fix(proxies):** pool validation no longer rewrites proxies set to inactive or dead; only active and error statuses are updated ([#13612](https://github.com/diegosouzapw/OmniRoute/pull/13612)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(opencode):** the v2 plugin reads the management token from OMNIROUTE_MANAGEMENT_API_KEY (plugin option wins) and warns once at startup when management calls fall back to the inference key ([#13613](https://github.com/diegosouzapw/OmniRoute/pull/13613)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(routing):** a failed stale-pin (LKGP) clear on the combo fallback path now logs the combo and execution key while staying non-blocking, and a new opt-in `npm run check:routing-error-guard` script (not wired into CI) flags new swallowed catches and unanchored fire-and-forget async on routing paths, with frozen entries keyed by file and catch body instead of line numbers ([#13614](https://github.com/diegosouzapw/OmniRoute/pull/13614)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(opencode):** opt-in `OPENCODE_TRANSIENT_FAILOVER_BACKOFF` flag (default off): once two consecutive opencode accounts fail with a transient upstream error, the rotation pauses before the next account (1.5 s doubling, capped at 6 s per pause and 10 s per request), releases the failed response body first and stops dispatching if the client disconnects during the pause; with the flag off failover stays immediate ([#13615](https://github.com/diegosouzapw/OmniRoute/pull/13615)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(stream-recovery):** opt-in `STREAM_RECOVERY_TOOLCALL_ORDER_FIX` (default off) makes mid-stream continuation tool-call safe — a cut stream is never resumed once a tool call was emitted, whether still in flight or already finished with `finish_reason: "tool_calls"` — and closes after one empty continuation instead of spending the whole budget ([#13633](https://github.com/diegosouzapw/OmniRoute/pull/13633)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(ci):** clear the `release/v3.8.51` base-reds on the PR fast path. The provider pipeline keeps the upstream error code and type again, so an Antigravity missing-project 422 stays fail-closed. The glued-prefix `sk-` credential pattern scans error text in linear time instead of quadratic. The provider detail page no longer bundles `node:fs`. `/v1/models` stops listing custom Jina models twice. The `models`/`providers` import cycle is gone, and `opencode-plugin-v2` passes the pack policy. Stale guards, fixtures, locale keys and docs counts are aligned with their merged changes ([#13635](https://github.com/diegosouzapw/OmniRoute/pull/13635))

View File

@@ -1 +0,0 @@
- **fix(db):** search stats and analytics no longer surface "ghost" rows — a NULL/`-` provider or a keyed search provider whose connection was deleted — while keyless providers (`duckduckgo-free`, `searxng-search`, anonymous `context7`) and credential-fallback providers (`perplexity-search` on a `perplexity` key) stay visible; the analytics totals apply the same filter, so `total` always matches the per-provider breakdown ([#13641](https://github.com/diegosouzapw/OmniRoute/pull/13641)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(dashboard):** new opt-in flag `FREE_BADGE_REQUIRES_PROVIDER_FREE_TIER` (default off) makes the provider-page Free badge strict — it drops the display-name heuristic, non-boolean `free` fields and `:free` suffixes on registered providers without a documented free tier, while keeping catalogued free models, explicit `free: true` and `:free` on free-tier providers and compatible nodes; with the flag off the badges are unchanged ([#13645](https://github.com/diegosouzapw/OmniRoute/pull/13645)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(api):** share the single SOCKS5 flag reader across the settings proxy routes so the dashboard and the dispatcher stay consistent ([#13646](https://github.com/diegosouzapw/OmniRoute/pull/13646)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(stream-recovery):** log every mid-stream continuation outcome with its `attempt N/MAX` token — the stitched suffix, overlap rejection, terminal/empty continuation and tool-call refusals at debug, and a recovery that gives up (budget spent, or the continuation request returned no stream) at warn — without adding any warn line to a healthy or tool-call stream; the existing `mid-stream continuation attempt N/MAX` line is unchanged ([#13650](https://github.com/diegosouzapw/OmniRoute/pull/13650)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **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

View File

@@ -1 +0,0 @@
- **fix(sse):** a configured daily-quota reset hour that falls inside a daylight-saving gap (New York 02:00 on spring-forward, Havana/Santiago midnight) now resolves to the first wall-clock time that exists instead of landing an hour early, sometimes on the previous day ([#13671](https://github.com/diegosouzapw/OmniRoute/pull/13671)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(sse):** new opt-in flag `RETRY_AFTER_PROVENANCE_ENABLED` (default off): aggregated 429/503 unavailable responses omit `Retry-After` when no concrete future retry time is known instead of sending a synthetic 1s, carry `error.retry_after_provenance` (`signal` | `none`), and combo drain paths read prose retry hints from JSON and plain-text upstream bodies; non-JSON upstream error pages no longer log at warn ([#13672](https://github.com/diegosouzapw/OmniRoute/pull/13672)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **fix(api):** `DELETE /v1/batches/delete-completed` now applies the caller's API-key policy (endpoint allowlist, schedule, usage cap, rate limit) like every other `/v1` route, commits the key-scoped sweep per 200-batch chunk instead of holding one write lock for the whole sweep, refuses to loop without progress, and rejects a scope that names both a key and `allTenants` ([#13684](https://github.com/diegosouzapw/OmniRoute/pull/13684))

View File

@@ -1 +0,0 @@
- **fix(usage):** mark locally estimated token usage in the call log (`_omniroute.usageEstimated` on the logged response) so operators can tell estimated counts and costs from provider-reported ones — covers OmniRoute's own estimate for streams without upstream usage and web executors that report `estimated: true`; billing, API-key budgets, quota-share and client payloads are unchanged ([#13686](https://github.com/diegosouzapw/OmniRoute/pull/13686)) — thanks @maxmad64bis

View File

@@ -1 +0,0 @@
- **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)

View File

@@ -1 +0,0 @@
- **fix(security):** client-supplied image URLs (`image_url` / `mask_url` / message parts on image generation and upscale, chat `image_url` parts inlined by the vision bridge) and the NanoBanana result download now pin the `public-only` outbound guard with DNS validation, instead of inheriting the operator provider policy — `block-metadata` on a default install let a request body make the server fetch loopback/LAN URLs and forward the bytes upstream (GHSA-34rg-3pqj-35g9)

View File

@@ -1 +0,0 @@
- **fix(authz):** classify the 14 remaining spawn-capable `/api/cli-tools/*` routes (`all-statuses`, `status`, `detect` and the `claude/cline/codewhale/codex/crush/deepseek-tui/droid/kilo/openclaw/pi/smelt-settings` writers) and the `/api/skills/install` + `/api/skills/executions` pair as LOCAL_ONLY — they reach `child_process.spawn` transitively (`getCliRuntimeStatus()` / `detectAllTools()` / the skills sandbox) but only sat behind Tier 3 MANAGEMENT auth, which `requireLogin=false` waives; loopback/LAN enforcement now runs before any auth check, matching their already-gated siblings (GHSA-35fw-cv32-2373 — thanks Parth Narula; GHSA-jx89-f37j-pq89 — thanks Aeon). Tunnel-served dashboards lose the CLI Tools status badges, the same trade-off already accepted for grok/forge/jcode/qwen.

View File

@@ -1 +0,0 @@
- **fix(security):** redact Groq (`gsk_…`), xAI (`xai-…`) and every OpenAI-compatible `sk-…` key shape (DeepSeek 32-hex, Moonshot/Kimi, Together, …) in error bodies and the opt-in credential-masker guardrail — the catalog only knew the exact 48-char OpenAI form, so those keys passed through the guardrail verbatim and `gsk_`/`xai-` also reached public error responses (GHSA-r4q7-7f24-m29p)

View File

@@ -1,6 +0,0 @@
- **fix(api):** the model-test skip for image/music/video-only models (#13376) returned a
result with no `httpStatus`, and the `/api/models/test` route hands that field straight to
`NextResponse` — so a skipped test reached the client as HTTP 200 carrying `status: "error"`
in the body. It now answers 422: the request is valid, but that model's modality cannot be
exercised by a chat test. Also clears the `TS2741` that was failing `API Route Typecheck`
on the release branch.

View File

@@ -1 +0,0 @@
- **fix(security):** bump the `adm-zip` override to `^0.6.1` — 0.6.0 followed a symlink already present inside the extraction root and could write outside it (GHSA-vwc7-r8mq-g2x9 / CVE-2026-76845); 0.6.1 walks every path component with `lstat` and refuses symlinks. Reached only through `onnxruntime-node`'s install script, which unpacks the vendor's own binary — no request-path exposure.

View File

@@ -1 +0,0 @@
- Clear the `release/v3.8.51` `check:agent-skills-sync` base-red: regenerate `skills/cli-tunnel/SKILL.md` so the `tunnel create [type]` positional that #13009 taught the generator to read is reflected in the committed skill.

View File

@@ -1 +0,0 @@
- Clear the `release/v3.8.51` typecheck base-red from #12731: the new `Mode pack ranking gates` candidates in `open-sse/services/autoCombo/__tests__/autoCombo.test.ts` omitted the required `provider`, `model` and `errorRate` fields of `ProviderCandidate`, failing `check:open-sse-typecheck` (and with it `Fast Quality Gates`) on every open PR.

View File

@@ -1 +0,0 @@
- Clear the `release/v3.8.51` docs-sync base-red: EURouter (#13025) and GreenPT (#13024) took the live provider count to 358, leaving 7 STRICT drifts (`PROVIDER_REFERENCE.md`, 4 diagrams, 2 tier-flow images) plus the `AGENTS.md` / `llm.txt` / `package.json` count claims stale.

View File

@@ -1 +0,0 @@
- **test(models):** Pin `CATALOG_BUILD_TIMEOUT_MS` in the #12058 canonical-catalog regression so a cold catalog build racing the 8s default bound no longer flakes the suite with a 500 ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **fix(ci):** Allow `@omniroute/opencode-plugin-v2/` in the npm pack policy — [#12870](https://github.com/diegosouzapw/OmniRoute/pull/12870) shipped the v2 plugin beside its v1 sibling without widening the allowlist, so every packed file under it read as an unexpected artifact and failed `check:pack-policy` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **fix(ci):** Point the release-green validator's pack gate at the tree it is validating — the build-provenance guard defaulted to `origin/main`, which a release branch tip can never be an ancestor of mid-cycle, so `Package artifact` reported a false red on every run and the tarball boot-smoke cascaded off it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **docs(env):** document `OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS` (#12849, default 30 days) in `.env.example` and `ENVIRONMENT.md`; the stale-synced-catalog fail-open shipped the override without either, which the env/docs contract gate reports as code-only ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- **fix(ci):** the release-green validator now runs its package-artifact gate the way `ci.yml` does — build, stamp `dist/BUILD_SHA`, then validate against the tree under test. `build:cli` never writes the stamp, so even with the provenance ref pointed at `HEAD` the gate could only ever report "dist/BUILD_SHA is missing" once the build itself compiled ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **test(resilience):** the `/api/resilience` configuration-only key-set assertion now lists `credentialHealthCheck`, the sweep-interval setting #12043 added to the projection, so the integration suite stops reading a documented configuration key as leaked runtime state ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1,2 +0,0 @@
- **test(batches):** the two seeded-batch labels of the delete-completed route-scope suite that sat right after a `key*.id` argument are renamed to short literals (`route401`/`route500`), so a gitleaks scan that reads those lines (full-tree, or git-mode on a branch that adds them) no longer reports them as `generic-api-key` hits ([#13729](https://github.com/diegosouzapw/OmniRoute/pull/13729))
— no gate changes: the CI secret ratchet scans `src`/`open-sse`/`bin`/`electron`/`scripts`, never `tests/`

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