Compare commits

...

13 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
fe455d096d docs(changelog): link dependency policy fix to PR 11342 2026-08-24 02:11:04 -03:00
Diego Rodrigues de Sa e Souza
6853f8275d fix(deps): keep unused pnpm peers out of production 2026-08-24 02:07:04 -03:00
Ravi Tharuma
9b14896a6c feat(api): add Google AI Studio Gemini TTS (#11315)
Validated on a 17-PR combined board: gemini-tts + vertex-media + audio-speech-handler (41/41) within the board's 287/287, typecheck:core clean. Registers public google/gemini-*-tts speech models and translates OpenAI-compatible /v1/audio/speech to the AI Studio generateContent audio contract, reusing the Vertex inline-audio/PCM/WAV conversion path. Batch TTS only, Gemini Live is out of scope. Thank you @RaviTharuma!
2026-08-24 01:50:44 -03:00
Ravi Tharuma
29f26293c3 feat(compression): isolate sync engines in bounded worker pool (#11318)
Validated on a 17-PR combined board: compression-worker + colocate-standalone-esm-scope within the board's 287/287, typecheck:core clean, env-doc-sync clean. Offloads eligible sync compression engines into a bounded worker_threads pool with a strict serializable DTO boundary and fail-open on spawn/worker/timeout failure. Closes #11023. Thank you @RaviTharuma!
2026-08-24 01:50:39 -03:00
Nguyen Thanh Dat
cb11592441 fix(db): judge the proxy URL host by address, not by spelling (#11319)
Validated on a 17-PR combined board: upstream-proxy-host-spelling 8/8 within the board's 287/287, typecheck:core clean. Routes src/lib/db/upstreamProxy.ts through the shared outbound-guard helpers instead of a private dotted-quad regex copy that had drifted since #10843 — closes the IPv4-mapped IPv6, ULA, link-local and CGNAT bypasses while preserving the deliberate loopback allow (CLIProxyAPI on localhost:8317). Multicast widened from /224\. to the full 224.0.0.0/4, called out explicitly. Thank you @ntdat812!
2026-08-24 01:50:35 -03:00
Ravi Tharuma
5ee646e68e fix(github): verify access tokens during health checks (#11320)
Validated on a 17-PR combined board: token-health-check + token-health-no-refresh-token-expired-5326 + token-refresh-service within the board's 287/287, typecheck:core clean. GitHub access-token-only connections are now actively verified on each due health interval (via the existing Copilot token exchange); the parent credential is marked expired only on a confirmed 401, never on 403/429/5xx/network failures; response bodies and transport messages no longer enter token-refresh logs. Closes #10352. Thank you @RaviTharuma!
2026-08-24 01:50:30 -03:00
Paco Cartones
6984676d95 fix(quality): report the real failure line and stop double-counting ci.yml gates (#11321)
Validated on a 17-PR combined board: validate-release-green within the board's 287/287, typecheck:core clean. Two accuracy bugs in the release-green verdict tool: an unanchored regex blamed a passing test line (matching a filename containing 'fail'), and 6 gates were double-recorded as both hard-failure and drift due to an id-format mismatch (ci.yml script name vs curated id). Found while reading the #9985 verdict — good catch.
2026-08-24 01:50:26 -03:00
Paco Cartones
79f8ae9d1e fix(i18n): add the 3 pt-BR CLI keys that break the locale parity test (#11322)
Validated on a 17-PR combined board: typecheck:core clean, gates within baseline. Restores 3 missing pt-BR CLI keys (setup.opencode, serve.tls_cert, serve.tls_key) — parity restored, 823/823. Thank you @pacocartones!
2026-08-24 01:49:52 -03:00
Nguyen Thanh Dat
04b2c47940 fix(i18n): restore three placeholders dropped from the pt catalogue (#11325)
Validated on a 17-PR combined board: i18n-placeholder-parity within the board's 287/287, typecheck:core clean. Restores 3 dropped placeholders in pt.json (the visible one: the cache tile's subtitle was repeating its own label instead of showing the total) and adds a 42-locale placeholder-set gate so this class of drift can't recur silently. Thank you @ntdat812!
2026-08-24 01:49:48 -03:00
Paco Cartones
24ac71465e test(db): make singleton reset survive the full suite and un-skip the 3 DB-state tests (#11327)
Validated on a 17-PR combined board: capture-critical-db-state 7/7 (all three previously-skipped tests now run) within the board's 287/287, typecheck:core clean. Fixes the racy DATA_DIR-after-dynamic-import isolation and removes a duplicate type declaration. Thank you @pacocartones!
2026-08-24 01:49:42 -03:00
Nguyen Thanh Dat
8d6f91b558 fix(security): refuse proxy-authorization and proxy-authenticate upstream (#11328)
Validated on a 17-PR combined board: upstream-headers-proxy-auth within the board's 287/287, typecheck:core clean, gates within baseline. proxy-authorization and proxy-authenticate join the FORBIDDEN denylist — forwarding proxy-authorization to a model provider would hand that provider the operator's own proxy credential. Thank you @ntdat812!
2026-08-24 01:49:37 -03:00
Diego Rodrigues de Sa e Souza
c3698eedcb fix(dashboard): route the Adapta tutorial CTA through the branded shortener (#11329)
Validated on a 17-PR combined board: TSX parses clean, eslint clean. Adapta tutorial CTA href now points at the branded shortener (link.omniroute.online/adapta) while keeping the visible link text as the real domain. Completes #11196's shortener rollout.
2026-08-24 01:49:32 -03:00
Diego Rodrigues de Sa e Souza
adca3b881c fix(kie): map remaining google-imagen Market ids to their real KIE upstream ids (#11326)
Merging --admin with red discrimination (merge-gates §4). Fails: ESLint warnings ratchet drift (inherited base-red), Unit Tests shards containing stream-timing.test.ts (CPU-contention timing flake, assert.ok(total >= 15)ms — unrelated to this PR's scope, open-sse/handlers/imageGeneration.ts), and dast-smoke (advisory, isRequired:null).
2026-08-24 01:10:23 -03:00
47 changed files with 1716 additions and 160 deletions

View File

@@ -1027,6 +1027,16 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
# Maximum concurrent synchronous compression workers. Excess jobs wait FIFO.
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 2.
#OMNI_COMPRESSION_WORKERS=2
# Per-job worker timeout (ms). A timed-out worker is terminated and the request fails open.
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 120000.
#OMNI_COMPRESSION_WORKER_TIMEOUT_MS=120000
# Terminate idle compression workers after this many milliseconds.
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 60000.
#OMNI_COMPRESSION_WORKER_IDLE_MS=60000
# T02 stacked-pipeline engine circuit-breaker (OPT-IN, default off). When enabled, a compression
# engine that throws repeatedly across requests is skipped (fail-open) for a cooldown.
# Used by: open-sse/services/compression/pipelineEngineBreaker.ts.

View File

@@ -26,7 +26,8 @@
"testFailed": "Teste do provedor falhou: {error}",
"loginEnabled": "Login: habilitado (senha atualizada)",
"loginDisabled": "Login: desabilitado",
"providerInfo": "Provedor: {info}"
"providerInfo": "Provedor: {info}",
"opencode": "Instala e configura o plugin @omniroute/opencode-plugin incluído para o OpenCode"
},
"doctor": {
"title": "OmniRoute Doctor",
@@ -254,7 +255,9 @@
"no_recovery": "Desabilitar reinício automático em crash (modo debug)",
"max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)",
"tray": "Mostrar ícone na bandeja do sistema (apenas desktop, opt-in)",
"no_tray": "Desabilitar ícone na bandeja do sistema"
"no_tray": "Desabilitar ícone na bandeja do sistema",
"tls_cert": "Caminho para um certificado TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_CERT)",
"tls_key": "Caminho para a chave privada TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_KEY)"
},
"backup": {
"title": "Backup",

View File

@@ -0,0 +1 @@
- Added Google AI Studio Gemini batch text-to-speech support through `POST /v1/audio/speech`.

View File

@@ -0,0 +1,3 @@
- Run synchronous RTK and Caveman request compression in a bounded worker-thread pool, keeping
large `/v1/responses` compression heaps outside the HTTP isolate while preserving strict
fail-open behavior and per-engine telemetry.

View File

@@ -0,0 +1 @@
- **fix(github):** proactive credential health now verifies GitHub access tokens through the existing Copilot token exchange, marks only a confirmed `401 Unauthorized` as expired, and leaves rate limits, permission failures, upstream failures, and network errors routable ([#10352](https://github.com/diegosouzapw/OmniRoute/issues/10352)) — thanks @RaviTharuma

View File

@@ -0,0 +1 @@
- **fix(db):** the upstream proxy URL check judges the host by address instead of by spelling, so `http://[::ffff:169.254.169.254]`, `[::ffff:10.0.0.5]`, ULA/link-local and CGNAT targets are refused like their dotted equivalents ([#11319](https://github.com/diegosouzapw/OmniRoute/pull/11319))

View File

@@ -0,0 +1 @@
- **fix(i18n):** three `pt` strings had dropped their placeholders — the cache tile's subtitle repeated its own label instead of showing `{total}` — and a unit test now enforces placeholder parity with `en` across all locales ([#11325](https://github.com/diegosouzapw/OmniRoute/pull/11325))

View File

@@ -0,0 +1 @@
- **fix(kie):** map the remaining `google-imagen/*` KIE Market catalog ids (`nano-banana`, `nano-banana-pro`, `nano-banana-edit`) to their real, KIE-documented upstream `model` values — `#11225`'s fix only covered `nano-banana-2` ([#11326](https://github.com/diegosouzapw/OmniRoute/pull/11326)).

View File

@@ -0,0 +1 @@
- **fix(security):** `proxy-authorization` and `proxy-authenticate` are refused as upstream/custom headers, so a proxy credential is no longer forwarded to the model provider — the canonical denylist now matches the RFC 7230 §6.1 set the rest of the codebase already strips ([#11328](https://github.com/diegosouzapw/OmniRoute/pull/11328))

View File

@@ -0,0 +1,3 @@
- **fix(deps):** prevent pnpm from auto-installing the unused `@lobehub/ui` peer subtree of
`@lobehub/icons`, keeping six unneeded packages with incompatible or unverifiable license
metadata out of production installs ([#11342](https://github.com/diegosouzapw/OmniRoute/pull/11342)).

View File

@@ -531,6 +531,9 @@ detection above).
| `OMNIROUTE_CONFIG_HOT_RELOAD_MS` | `5000` | `src/lib/config/hotReload.ts` | Polling interval (ms) for config hot-reload. Lower than `1000` is rejected. |
| `OMNIROUTE_DISABLE_REDIS_AUTH_CACHE` | _(enabled)_ | `src/lib/db/apiKeys.ts` | Set `1` to bypass the Redis-backed API-key auth cache (forces DB reads). |
| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | `0` | `open-sse/services/compression/engines/rtk/filterLoader.ts` | Trust user-managed RTK project filter rules without strict signature checks. |
| `OMNI_COMPRESSION_WORKERS` | `2` | `open-sse/services/compression/compressionWorkerPool.ts` | Maximum concurrent synchronous RTK/Caveman workers; excess jobs wait FIFO. |
| `OMNI_COMPRESSION_WORKER_TIMEOUT_MS` | `120000` | `open-sse/services/compression/compressionWorkerPool.ts` | Per-job timeout in milliseconds. Timed-out workers are terminated and the request fails open unchanged. |
| `OMNI_COMPRESSION_WORKER_IDLE_MS` | `60000` | `open-sse/services/compression/compressionWorkerPool.ts` | Idle lifetime in milliseconds before an unused compression worker is terminated. |
| `COMPRESSION_PIPELINE_BREAKER_ENABLED` | `false` | `open-sse/services/compression/pipelineEngineBreaker.ts` | T02 stacked-pipeline per-engine circuit-breaker master switch. **Opt-in (default off)** — when on, an engine that throws repeatedly across requests is skipped (fail-open) for a cooldown; off = byte-identical legacy behavior. |
| `COMPRESSION_PIPELINE_BREAKER_THRESHOLD` | `3` | `open-sse/services/compression/pipelineEngineBreaker.ts` | Consecutive cross-request failures before an engine's breaker opens. |
| `COMPRESSION_PIPELINE_BREAKER_COOLDOWN_MS` | `30000` | `open-sse/services/compression/pipelineEngineBreaker.ts` | Milliseconds an opened engine stays skipped before a half-open probe. |

View File

@@ -287,6 +287,19 @@ export const AUDIO_TRANSLATION_PROVIDERS: Record<string, AudioProvider> = {
};
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
google: {
id: "google",
credentialProviderId: "gemini",
baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
authType: "apikey",
authHeader: "x-goog-api-key",
format: "gemini-tts",
models: [
{ id: "gemini-3.1-flash-tts-preview", name: "Gemini 3.1 Flash TTS" },
{ id: "gemini-2.5-flash-preview-tts", name: "Gemini 2.5 Flash TTS" },
{ id: "gemini-2.5-pro-preview-tts", name: "Gemini 2.5 Pro TTS" },
],
},
vertex: {
id: "vertex",
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",

View File

@@ -0,0 +1,81 @@
import { Buffer } from "node:buffer";
import { extractInlineAudio, parsePcmSampleRate, pcmToWav } from "./vertexMedia.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
import { upstreamErrorResponse } from "../utils/audioResponse.ts";
import { errorResponse } from "../utils/error.ts";
type GeminiTtsCredentials = {
apiKey?: string | null;
accessToken?: string | null;
};
export class GeminiTtsUpstreamError extends Error {
constructor(
public readonly response: Response,
public readonly body: string
) {
super(`Gemini TTS upstream error (${response.status})`);
}
}
export async function geminiGenerateSpeech(
credentials: GeminiTtsCredentials,
options: { model: string; text: string; voice: string }
): Promise<Buffer> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (credentials.apiKey) {
headers["x-goog-api-key"] = credentials.apiKey;
} else if (credentials.accessToken) {
headers.Authorization = `Bearer ${credentials.accessToken}`;
}
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(options.model)}:generateContent`,
{
method: "POST",
headers,
body: JSON.stringify({
contents: [{ parts: [{ text: options.text }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: options.voice },
},
},
},
}),
}
);
if (!response.ok) {
throw new GeminiTtsUpstreamError(response, await response.text());
}
const inline = extractInlineAudio(await response.json());
if (!inline) throw new Error("Gemini TTS response did not contain audio data");
return pcmToWav(Buffer.from(inline.base64, "base64"), parsePcmSampleRate(inline.mimeType));
}
export async function handleGeminiTtsSpeech(
credentials: GeminiTtsCredentials,
options: { model: string; text: string; voice?: unknown }
): Promise<Response> {
try {
const wav = await geminiGenerateSpeech(credentials, {
model: options.model,
text: options.text,
voice:
typeof options.voice === "string" && options.voice.trim() ? options.voice.trim() : "Kore",
});
return new Response(new Uint8Array(wav), {
status: 200,
headers: { ...CORS_HEADERS, "Content-Type": "audio/wav" },
});
} catch (error) {
if (error instanceof GeminiTtsUpstreamError) {
return upstreamErrorResponse(error.response, error.body);
}
const message = error instanceof Error ? error.message : String(error);
return errorResponse(500, `Speech request failed: ${message}`);
}
}

View File

@@ -156,13 +156,13 @@ export function pcmToWav(
return Buffer.concat([header, pcm]);
}
function parseSampleRate(mimeType: string | undefined): number {
export function parsePcmSampleRate(mimeType: string | undefined): number {
if (!mimeType) return 24000;
const match = /rate=(\d+)/i.exec(mimeType);
return match ? parseInt(match[1], 10) : 24000;
}
function extractInlineAudio(
export function extractInlineAudio(
data: unknown
): { base64: string; mimeType: string } | null {
const parts = (data as { candidates?: Array<{ content?: { parts?: unknown[] } }> })?.candidates?.[0]
@@ -215,7 +215,7 @@ export async function vertexGenerateSpeech(
const inline = extractInlineAudio(data);
if (!inline) throw new Error("Vertex TTS returned no audio content");
const pcm = Buffer.from(inline.base64, "base64");
return { audio: pcmToWav(pcm, parseSampleRate(inline.mimeType)), contentType: "audio/wav" };
return { audio: pcmToWav(pcm, parsePcmSampleRate(inline.mimeType)), contentType: "audio/wav" };
}
/** Gemini transcription (audio → text). `audioBase64` is the raw file bytes, base64-encoded. */

View File

@@ -21,6 +21,7 @@ import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts"
import { buildAuthHeaders } from "../config/registryUtils.ts";
import { kieExecutor } from "../executors/kie.ts";
import { vertexGenerateSpeech } from "../executors/vertexMedia.ts";
import { handleGeminiTtsSpeech } from "../executors/geminiTts.ts";
import { handleAwsPollySpeech } from "../executors/awsPollyTts.ts";
import { handleEdgeTtsSpeech } from "../executors/edgeTts.ts";
import { GttsUpstreamError, normalizeGttsLang, synthesizeGtts } from "../executors/gtts.ts";
@@ -889,6 +890,13 @@ export async function handleAudioSpeech({
headers: { ...CORS_HEADERS, "Content-Type": contentType },
});
}
if (providerConfig.format === "gemini-tts") {
return handleGeminiTtsSpeech(credentials, {
model: modelId,
text: body.input,
voice: body.voice,
});
}
if (providerConfig.format === "hyperbolic") {
return handleHyperbolicSpeech(providerConfig, body, token);

View File

@@ -91,8 +91,20 @@ interface KieImageOptions {
} | null;
}
// KIE Market catalog ids are namespaced for OmniRoute's catalog
// (`google-imagen/<model>`), but the KIE Market createTask API expects
// vendor-specific upstream ids that do not follow a single consistent
// pattern (confirmed against docs.kie.ai/market/google/* — see #11225,
// #11296): nano-banana-2 and nano-banana-pro drop the vendor namespace
// entirely, while nano-banana and nano-banana-edit use a `google/` prefix
// instead of `google-imagen/`. Every other KIE Market namespace (seedream,
// flux, ideogram, qwen, wan, grok-imagine, gpt) already matches its real
// upstream id byte-for-byte, so this map stays scoped to google-imagen.
export const KIE_MARKET_UPSTREAM_MODEL_IDS: ReadonlyMap<string, string> = new Map([
["google-imagen/nano-banana", "google/nano-banana"],
["google-imagen/nano-banana-2", "nano-banana-2"],
["google-imagen/nano-banana-pro", "nano-banana-pro"],
["google-imagen/nano-banana-edit", "google/nano-banana-edit"],
]);
export function resolveKieMarketUpstreamModelId(publicModelId: string): string {

View File

@@ -0,0 +1,40 @@
import { parentPort } from "node:worker_threads";
import {
applyCompression,
applyStackedCompression,
type StackedCompressionStep,
} from "./strategySelector.ts";
import type {
CompressionWorkerJob,
CompressionWorkerMessage,
} from "./compressionWorkerProtocol.ts";
if (!parentPort) throw new Error("compressionWorker must run in a worker thread");
parentPort.on("message", (job: CompressionWorkerJob) => {
try {
const onEngineStep = (step: StackedCompressionStep) =>
parentPort.postMessage({
id: job.id,
type: "step",
step,
} satisfies CompressionWorkerMessage);
const result =
job.mode === "stacked"
? applyStackedCompression(job.body, job.options?.config?.stackedPipeline, {
...job.options,
onEngineStep,
})
: applyCompression(job.body, job.mode, job.options);
parentPort.postMessage({
id: job.id,
type: "result",
result,
} satisfies CompressionWorkerMessage);
} catch (error) {
parentPort.postMessage({
id: job.id,
type: "error",
error: error instanceof Error ? error.message : String(error),
} satisfies CompressionWorkerMessage);
}
});

View File

@@ -0,0 +1,164 @@
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Worker } from "node:worker_threads";
import type { CompressionResult } from "./types.ts";
import type { StackedCompressionStep } from "./strategySelector.ts";
import type {
CompressionWorkerJob,
CompressionWorkerMessage,
CompressionWorkerOptions,
} from "./compressionWorkerProtocol.ts";
function positiveInteger(value: string | undefined, fallback: number): number {
const parsed = Number(value);
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function workerUrl(): URL {
const dir = dirname(fileURLToPath(import.meta.url));
for (const name of ["compressionWorker.js", "compressionWorker.ts"]) {
if (existsSync(join(dir, name))) return new URL(name, import.meta.url);
}
return new URL("compressionWorker.js", import.meta.url);
}
function unchanged(body: Record<string, unknown>): CompressionResult {
return { body, compressed: false, stats: null };
}
interface PendingJob extends CompressionWorkerJob {
originalBody: Record<string, unknown>;
resolve: (result: CompressionResult) => void;
onEngineStep?: (step: StackedCompressionStep) => void;
}
interface PoolWorker {
worker: Worker;
job: PendingJob | null;
timeout: NodeJS.Timeout | null;
idle: NodeJS.Timeout | null;
}
export class CompressionWorkerPool {
private readonly queue: PendingJob[] = [];
private readonly workers = new Set<PoolWorker>();
private nextId = 1;
private readonly size: number;
private readonly timeoutMs: number;
private readonly idleMs: number;
constructor({
size = positiveInteger(process.env.OMNI_COMPRESSION_WORKERS, 2),
timeoutMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_TIMEOUT_MS, 120_000),
idleMs = positiveInteger(process.env.OMNI_COMPRESSION_WORKER_IDLE_MS, 60_000),
}: { size?: number; timeoutMs?: number; idleMs?: number } = {}) {
this.size = Math.max(1, Math.floor(size));
this.timeoutMs = Math.max(1, Math.floor(timeoutMs));
this.idleMs = Math.max(1, Math.floor(idleMs));
}
run(
body: Record<string, unknown>,
mode: CompressionWorkerJob["mode"],
options?: CompressionWorkerOptions,
onEngineStep?: (step: StackedCompressionStep) => void
): Promise<CompressionResult> {
return new Promise((resolve) => {
this.queue.push({
id: this.nextId++,
body,
mode,
options,
originalBody: body,
resolve,
onEngineStep,
});
this.dispatch();
});
}
async close(): Promise<void> {
for (const job of this.queue.splice(0)) job.resolve(unchanged(job.originalBody));
await Promise.all([...this.workers].map((slot) => this.remove(slot, true)));
}
private spawn(): PoolWorker {
const slot: PoolWorker = {
worker: new Worker(workerUrl()),
job: null,
timeout: null,
idle: null,
};
this.workers.add(slot);
slot.worker.on("message", (message: CompressionWorkerMessage) =>
this.handleMessage(slot, message)
);
slot.worker.on("error", () => this.fail(slot));
slot.worker.on("exit", () => {
if (this.workers.has(slot)) this.fail(slot);
});
return slot;
}
private dispatch(): void {
while (this.queue.length) {
let slot = [...this.workers].find((candidate) => !candidate.job);
if (!slot && this.workers.size < this.size) slot = this.spawn();
if (!slot) return;
if (slot.idle) clearTimeout(slot.idle);
const job = this.queue.shift();
if (!job) return;
slot.job = job;
slot.timeout = setTimeout(() => this.fail(slot!), this.timeoutMs);
slot.timeout.unref();
const { originalBody: _body, resolve: _resolve, onEngineStep: _step, ...wireJob } = job;
slot.worker.postMessage(wireJob);
}
}
private handleMessage(slot: PoolWorker, message: CompressionWorkerMessage): void {
const job = slot.job;
if (!job || job.id !== message.id) return;
if (message.type === "step") {
try {
job.onEngineStep?.(message.step);
} catch {
// Telemetry is best-effort.
}
return;
}
this.finish(slot, message.type === "result" ? message.result : unchanged(job.originalBody));
}
private finish(slot: PoolWorker, result: CompressionResult): void {
const job = slot.job;
if (!job) return;
if (slot.timeout) clearTimeout(slot.timeout);
slot.timeout = null;
slot.job = null;
job.resolve(result);
slot.idle = setTimeout(() => void this.remove(slot, false), this.idleMs);
slot.idle.unref();
this.dispatch();
}
private fail(slot: PoolWorker): void {
const job = slot.job;
if (job) job.resolve(unchanged(job.originalBody));
slot.job = null;
void this.remove(slot, true).finally(() => this.dispatch());
}
private async remove(slot: PoolWorker, terminate: boolean): Promise<void> {
if (!this.workers.delete(slot)) return;
if (slot.timeout) clearTimeout(slot.timeout);
if (slot.idle) clearTimeout(slot.idle);
if (terminate) await slot.worker.terminate().catch(() => undefined);
}
}
let pool: CompressionWorkerPool | null = null;
export function runCompressionInWorker(
body: Record<string, unknown>,
mode: CompressionWorkerJob["mode"],
options?: CompressionWorkerOptions,
onEngineStep?: (step: StackedCompressionStep) => void
): Promise<CompressionResult> {
pool ??= new CompressionWorkerPool();
return pool.run(body, mode, options, onEngineStep);
}
export async function closeCompressionWorkerPoolForTests(): Promise<void> {
const active = pool;
pool = null;
await active?.close();
}

View File

@@ -0,0 +1,71 @@
import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts";
import type { StackedCompressionStep } from "./strategySelector.ts";
import type {
CompressionStage,
CompressionWireFormat,
ImageTransportFidelity,
} from "./engines/types.ts";
export interface CompressionWorkerOptions {
model?: string;
supportsVision?: boolean | null;
providerTransport?: "direct" | "aggregator";
provider?: string;
imageTransportFidelity?: ImageTransportFidelity;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
}
export interface CompressionWorkerJob {
id: number;
body: Record<string, unknown>;
mode: CompressionMode;
options?: CompressionWorkerOptions;
}
export type CompressionWorkerMessage =
| { id: number; type: "step"; step: StackedCompressionStep }
| { id: number; type: "result"; result: CompressionResult }
| { id: number; type: "error"; error: string };
function isPlainObject(value: object): value is Record<string, unknown> {
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
export function isStrictlySerializable(value: unknown, seen = new Set<object>()): boolean {
if (
value === null ||
typeof value === "string" ||
typeof value === "boolean" ||
typeof value === "number"
) {
return typeof value !== "number" || Number.isFinite(value);
}
if (typeof value !== "object" || seen.has(value)) return false;
seen.add(value);
if (Array.isArray(value)) return value.every((entry) => isStrictlySerializable(entry, seen));
if (!isPlainObject(value)) return false;
return Object.values(value).every((entry) => isStrictlySerializable(entry, seen));
}
const WORKER_STACK_ENGINES = new Set(["caveman", "rtk", "standard"]);
export function isCompressionWorkerEligible(
body: Record<string, unknown>,
mode: CompressionMode,
options?: CompressionWorkerOptions
): boolean {
if (mode !== "standard" && mode !== "rtk" && mode !== "stacked") return false;
if (mode === "stacked") {
const pipeline = options?.config?.stackedPipeline;
if (!Array.isArray(pipeline) || pipeline.length === 0) return false;
if (
pipeline.some((step) => {
const engine = typeof step === "string" ? step : step.engine;
return !WORKER_STACK_ENGINES.has(engine);
})
) {
return false;
}
}
return isStrictlySerializable({ body, mode, ...(options ? { options } : {}) });
}

View File

@@ -519,6 +519,28 @@ async function runCompressionAsync(
cachingContext?: CachingDetectionContext;
}
): Promise<CompressionResult> {
const workerOptions = options
? {
model: options.model,
supportsVision: options.supportsVision,
providerTransport: options.providerTransport,
provider: options.provider,
imageTransportFidelity: options.imageTransportFidelity,
sourceFormat: options.sourceFormat,
targetFormat: options.targetFormat,
compressionStage: options.compressionStage,
config: options.config,
}
: undefined;
const { isCompressionWorkerEligible } = await import("./compressionWorkerProtocol.ts");
if (isCompressionWorkerEligible(body, mode, workerOptions)) {
try {
const { runCompressionInWorker } = await import("./compressionWorkerPool.ts");
return await runCompressionInWorker(body, mode, workerOptions, options?.onEngineStep);
} catch {
return { body, compressed: false, stats: null };
}
}
if (
options?.config?.memoizeCompressionResults === true &&
// Only memoize for an explicit principal — a missing principalId would collapse

View File

@@ -28,12 +28,10 @@ export async function refreshCopilotToken(
);
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh Copilot token", {
status: response.status,
error: errorText,
});
return null;
return { status: response.status };
}
const data = await response.json();
@@ -49,8 +47,8 @@ export async function refreshCopilotToken(
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", "Error refreshing Copilot token", {
error: error.message,
errorType: error?.name || "Error",
});
return null;
return { status: null };
}
}

View File

@@ -1,6 +1,10 @@
packages:
- "packages/*"
- "open-sse"
# Match `.npmrc`'s legacy-peer-deps posture. OmniRoute imports only the deep
# icon modules from @lobehub/icons; auto-installing its unused @lobehub/ui peer
# pulls a large UI subtree (including packages without distributable licenses).
autoInstallPeers: false
allowBuilds:
"@parcel/watcher": true
"@swc/core": true

View File

@@ -33,6 +33,14 @@ const STANDALONE = process.env.OMNIROUTE_STANDALONE_DIR
const CALL_LOG_WORKER_REL = join("src", "lib", "usage", "callLogArtifactWorker.js");
const CALL_LOG_WORKER_SRC = join(ROOT, "src", "lib", "usage", "callLogArtifactWorker.ts");
const COMPRESSION_WORKER_REL = join("open-sse", "services", "compression", "compressionWorker.js");
const COMPRESSION_WORKER_SRC = join(
ROOT,
"open-sse",
"services",
"compression",
"compressionWorker.ts"
);
const WORKER_REL = join(
"open-sse",
"services",
@@ -107,9 +115,26 @@ function main() {
);
console.log("[colocate-standalone] ✅ call-log artifact worker bundled");
const compressionWorkerDest = join(STANDALONE, COMPRESSION_WORKER_REL);
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
runBuildTool(
"esbuild",
"esbuild",
[
COMPRESSION_WORKER_SRC,
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${compressionWorkerDest}`,
],
{ stdio: "inherit" }
);
console.log("[colocate-standalone] ✅ compression worker bundled");
// The call-log worker is always present; scope it to ESM immediately. The
// optional LLMLingua worker dir is added below only when its deps are installed.
const workerDirs = [dirname(callLogWorkerDest)];
const workerDirs = [dirname(callLogWorkerDest), dirname(compressionWorkerDest)];
if (!hasOptionals) {
console.log(

View File

@@ -45,6 +45,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
// LLMLingua ONNX worker — esbuild'd standalone .js spawned via worker_threads
// (the Next.js bundler can't trace the computed Worker path). Kept like the MCP server.
"open-sse/services/compression/engines/llmlingua/onnxWorker.js",
"open-sse/services/compression/compressionWorker.js",
"src/lib/usage/callLogArtifactWorker.js",
"package.json",
"peer-stamp.mjs",

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env node
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
import { existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { basename, dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleStandalone } from "./assembleStandalone.mjs";
import { assertSqlitePrebuildExists } from "./electronRebuildPlan.mjs";
import { pruneElectronRuntimeDocs } from "./electronRuntimeDocs.mjs";
import { stageOptionalPacks } from "./optionalPackStaging.mjs";
import { runBuildTool } from "./buildToolRunner.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -169,6 +170,27 @@ assembleStandalone({
// app they would point at the build machine's absolute paths and break on install.
materializeSymlinks: true,
});
const compressionWorkerDest = join(
ELECTRON_STANDALONE_DIR,
"open-sse",
"services",
"compression",
"compressionWorker.js"
);
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
runBuildTool(
"esbuild",
"esbuild",
[
join(ROOT, "open-sse", "services", "compression", "compressionWorker.ts"),
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
`--outfile=${compressionWorkerDest}`,
],
{ stdio: "inherit" }
);
const docsPrune = pruneElectronRuntimeDocs(ELECTRON_STANDALONE_DIR);
if (docsPrune.removedFiles > 0) {

View File

@@ -407,6 +407,40 @@ if (existsSync(llmWorkerSrc)) {
}
}
// ── Step 8.6b: Bundle synchronous compression worker ──────────────────
const compressionWorkerSrc = join(
ROOT,
"open-sse",
"services",
"compression",
"compressionWorker.ts"
);
const compressionWorkerDest = join(
DIST_DIR,
"open-sse",
"services",
"compression",
"compressionWorker.js"
);
if (!existsSync(compressionWorkerSrc)) {
throw new Error("Required compression worker source is missing");
}
console.log(" 🔨 Bundling compression worker...");
mkdirSync(dirname(compressionWorkerDest), { recursive: true });
runBuildTool(
"esbuild",
"esbuild",
[
"open-sse/services/compression/compressionWorker.ts",
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
"--outfile=dist/open-sse/services/compression/compressionWorker.js",
],
{ cwd: ROOT, stdio: "inherit" }
);
// ── Step 8.7: Bundle CLI Entrypoint ──────────────────────────
const cliSrcFile = join(ROOT, "bin", "omniroute.ts");
const cliDestFile = join(ROOT, "bin", "omniroute.mjs");

View File

@@ -90,13 +90,30 @@ export function baselineValue(metric, root = ROOT) {
}
}
// A line that is unambiguously a PASS. Test reporters print the file name on BOTH the
// pass and the fail line, so a green line for a file whose NAME contains "fail"
// (fail-fast-*.test.ts, failover-*.test.ts) must never be offered as a failure cause.
const GREEN_LINE_RE = /^[✓✔√]/;
// Markers that are only meaningful at the START of a line: "FAIL" also occurs inside test
// FILE NAMES and inside summary prose ("Test Files 1 failed"), so matching it anywhere —
// and case-insensitively — reports a PASSING file as the cause of the red.
const LINE_START_FAILURE_RE = /^(?:[✖✗×]|FAIL\b|not ok\b|REGRESS)/;
// Markers that are unambiguous ANYWHERE in the line: tsc and Node emit them mid-line
// ("src/x.ts(10,5): error TS2322: ..."), so these stay unanchored. They are matched
// case-SENSITIVELY because that is how the emitting tools actually spell them.
const INLINE_FAILURE_RE = /\berror TS\d+\b|\bAssertionError\b|\bError:|\bREGRESS/;
/** Best-effort "first meaningful failure line" from captured command output. */
export function firstFailureLine(out) {
const lines = String(out || "")
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
const hit = lines.find((l) => /||not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
const hit = lines.find(
(l) => !GREEN_LINE_RE.test(l) && (LINE_START_FAILURE_RE.test(l) || INLINE_FAILURE_RE.test(l))
);
return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
}
@@ -232,6 +249,36 @@ export function fullCiTimeoutFor(gateId) {
return FULL_CI_TIMEOUT_OVERRIDES_MS[gateId] ?? FULL_CI_DEFAULT_TIMEOUT_MS;
}
// ci.yml gate scripts whose result the CURATED pass already records under a DIFFERENT id.
// Without this map the --full-ci pass re-records them unconditionally as kind:"hard" while
// the curated pass recorded them as kind:"drift", and the SAME gate is printed in BOTH
// verdict buckets of one report (file-size / compression-budget appeared as a hard failure
// and as drift simultaneously in the #9985 verdict).
export const FULL_CI_CURATED_ALIASES = {
lint: "lint-errors",
"check:workflows": "workflow-lint",
"check:complexity-ratchets": "complexity",
};
/** Curated-pass id equivalent to a ci.yml gate script id ("check:file-size" -> "file-size"). */
export function curatedEquivalentId(scriptId) {
const id = String(scriptId || "");
if (Object.hasOwn(FULL_CI_CURATED_ALIASES, id)) return FULL_CI_CURATED_ALIASES[id];
return id.startsWith("check:") ? id.slice("check:".length) : id;
}
/**
* Bucket a --full-ci gate must be reported under: the classification the curated pass already
* gave the equivalent gate, else "hard" (the --full-ci default for gates the curated list does
* not cover). This only changes WHICH BUCKET a result is printed in — it never changes whether
* a gate runs, nor whether it passed.
*/
export function fullCiKindFor(scriptId, results) {
const equivalent = curatedEquivalentId(scriptId);
const curated = (results || []).find((r) => r.id === scriptId || r.id === equivalent);
return curated?.kind ?? "hard";
}
/**
* Parse a ci.yml text and return the ordered, de-duplicated list of gate commands to run.
* Each entry: { id, job, args:["run", <script>, ...("--" + args)], env }.
@@ -716,7 +763,10 @@ async function main() {
record({
id: g.id,
label: `ci.yml:${g.job} → npm ${g.args.join(" ")}`,
kind: "hard",
// Respect the curated classification when the curated pass already ran an equivalent
// gate under a different id — otherwise the same ratchet is reported as a HARD failure
// here AND as drift above, in one self-contradicting verdict.
kind: fullCiKindFor(g.id, results),
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});

View File

@@ -7,6 +7,10 @@ type AdaptaTutorialModalProps = {
onClose: () => void;
};
// The Adapta CTA href points at https://link.omniroute.online/adapta (our own
// shortener, the `adapta` slug) so the click lands in our Kutt metrics. The visible
// link text intentionally stays the real domain (agent.adapta.one/agentic-chat) so
// users still see where they are going.
export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProps) {
const t = useTranslations("providers.adaptaTutorial");
@@ -29,7 +33,7 @@ export function AdaptaTutorialModal({ isOpen, onClose }: AdaptaTutorialModalProp
<p className="text-text-muted mt-0.5">
{t("step1DescPrefix")}{" "}
<a
href="https://agent.adapta.one/agentic-chat"
href="https://link.omniroute.online/adapta"
target="_blank"
rel="noopener noreferrer"
className="underline text-primary"

View File

@@ -4242,7 +4242,7 @@
"smokeSendSuccessWithTask": "message/send ok (tarefa {taskId}).",
"smokeSendSuccess": "message/send ok.",
"smokeStreamFailed": "Teste de fumo message/stream falhou.",
"smokeStreamSuccessWithTask": "message/stream ok (tarefa {taskId}).",
"smokeStreamSuccessWithTask": "message/stream ok (tarefa {taskId}{stateSuffix}).",
"smokeStreamNoTaskId": "message/stream terminou sem ID de tarefa.",
"health": "Estado de saúde",
"ok": "OK",
@@ -10202,7 +10202,7 @@
"scanning": "A analisar...",
"opencodeIntegration": "Integração OpenCode",
"opencodeDetected": "opencode {version} detetado",
"opencodeDesc": "Gera um {configFile} pronto a usar com a tua configuração OmniRoute",
"opencodeDesc": "Gera um {configFile} pronto a usar com o URL base do OmniRoute e todos os modelos disponíveis — coloca-o na raiz do teu projeto e executa {command}.",
"downloadConfig": "Descarregar {file}",
"downloaded": "Descarregado!",
"setupGuideTitle": "Guia de configuração",
@@ -10395,7 +10395,7 @@
"dbEntries": "Entradas na BD",
"dbEntriesSub": "Persistido (SQLite)",
"cacheHits": "Acertos de cache",
"cacheHitsSub": "Acertos",
"cacheHitsSub": "de {total} no total",
"tokensSaved": "Tokens Poupançados",
"tokensSavedSub": "Estimado a partir de acertos",
"hitRate": "Taxa de acertos",

View File

@@ -1,5 +1,11 @@
/** Upstream proxy config persistence for upstream_proxy_config table. */
import { getDbInstance } from "./core";
import {
isCloudMetadataHost,
isPrivateHost as isPrivateNetworkHost,
mappedIpv4Host,
} from "@/shared/network/outboundUrlGuard";
import { ipVersion, normalizeHost } from "@/shared/network/privateHost";
/** Which embedded proxy handles the retry leg when mode === "fallback". */
export type FallbackBackend = "cliproxyapi" | "dario";
@@ -37,26 +43,39 @@ function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"];
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
/** IPv4 multicast (224.0.0.0/4) — kept from this module's original rule set. */
function isMulticastIpv4(host: string): boolean {
const first = Number.parseInt(host.split(".")[0], 10);
return ipVersion(host) === 4 && first >= 224 && first <= 239;
}
/**
* Reject a proxy target that is private or cloud-metadata, judging the ADDRESS
* rather than its spelling.
*
* This module used to carry its own prefix regexes, which matched only the
* dotted form: `http://169.254.169.254` was refused while
* `http://[::ffff:169.254.169.254]` — the same address, serialised by WHATWG
* URL as `::ffff:a9fe:a9fe` — was accepted, as were `::ffff:10.0.0.5`,
* `fd00::/8`, `fe80::/10` and CGNAT `100.64.0.0/10`. #10843 fixed exactly that
* class in the shared guard; routing this copy through the same helpers keeps
* the two from drifting apart again.
*
* The deliberate exception stays: CLIProxyAPI runs on localhost:8317, so
* loopback is allowed — and now so is its mapped spelling, for the same
* address-not-spelling reason.
*/
function isPrivateHost(hostname: string): boolean {
// CLIProxyAPI runs on localhost:8317 — allow loopback explicitly
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false;
if (BLOCKED_HOSTNAMES.includes(hostname)) return true;
if (
/^10\./.test(hostname) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
/^192\.168\./.test(hostname)
)
return true;
if (
/^0\./.test(hostname) ||
/^127\./.test(hostname) ||
/^224\./.test(hostname) ||
/^169\.254\./.test(hostname)
)
return true;
return false;
const normalized = normalizeHost(hostname);
const asIpv4 = mappedIpv4Host(normalized) ?? normalized;
if (LOOPBACK_HOSTNAMES.has(normalized) || LOOPBACK_HOSTNAMES.has(asIpv4)) return false;
return (
isCloudMetadataHost(normalized) || isPrivateNetworkHost(normalized) || isMulticastIpv4(asIpv4)
);
}
export function validateProxyUrl(

View File

@@ -636,35 +636,45 @@ export async function checkConnection(conn) {
copilotExpiresAtMs - Date.now() < TOKEN_EXPIRY_BUFFER;
let refreshedProviderSpecificData: Record<string, unknown> | null = null;
if (copilotAboutToExpire) {
const hideLogs = await shouldHideLogs();
const proxyResolution = await resolveProxyForConnection(conn.id);
const proxyConfig = extractResolvedProxyConfig(proxyResolution);
const healthCheckLog = {
info: (tag: string, msg: string) => {
if (!hideLogs) console.log(LOG_PREFIX, `[${tag}]`, msg);
},
warn: (tag: string, msg: string) => {
if (!hideLogs) console.warn(LOG_PREFIX, `[${tag}]`, msg);
},
error: (tag: string, msg: string, extra?: Record<string, unknown>) => {
if (!hideLogs) console.error(LOG_PREFIX, `[${tag}]`, msg, extra || "");
},
};
const hideLogs = await shouldHideLogs();
const proxyResolution = await resolveProxyForConnection(conn.id);
const proxyConfig = extractResolvedProxyConfig(proxyResolution);
const healthCheckLog = {
info: (tag: string, msg: string) => {
if (!hideLogs) console.log(LOG_PREFIX, `[${tag}]`, msg);
},
warn: (tag: string, msg: string) => {
if (!hideLogs) console.warn(LOG_PREFIX, `[${tag}]`, msg);
},
error: (tag: string, msg: string, extra?: Record<string, unknown>) => {
if (!hideLogs) console.error(LOG_PREFIX, `[${tag}]`, msg, extra || "");
},
};
const copilotResult = await refreshCopilotToken(
conn.accessToken,
healthCheckLog,
proxyConfig,
getCopilotTokenBaseUrl(conn)
);
if (copilotResult?.token) {
refreshedProviderSpecificData = {
...providerSpecificData,
copilotToken: copilotResult.token,
copilotTokenExpiresAt: copilotResult.expiresAt,
};
}
const copilotResult = await refreshCopilotToken(
conn.accessToken,
healthCheckLog,
proxyConfig,
getCopilotTokenBaseUrl(conn)
);
if (copilotResult?.status === 401) {
await updateProviderConnection(conn.id, {
testStatus: "expired",
lastHealthCheckAt: now,
lastError: "GitHub rejected the access token",
lastErrorAt: now,
lastErrorType: "github_access_token_invalid",
lastErrorSource: "oauth",
errorCode: "github_access_token_invalid",
});
return;
}
if (copilotResult?.token && copilotAboutToExpire) {
refreshedProviderSpecificData = {
...providerSpecificData,
copilotToken: copilotResult.token,
copilotTokenExpiresAt: copilotResult.expiresAt,
};
}
if (canClearGitHubNoRefreshTokenState(conn)) {

View File

@@ -10,6 +10,16 @@ const FORBIDDEN = new Set(
"content-length",
"keep-alive",
"proxy-connection",
// The two RFC 7230 §6.1 hop-by-hop names this list was missing. They belong
// to the connection between the client and OmniRoute (or its upstream
// proxy), never to the request OmniRoute makes to the model provider —
// forwarding `proxy-authorization` hands that proxy credential to the
// provider. `src/lib/services/reverseProxy.ts` (HOP_BY_HOP),
// `src/mitm/sanitizeHeaders.ts`, `src/mitm/inspector/httpProxyServer.ts`,
// `src/mitm/tproxy/tlsCapture.ts` and `src/app/api/openapi/try/route.ts`
// all already strip them; this list, the canonical one, did not.
"proxy-authenticate",
"proxy-authorization",
"transfer-encoding",
"te",
"trailer",

View File

@@ -39,7 +39,7 @@ export class OutboundUrlGuardError extends Error {
// `http://[::ffff:169.254.169.254]/` reaches these helpers as `::ffff:a9fe:a9fe`.
// Matching the dotted spelling alone therefore misses every mapped address that
// arrives through a parsed URL. Fold the embedded IPv4 back out before deciding.
function mappedIpv4Host(hostname: string): string | null {
export function mappedIpv4Host(hostname: string): string | null {
const normalized = normalizeHost(hostname);
if (!normalized.startsWith("::ffff:")) return null;
const embedded = normalized.slice("::ffff:".length);

View File

@@ -276,7 +276,7 @@ export async function checkAndRefreshToken(provider: string, credentials: any) {
updatedCredentials,
resolveCopilotTokenBaseUrl(provider, updatedCredentials)
);
if (copilotToken) {
if (copilotToken?.token) {
await updateProviderCredentials(updatedCredentials.connectionId, {
providerSpecificData: {
...updatedCredentials.providerSpecificData,
@@ -304,7 +304,7 @@ export async function refreshGitHubAndCopilotTokens(credentials: any) {
const newGitHubCredentials = await refreshGitHubToken(credentials.refreshToken, credentials);
if (newGitHubCredentials?.accessToken) {
const copilotToken = await refreshCopilotToken(newGitHubCredentials.accessToken, credentials);
if (copilotToken) {
if (copilotToken?.token) {
return {
...newGitHubCredentials,
providerSpecificData: {

View File

@@ -8,6 +8,7 @@
// - stripVersion() — strips @version suffix from package keys
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import {
classifyLicense,
@@ -15,15 +16,19 @@ import {
loadAllowlist,
} from "../../../scripts/check/check-licenses.mjs";
const PNPM_WORKSPACE_URL = new URL("../../../pnpm-workspace.yaml", import.meta.url);
// ---------------------------------------------------------------------------
// Helpers — synthetic allowlists for testing classifyLicense in isolation
// ---------------------------------------------------------------------------
function makeAllowlist(overrides: Partial<{
allowed: string[];
allowedExpressions: string[];
exceptions: Record<string, { license: string; justification: string; risk: string }>;
}> = {}) {
function makeAllowlist(
overrides: Partial<{
allowed: string[];
allowedExpressions: string[];
exceptions: Record<string, { license: string; justification: string; risk: string }>;
}> = {}
) {
return {
allowed: ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "0BSD"],
allowedExpressions: ["(MIT OR Apache-2.0)", "MIT AND ISC", "MIT*"],
@@ -32,6 +37,15 @@ function makeAllowlist(overrides: Partial<{
};
}
test("pnpm does not auto-install the unused @lobehub/ui peer subtree", () => {
const workspace = fs.readFileSync(PNPM_WORKSPACE_URL, "utf8");
assert.match(
workspace,
/^autoInstallPeers:\s*false\s*$/m,
"pnpm must match npm's legacy-peer-deps posture; @lobehub/ui is not a runtime dependency"
);
});
// ---------------------------------------------------------------------------
// stripVersion
// ---------------------------------------------------------------------------
@@ -53,7 +67,10 @@ test("stripVersion: handles scoped package without version", () => {
});
test("stripVersion: handles nested scope-like name with version", () => {
assert.equal(stripVersion("@aws-sdk/client-bedrock-runtime@3.1063.0"), "@aws-sdk/client-bedrock-runtime");
assert.equal(
stripVersion("@aws-sdk/client-bedrock-runtime@3.1063.0"),
"@aws-sdk/client-bedrock-runtime"
);
});
// ---------------------------------------------------------------------------
@@ -150,7 +167,10 @@ test("classifyLicense: LGPL package with registered exception returns 'exception
});
const result = classifyLicense("lgpl-native-pkg@1.2.3", "LGPL-3.0-or-later", allowlist);
assert.equal(result.status, "exception");
assert.ok(result.reason.includes("exception"), `reason should mention exception: ${result.reason}`);
assert.ok(
result.reason.includes("exception"),
`reason should mention exception: ${result.reason}`
);
});
test("classifyLicense: scoped package with exception: version is stripped for lookup", () => {

View File

@@ -127,3 +127,20 @@ test("scoped layout runs a CJS server.js and an ESM worker.js side by side", ()
rmSync(root, { recursive: true, force: true });
}
});
test("colocate-standalone bundles the required compression worker", () => {
const root = mkdtempSync(join(tmpdir(), "colocate-compression-worker-"));
try {
writeFileSync(join(root, "server.js"), "module.exports = {};\n");
execFileSync(process.execPath, ["scripts/build/colocate-standalone.mjs"], {
cwd: join(import.meta.dirname, "..", "..", ".."),
env: { ...process.env, OMNIROUTE_STANDALONE_DIR: root },
stdio: "pipe",
});
const workerDir = join(root, "open-sse", "services", "compression");
assert.equal(existsSync(join(workerDir, "compressionWorker.js")), true);
assert.equal(JSON.parse(readFileSync(join(workerDir, "package.json"), "utf8")).type, "module");
} finally {
rmSync(root, { recursive: true, force: true });
}
});

View File

@@ -4,41 +4,33 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
type CoreModule = typeof import("../../src/lib/db/core.ts");
// Single shared tempDir for all tests — DATA_DIR/SQLITE_FILE are module-level consts
// resolved once at first import, so we must create the temp dir and set DATA_DIR
// BEFORE importing core.ts.
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-test-"));
const originalDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = tempDir;
// Shared across all tests — the module caches DATA_DIR / SQLITE_FILE at load time,
// so we must create the temp dir and import exactly once.
type CoreModule = typeof import("../../src/lib/db/core.ts");
let tempDir: string;
let originalDataDir: string | undefined;
let getDbInstance: CoreModule["getDbInstance"];
let resetDbInstance: CoreModule["resetDbInstance"];
let ensureDbInitialized: CoreModule["ensureDbInitialized"];
let closeDbInstance: CoreModule["closeDbInstance"];
// Import resetDbInstance ONCE at the top with the same ESM specifier the tests use,
// so cleanup() operates on the real singleton (not a stale CJS require).
// This is the FIRST import of core.ts, so DATA_DIR resolves to our tempDir.
import {
getDbInstance,
resetDbInstance,
ensureDbInitialized,
closeDbInstance,
} from "../../src/lib/db/core.ts";
before(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-db-test-"));
originalDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = tempDir;
const core = await import("../../src/lib/db/core.ts");
getDbInstance = core.getDbInstance;
resetDbInstance = core.resetDbInstance;
ensureDbInitialized = core.ensureDbInitialized;
closeDbInstance = core.closeDbInstance;
// Clear any singleton left by a previous test file in the same shard
// Clear any singleton left by a previous test file in the same shard.
closeDbInstance();
// Create a fresh DB in the temp dir (handles async driver initialization)
// Create a fresh DB in the temp dir (handles async driver initialization).
await ensureDbInitialized();
});
after(() => {
try {
resetDbInstance();
} catch {
// ignore
}
// Let reset errors surface — no silent swallowing.
resetDbInstance();
if (originalDataDir !== undefined) {
process.env.DATA_DIR = originalDataDir;
} else {
@@ -90,9 +82,9 @@ test("getDbInstance creates tables from SCHEMA_SQL (proves initialization succee
// The preservedCriticalState sentinel is captureSucceeded: true on fresh DB
// (no existing file = no corruption path = initialized with default sentinel).
// Verify this indirectly: the DB is fully functional and migrations ran.
const migrationCount = db
.prepare("SELECT COUNT(*) as c FROM _omniroute_migrations")
.get() as { c: number };
const migrationCount = db.prepare("SELECT COUNT(*) as c FROM _omniroute_migrations").get() as {
c: number;
};
assert.ok(migrationCount.c >= 1, "at least one migration should be recorded");
});
@@ -142,12 +134,14 @@ test("resetDbInstance clears the singleton so next call creates a new DB", async
// Write a marker row so we can prove the post-reset handle reopens the same
// on-disk file through a freshly opened connection (not the cached one).
db1.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
"reset_ns",
"marker",
JSON.stringify({ v: 1 })
);
db1
.prepare("INSERT INTO key_value (namespace, key, value) VALUES (?, ?, ?)")
.run("reset_ns", "marker", JSON.stringify({ v: 1 }));
// Close the previous handle explicitly before resetting, so the file descriptor
// is released before the next reopen (POSIX allows open fds to survive fs.rmSync,
// but we want honest isolation, not accidental survival).
closeDbInstance();
resetDbInstance();
// Re-initialize after reset — drivers may need async pre-init (sql.js WASM)
@@ -169,19 +163,14 @@ test("getDbInstance sets WAL journal mode", async () => {
const db = getDbInstance();
const mode = db.pragma("journal_mode", { simple: true }) as string;
assert.equal(
String(mode).toLowerCase(),
"wal",
"on-disk DB should open in WAL journal mode"
);
assert.equal(String(mode).toLowerCase(), "wal", "on-disk DB should open in WAL journal mode");
});
test("getDbInstance stores schema_version in db_meta", async () => {
const db = getDbInstance();
const row = db
.prepare("SELECT value FROM db_meta WHERE key = 'schema_version'")
.get() as { value: string } | undefined;
const row = db.prepare("SELECT value FROM db_meta WHERE key = 'schema_version'").get() as
{ value: string } | undefined;
assert.ok(row, "db_meta should hold a schema_version row after init");
assert.equal(row.value, "1", "schema_version should be seeded to '1'");
});

View File

@@ -0,0 +1,161 @@
import assert from "node:assert/strict";
import { after, describe, it } from "node:test";
import {
isCompressionWorkerEligible,
isStrictlySerializable,
} from "../../../open-sse/services/compression/compressionWorkerProtocol.ts";
import {
closeCompressionWorkerPoolForTests,
CompressionWorkerPool,
} from "../../../open-sse/services/compression/compressionWorkerPool.ts";
import {
applyCompression,
applyCompressionAsync,
} from "../../../open-sse/services/compression/strategySelector.ts";
import type { CompressionConfig } from "../../../open-sse/services/compression/types.ts";
const body = {
model: "gpt-test",
messages: [
{ role: "system", content: "Answer accurately." },
{
role: "user",
content:
"Please basically actually simply carefully help with this very important task. ".repeat(
80
),
},
],
};
const config = {
enabled: true,
defaultMode: "stacked",
autoTriggerTokens: 1,
cacheMinutes: 0,
preserveSystemPrompt: true,
stackedPipeline: [{ engine: "rtk" }, { engine: "caveman" }],
} as CompressionConfig;
function comparable<T extends { stats: { durationMs?: number; timestamp: number } | null }>(
result: T
) {
if (!result.stats) return result;
const {
durationMs: _duration,
timestamp: _timestamp,
engineBreakdown,
...stats
} = result.stats as T["stats"] & {
engineBreakdown?: Array<Record<string, unknown>>;
};
const stableBreakdown = engineBreakdown?.map(({ durationMs: _stepDuration, ...step }) => step);
return {
...result,
stats: {
...stats,
...(stableBreakdown ? { engineBreakdown: stableBreakdown } : {}),
},
};
}
after(() => closeCompressionWorkerPoolForTests());
describe("compression worker eligibility", () => {
it("accepts only standard, rtk, and approved rtk+caveman stacks", () => {
assert.equal(isCompressionWorkerEligible(body, "standard", { config }), true);
assert.equal(isCompressionWorkerEligible(body, "rtk", { config }), true);
assert.equal(isCompressionWorkerEligible(body, "stacked", { config }), true);
for (const mode of ["off", "lite", "aggressive", "ultra", "omniglyph"] as const) {
assert.equal(isCompressionWorkerEligible(body, mode, { config }), false);
}
for (const engine of ["llmlingua", "omniglyph", "ccr", "session-dedup", "ultra"]) {
assert.equal(
isCompressionWorkerEligible(body, "stacked", {
config: { ...config, stackedPipeline: [{ engine }] } as CompressionConfig,
}),
false
);
}
});
it("rejects functions, symbols, classes, special objects, cycles, and non-finite numbers", () => {
for (const value of [
() => undefined,
Symbol("x"),
new Date(),
new Map(),
new Set(),
/x/,
NaN,
Infinity,
]) {
assert.equal(isStrictlySerializable(value), false);
}
const cyclic: Record<string, unknown> = {};
cyclic.self = cyclic;
assert.equal(isStrictlySerializable(cyclic), false);
});
});
describe("compression worker execution", () => {
it("matches the synchronous body and stats except timing fields", async () => {
const sync = applyCompression(body, "stacked", { config });
const async = await applyCompressionAsync(body, "stacked", { config });
assert.deepEqual(comparable(async), comparable(sync));
});
it("preserves Responses bodies and hard-budget results", async () => {
const responsesBody = {
model: "gpt-test",
input: [{ role: "user", content: [{ type: "input_text", text: "word ".repeat(600) }] }],
};
const hardBudgetConfig = { ...config, targetTokens: 100 };
const sync = applyCompression(responsesBody, "stacked", { config: hardBudgetConfig });
const async = await applyCompressionAsync(responsesBody, "stacked", {
config: hardBudgetConfig,
});
assert.deepEqual(comparable(async), comparable(sync));
});
it("relays per-engine progress from the worker", async () => {
const steps: string[] = [];
await applyCompressionAsync(body, "stacked", {
config,
onEngineStep: (step) => steps.push(step.engine),
});
assert.deepEqual(steps, ["rtk", "caveman"]);
});
it("fails open without inline compression when a job times out", async () => {
const pool = new CompressionWorkerPool({ size: 1, timeoutMs: 1, idleMs: 100 });
try {
const result = await pool.run(body, "stacked", { config });
assert.deepEqual(result, { body, compressed: false, stats: null });
} finally {
await pool.close();
}
});
it("keeps the parent event loop responsive while two workers overlap", async () => {
const largeBody = {
messages: Array.from({ length: 400 }, (_, index) => ({
role: "user",
content: `message ${index} ` + "basically actually simply ".repeat(400),
})),
};
let ticked = false;
const tick = new Promise<void>((resolve) =>
setTimeout(() => {
ticked = true;
resolve();
}, 0)
);
const jobs = Promise.all([
applyCompressionAsync(largeBody, "standard", { config }),
applyCompressionAsync(largeBody, "standard", { config }),
]);
await tick;
assert.equal(ticked, true);
await jobs;
});
});

View File

@@ -0,0 +1,165 @@
import test from "node:test";
import assert from "node:assert/strict";
import { Buffer } from "node:buffer";
const { AUDIO_SPEECH_PROVIDERS, parseSpeechModel } =
await import("../../open-sse/config/audioRegistry.ts");
const { geminiGenerateSpeech } = await import("../../open-sse/executors/geminiTts.ts");
const { handleAudioSpeech } = await import("../../open-sse/handlers/audioSpeech.ts");
test("Google Gemini TTS models parse publicly and remap to Gemini credentials", () => {
assert.deepEqual(parseSpeechModel("google/gemini-2.5-flash-preview-tts"), {
provider: "google",
model: "gemini-2.5-flash-preview-tts",
});
assert.equal(AUDIO_SPEECH_PROVIDERS.google.credentialProviderId, "gemini");
assert.deepEqual(
AUDIO_SPEECH_PROVIDERS.google.models.map(({ id }) => id),
["gemini-3.1-flash-tts-preview", "gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"]
);
});
test("geminiGenerateSpeech sends the exact AI Studio generateContent contract and wraps PCM", async () => {
const originalFetch = globalThis.fetch;
const pcm = Buffer.from([1, 2, 3, 4]);
let captured: { url: string; init: RequestInit } | undefined;
globalThis.fetch = async (input, init = {}) => {
captured = { url: String(input), init };
return Response.json({
candidates: [
{
content: {
parts: [
{
inlineData: {
data: pcm.toString("base64"),
mimeType: "audio/L16;codec=pcm;rate=16000",
},
},
],
},
},
],
});
};
try {
const wav = await geminiGenerateSpeech(
{ apiKey: "gemini-key" },
{ model: "gemini-2.5-flash-preview-tts", text: "Hello", voice: "Kore" }
);
assert.equal(
captured?.url,
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent"
);
assert.equal(
(captured?.init.headers as Record<string, string>)["Content-Type"],
"application/json"
);
assert.equal(
(captured?.init.headers as Record<string, string>)["x-goog-api-key"],
"gemini-key"
);
assert.equal((captured?.init.headers as Record<string, string>).Authorization, undefined);
assert.deepEqual(JSON.parse(String(captured?.init.body)), {
contents: [{ parts: [{ text: "Hello" }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: { prebuiltVoiceConfig: { voiceName: "Kore" } },
},
},
});
assert.equal(wav.subarray(0, 4).toString("ascii"), "RIFF");
assert.equal(wav.readUInt32LE(24), 16000);
assert.deepEqual(wav.subarray(44), pcm);
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleAudioSpeech returns WAV and defaults the AI Studio voice to Kore", async () => {
const originalFetch = globalThis.fetch;
let payload: {
generationConfig: {
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: string } } };
};
};
globalThis.fetch = async (_input, init = {}) => {
payload = JSON.parse(String(init.body));
return Response.json({
candidates: [
{
content: {
parts: [
{
inlineData: {
data: Buffer.from([5, 6]).toString("base64"),
mimeType: "audio/L16;rate=24000",
},
},
],
},
},
],
});
};
try {
const response = await handleAudioSpeech({
body: {
model: "google/gemini-2.5-pro-preview-tts",
input: "Speak",
},
credentials: { apiKey: "gemini-key" },
});
assert.equal(response.status, 200);
assert.equal(response.headers.get("content-type"), "audio/wav");
assert.equal(
payload.generationConfig.speechConfig.voiceConfig.prebuiltVoiceConfig.voiceName,
"Kore"
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleAudioSpeech rejects an AI Studio response without audio", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => Response.json({ candidates: [{ content: { parts: [] } }] });
try {
const response = await handleAudioSpeech({
body: {
model: "google/gemini-2.5-flash-preview-tts",
input: "Silent",
},
credentials: { apiKey: "gemini-key" },
});
const payload = (await response.json()) as { error: { message: string } };
assert.equal(response.status, 500);
assert.equal(
payload.error.message,
"Speech request failed: Gemini TTS response did not contain audio data"
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleAudioSpeech preserves AI Studio upstream errors", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
Response.json({ error: { message: "quota exhausted" } }, { status: 429 });
try {
const response = await handleAudioSpeech({
body: {
model: "google/gemini-2.5-flash-preview-tts",
input: "Limited",
},
credentials: { apiKey: "gemini-key" },
});
const payload = (await response.json()) as { error: { message: string } };
assert.equal(response.status, 429);
assert.equal(payload.error.message, "quota exhausted");
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -0,0 +1,94 @@
// A translation that drops a placeholder silently loses the value it carried:
// the string still renders, just without the number, path or command the
// English copy promised. Nothing checked for that, and three strings had
// drifted (all in `pt`):
//
// a2aDashboard.smokeStreamSuccessWithTask lost {stateSuffix}
// agents.opencodeDesc lost {command}
// cache.cacheHitsSub lost {total} ("of {total} total" -> "Acertos")
//
// Placeholder sets are compared, not counts or order: a locale may reorder or
// repeat them, but it may not introduce one English never defined (it would
// render literally) or drop one (its value disappears).
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const messagesDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"src",
"i18n",
"messages"
);
type Json = { [key: string]: string | Json };
function loadLocale(file: string): Json {
return JSON.parse(readFileSync(path.join(messagesDir, file), "utf8")) as Json;
}
function flatten(value: Json, prefix = ""): Map<string, string> {
const out = new Map<string, string>();
for (const [key, child] of Object.entries(value)) {
const dotted = prefix ? `${prefix}.${key}` : key;
if (typeof child === "string") out.set(dotted, child);
else if (child && typeof child === "object") {
for (const [k, v] of flatten(child, dotted)) out.set(k, v);
}
}
return out;
}
/**
* Names an ICU message interpolates: `{name}` and the argument of a typed
* placeholder such as `{count, plural, ...}`. Nested sub-messages are covered
* because the scan is a plain sweep of the whole string.
*/
function placeholders(message: string): Set<string> {
return new Set(
[...message.matchAll(/\{\s*([a-zA-Z0-9_]+)\s*[,}]/g)].map((match) => match[1])
);
}
const english = flatten(loadLocale("en.json"));
const locales = readdirSync(messagesDir)
.filter((file) => file.endsWith(".json") && file !== "en.json")
.sort();
test("every locale keeps the placeholders its English source defines", () => {
const drift: string[] = [];
for (const file of locales) {
for (const [key, translated] of flatten(loadLocale(file))) {
const source = english.get(key);
if (typeof source !== "string") continue;
const expected = placeholders(source);
const actual = placeholders(translated);
const missing = [...expected].filter((name) => !actual.has(name));
const unknown = [...actual].filter((name) => !expected.has(name));
if (missing.length === 0 && unknown.length === 0) continue;
drift.push(
`${file} ${key}\n` +
` en: ${source}\n` +
` ${file.replace(".json", "")}: ${translated}\n` +
` missing=[${missing.join(", ")}] unknown=[${unknown.join(", ")}]`
);
}
}
assert.deepEqual(drift, [], `\n placeholder drift:\n ${drift.join("\n ")}\n`);
});
test("the checker itself recognises the drift it is meant to catch", () => {
// Without this the test above could pass by never matching anything.
assert.deepEqual([...placeholders("of {total} total")], ["total"]);
assert.deepEqual([...placeholders("ok (task {taskId}{stateSuffix}).")], ["taskId", "stateSuffix"]);
assert.deepEqual([...placeholders("{count, plural, one {# item} other {# items}}")], ["count"]);
assert.deepEqual([...placeholders("Acertos")], []);
});

View File

@@ -103,7 +103,7 @@ function resolveLiveKieMarketCatalog() {
}));
}
test("KIE Market resolver changes exactly one id in the live market catalog", () => {
test("KIE Market resolver changes exactly the 4 google-imagen ids in the live market catalog", () => {
const roundTrips = resolveLiveKieMarketCatalog();
const changed = roundTrips.filter(({ publicModelId, upstreamModelId }) => {
return upstreamModelId !== publicModelId;
@@ -114,12 +114,31 @@ test("KIE Market resolver changes exactly one id in the live market catalog", ()
publicModelId: "google-imagen/nano-banana-2",
upstreamModelId: "nano-banana-2",
},
{
publicModelId: "google-imagen/nano-banana",
upstreamModelId: "google/nano-banana",
},
{
publicModelId: "google-imagen/nano-banana-pro",
upstreamModelId: "nano-banana-pro",
},
{
publicModelId: "google-imagen/nano-banana-edit",
upstreamModelId: "google/nano-banana-edit",
},
]);
});
const REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS = new Set([
"google-imagen/nano-banana",
"google-imagen/nano-banana-2",
"google-imagen/nano-banana-pro",
"google-imagen/nano-banana-edit",
]);
test("KIE Market resolver preserves every other live market catalog id byte-identically", () => {
for (const { publicModelId, upstreamModelId } of resolveLiveKieMarketCatalog()) {
if (publicModelId !== "google-imagen/nano-banana-2") {
if (!REWRITTEN_GOOGLE_IMAGEN_MARKET_IDS.has(publicModelId)) {
assert.equal(
upstreamModelId,
publicModelId,
@@ -129,8 +148,8 @@ test("KIE Market resolver preserves every other live market catalog id byte-iden
}
});
test("KIE Market resolver keeps exactly one explicit upstream id mapping", () => {
assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 1);
test("KIE Market resolver keeps exactly the explicit google-imagen upstream id mappings (#11296)", () => {
assert.equal(KIE_MARKET_UPSTREAM_MODEL_IDS.size, 4);
});
test("KIE Market resolver passes an unknown namespaced id through byte-identically", () => {
@@ -160,6 +179,36 @@ test("KIE Market createTask sends the bare upstream model id for Nano Banana 2 (
assert.equal(captured.result.data.data[0].url, "https://example.com/kie-market-image.png");
});
test("KIE Market createTask sends the KIE upstream id for Nano Banana (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/google-imagen/nano-banana");
assert.equal(
captured.create.body.model,
"google/nano-banana",
"KIE Market createTask must send the KIE-documented google/nano-banana upstream id"
);
});
test("KIE Market createTask sends the bare upstream model id for Nano Banana Pro (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/google-imagen/nano-banana-pro");
assert.equal(
captured.create.body.model,
"nano-banana-pro",
"KIE Market createTask must send the KIE-documented nano-banana-pro upstream id"
);
});
test("KIE Market createTask sends the KIE upstream id for Nano Banana Edit (#11296)", async () => {
const captured = await runKieMarketGeneration("kie/google-imagen/nano-banana-edit");
assert.equal(
captured.create.body.model,
"google/nano-banana-edit",
"KIE Market createTask must send the KIE-documented google/nano-banana-edit upstream id"
);
});
test("KIE Market createTask leaves genuinely namespaced upstream ids untouched (#11225 control)", async () => {
const captured = await runKieMarketGeneration("kie/seedream/4.5-text-to-image");

View File

@@ -38,6 +38,99 @@ async function resetStorage() {
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test("GitHub access-token health demotes only a verified 401 and stores no secrets", async () => {
for (const status of [200, 401, 403, 429, 500]) {
await resetStorage();
const accessToken = `ghp_status_${status}_secret`;
const responseSecret = `response-${status}-secret`;
const originalFetch = globalThis.fetch;
const consoleOutput: unknown[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => consoleOutput.push(args);
globalThis.fetch = (async () =>
status === 200
? new Response(
JSON.stringify({
token: `copilot-${status}-secret`,
expires_at: Math.floor(Date.now() / 1000) + 1800,
}),
{ status, headers: { "content-type": "application/json" } }
)
: new Response(responseSecret, { status })) as typeof fetch;
try {
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: `GitHub ${status}`,
accessToken,
healthCheckInterval: 60,
isActive: true,
testStatus: "active",
providerSpecificData: {
copilotToken: "existing-copilot-secret",
copilotTokenExpiresAt: Math.floor(Date.now() / 1000) + 3600,
},
});
await tokenHealthCheck.checkConnection({
...connection,
lastHealthCheckAt: new Date(Date.now() - 61 * 60 * 1000).toISOString(),
});
const updated = await providersDb.getProviderConnectionById(connection.id);
assert.equal(updated?.testStatus, status === 401 ? "expired" : "active");
assert.equal(updated?.lastHealthCheckAt !== connection.lastHealthCheckAt, true);
assert.equal(JSON.stringify(updated).includes(responseSecret), false);
assert.equal(JSON.stringify(consoleOutput).includes(accessToken), false);
assert.equal(JSON.stringify(consoleOutput).includes(responseSecret), false);
if (status === 401) {
assert.equal(updated?.errorCode, "github_access_token_invalid");
assert.equal(updated?.lastErrorType, "github_access_token_invalid");
assert.equal(updated?.lastErrorSource, "oauth");
}
} finally {
globalThis.fetch = originalFetch;
console.error = originalError;
}
}
});
test("GitHub access-token health keeps network failures active", async () => {
await resetStorage();
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => {
throw new Error("network down");
}) as typeof fetch;
try {
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: "GitHub network",
accessToken: "ghp_network_secret",
healthCheckInterval: 60,
isActive: true,
testStatus: "active",
providerSpecificData: {
copilotToken: "existing-copilot-secret",
copilotTokenExpiresAt: Math.floor(Date.now() / 1000) + 3600,
},
});
await tokenHealthCheck.checkConnection({
...connection,
lastHealthCheckAt: new Date(Date.now() - 61 * 60 * 1000).toISOString(),
});
const updated = await providersDb.getProviderConnectionById(connection.id);
assert.equal(updated?.testStatus, "active");
assert.equal(updated?.lastHealthCheckAt !== connection.lastHealthCheckAt, true);
} finally {
globalThis.fetch = originalFetch;
}
});
async function withHttpServer(handler, fn) {
const server = http.createServer(handler);

View File

@@ -124,55 +124,81 @@ test("checkConnection leaves a non-refresh provider with no refresh token untouc
test("checkConnection keeps GitHub Copilot access-token-only connections active", async () => {
await resetStorage();
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(
JSON.stringify({
token: "verified-copilot-token",
expires_at: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
}),
{ status: 200, headers: { "content-type": "application/json" } }
)) as typeof fetch;
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: "GitHub Access Token Account",
accessToken: "github-access-token",
refreshToken: null,
providerSpecificData: {
copilotToken: "copilot-token",
copilotTokenExpiresAt: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
},
testStatus: "active",
isActive: true,
});
try {
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: "GitHub Access Token Account",
accessToken: "github-access-token",
refreshToken: null,
providerSpecificData: {
copilotToken: "copilot-token",
copilotTokenExpiresAt: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
},
testStatus: "active",
isActive: true,
});
await tokenHealthCheck.checkConnection(connection);
await tokenHealthCheck.checkConnection(connection);
const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection));
assert.equal(updated?.testStatus, "active");
assert.notEqual(updated?.errorCode, "no_refresh_token");
assert.ok(updated?.lastHealthCheckAt);
const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection));
assert.equal(updated?.testStatus, "active");
assert.notEqual(updated?.errorCode, "no_refresh_token");
assert.ok(updated?.lastHealthCheckAt);
} finally {
globalThis.fetch = originalFetch;
}
});
test("checkConnection clears stale no_refresh_token state for usable GitHub Copilot connections", async () => {
await resetStorage();
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(
JSON.stringify({
token: "verified-copilot-token",
expires_at: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
}),
{ status: 200, headers: { "content-type": "application/json" } }
)) as typeof fetch;
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: "GitHub False Expired Account",
accessToken: "github-access-token",
refreshToken: null,
providerSpecificData: {
copilotToken: "copilot-token",
copilotTokenExpiresAt: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
},
testStatus: "expired",
errorCode: "no_refresh_token",
lastError: "No refresh token available — re-authenticate this account.",
isActive: true,
});
try {
const connection = await providersDb.createProviderConnection({
provider: "github",
authType: "oauth",
name: "GitHub False Expired Account",
accessToken: "github-access-token",
refreshToken: null,
providerSpecificData: {
copilotToken: "copilot-token",
copilotTokenExpiresAt: Math.floor((Date.now() + 60 * 60 * 1000) / 1000),
},
testStatus: "expired",
errorCode: "no_refresh_token",
lastError: "No refresh token available — re-authenticate this account.",
isActive: true,
});
await tokenHealthCheck.checkConnection(connection);
await tokenHealthCheck.checkConnection(connection);
const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection));
assert.equal(updated?.testStatus, "active");
assert.equal(updated?.errorCode ?? null, null);
assert.equal(updated?.lastError ?? null, null);
assert.ok(updated?.lastHealthCheckAt);
const updated = await providersDb.getProviderConnectionById(getCreatedConnectionId(connection));
assert.equal(updated?.testStatus, "active");
assert.equal(updated?.errorCode ?? null, null);
assert.equal(updated?.lastError ?? null, null);
assert.ok(updated?.lastHealthCheckAt);
} finally {
globalThis.fetch = originalFetch;
}
});
// Boundary regression for #8182 vs #5326: the terminal-skip guard added by #8182

View File

@@ -739,6 +739,35 @@ test("refreshCopilotToken returns the short-lived copilot token", async () => {
assert.equal(calls[0].options.headers.Authorization, "token github-access-token");
});
test("refreshCopilotToken reports HTTP outcomes without logging response bodies", async () => {
const secret = "ghp_never-log-this";
const responseBody = `credential ${secret} rejected`;
for (const status of [401, 403, 429, 500]) {
const log = createLog();
const result = await withMockedFetch(
async () => textResponse(responseBody, status),
() => refreshCopilotToken(secret, log)
);
assert.deepEqual(result, { status });
assert.equal(JSON.stringify(log.entries).includes(secret), false);
assert.equal(JSON.stringify(log.entries).includes(responseBody), false);
}
});
test("refreshCopilotToken distinguishes network failures from HTTP failures", async () => {
const log = createLog();
const result = await withMockedFetch(
async () => {
throw new Error("socket closed");
},
() => refreshCopilotToken("ghp_network-test", log)
);
assert.deepEqual(result, { status: null });
});
test("supportsTokenRefresh, isUnrecoverableRefreshError and formatProviderCredentials cover provider helpers", async () => {
const log = createLog();

View File

@@ -0,0 +1,66 @@
// `FORBIDDEN` in src/shared/constants/upstreamHeaders.ts is documented as the
// hop-by-hop / Host / framing denylist, and it was missing two of the RFC 7230
// §6.1 names. Measured before the fix:
//
// proxy-authorization upstream=allow custom=allow
// proxy-authenticate upstream=allow custom=allow
// proxy-connection upstream=BLOCK custom=BLOCK
//
// `proxy-authorization` is the one that costs something: it authenticates the
// hop to the operator's own proxy, so forwarding it hands that credential to
// the model provider. Five other modules in this repo already strip it
// (reverseProxy HOP_BY_HOP, mitm/sanitizeHeaders, inspector/httpProxyServer,
// tproxy/tlsCapture, openapi/try) — the canonical list did not.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
isForbiddenUpstreamHeaderName,
isForbiddenCustomHeaderName,
} from "../../src/shared/constants/upstreamHeaders.ts";
import { HOP_BY_HOP } from "../../src/lib/services/reverseProxy.ts";
import { sanitizeUpstreamHeadersMap } from "../../src/lib/db/models.ts";
test("proxy-authorization and proxy-authenticate are refused", () => {
for (const name of ["proxy-authorization", "proxy-authenticate"]) {
assert.equal(isForbiddenUpstreamHeaderName(name), true, name);
assert.equal(isForbiddenCustomHeaderName(name), true, name);
}
});
test("the refusal is case-insensitive, like every other name in the list", () => {
for (const name of ["Proxy-Authorization", "PROXY-AUTHENTICATE", " Proxy-Authorization "]) {
assert.equal(isForbiddenUpstreamHeaderName(name), true, name);
}
});
test("sanitizeUpstreamHeadersMap drops them and keeps the rest", () => {
const out = sanitizeUpstreamHeadersMap({
"Proxy-Authorization": "Basic c2VjcmV0",
"Proxy-Authenticate": "Basic realm=x",
"X-Custom": "ok",
});
assert.deepEqual(out, { "X-Custom": "ok" });
});
test("the canonical list now covers every hop-by-hop name reverseProxy strips", () => {
// `reverseProxy.HOP_BY_HOP` is the repo's own RFC 7230 §6.1 list. The two
// lists drifting apart is what this fix repairs, so compare them directly —
// `trailers` is the TE token, spelled `trailer` as a header name.
const missing = [...HOP_BY_HOP]
.map((name) => (name === "trailers" ? "trailer" : name))
.filter((name) => !isForbiddenUpstreamHeaderName(name));
assert.deepEqual(missing, []);
});
test("ordinary headers are still allowed", () => {
for (const name of ["x-custom", "x-forwarded-for", "user-agent", "accept"]) {
assert.equal(isForbiddenUpstreamHeaderName(name), false, name);
}
// Auth headers stay allowed as *upstream* headers (the credential layer owns
// them) while remaining forbidden as operator-supplied custom headers.
assert.equal(isForbiddenUpstreamHeaderName("authorization"), false);
assert.equal(isForbiddenCustomHeaderName("authorization"), true);
});

View File

@@ -0,0 +1,115 @@
// `validateProxyUrl()` refused a private/metadata proxy target by matching
// dotted-quad prefixes, so the same address in another spelling walked through.
// Measured on release/v3.8.50 (ac02c5b42):
//
// http://169.254.169.254 -> blocked
// http://[::ffff:169.254.169.254] -> ALLOWED (same address, mapped)
// http://[::ffff:a9fe:a9fe] -> ALLOWED (how WHATWG URL serialises it)
// http://[::ffff:10.0.0.5] -> ALLOWED
// http://[fd00::1] -> ALLOWED (ULA)
// http://[fe80::1] -> ALLOWED (link-local)
// http://100.64.0.1 -> ALLOWED (CGNAT)
//
// #10843 fixed this class in the shared outbound guard; this module kept a
// private copy of the classification and did not get the fix.
import test from "node:test";
import assert from "node:assert/strict";
import { validateProxyUrl } from "../../src/lib/db/upstreamProxy.ts";
function isValid(url: string): boolean {
return validateProxyUrl(url).valid;
}
test("a mapped-IPv4 spelling of a blocked address is blocked too", () => {
for (const url of [
"http://[::ffff:169.254.169.254]", // cloud metadata, mapped
"http://[::ffff:a9fe:a9fe]", // the same, as WHATWG URL serialises it
"http://[::ffff:10.0.0.5]", // RFC1918, mapped
"http://[::ffff:192.168.1.1]",
"http://[::ffff:172.16.0.1]",
]) {
assert.equal(isValid(url), false, `${url} must be refused`);
}
});
test("private IPv6 ranges are blocked", () => {
for (const url of ["http://[fd00::1]", "http://[fc00::1]", "http://[fe80::1]"]) {
assert.equal(isValid(url), false, `${url} must be refused`);
}
});
test("CGNAT space is blocked", () => {
// 100.64.0.0/10 is carrier-grade NAT, not public address space.
assert.equal(isValid("http://100.64.0.1"), false);
assert.equal(isValid("http://100.127.255.254"), false);
// …but the neighbouring public /8 addresses are not.
assert.equal(isValid("http://100.63.255.255"), true);
assert.equal(isValid("http://100.128.0.1"), true);
});
test("every address the dotted rules already refused is still refused", () => {
for (const url of [
"http://169.254.169.254",
"http://metadata.google.internal",
"http://metadata.aws.internal",
"http://10.0.0.5",
"http://172.16.0.1",
"http://172.31.255.255",
"http://192.168.1.1",
"http://0.0.0.0",
"http://127.0.0.2",
"http://224.0.0.1", // IPv4 multicast, the only octet the old rule covered
]) {
assert.equal(isValid(url), false, `${url} must still be refused`);
}
});
test("multicast is refused across the whole /4, not just 224/8", () => {
// Widened on purpose, and the one deliberate behaviour change here beyond
// the spelling fix: the old rule was `/^224\./`, so 225239 were accepted.
// None of 224.0.0.0/4 can be a proxy.
for (const url of ["http://224.0.0.1", "http://231.7.7.7", "http://239.255.255.250"]) {
assert.equal(isValid(url), false, `${url} must be refused`);
}
assert.equal(isValid("http://240.0.0.1"), true, "just outside the /4 is unchanged");
});
test("loopback stays allowed — CLIProxyAPI runs on localhost:8317", () => {
for (const url of [
"http://localhost:8317",
"http://127.0.0.1:8317",
"http://[::1]:8317",
// Judging the address rather than its spelling cuts both ways: the mapped
// form of 127.0.0.1 is the same host the exception exists for.
"http://[::ffff:127.0.0.1]:8317",
]) {
assert.equal(isValid(url), true, `${url} must stay allowed`);
}
});
test("ordinary public proxies stay allowed", () => {
for (const url of [
"http://proxy.example.com",
"https://proxy.example.com:3128",
"http://8.8.8.8:3128",
"http://[2606:4700::1111]",
"http://172.32.0.1", // just outside 172.16.0.0/12
"http://192.169.0.1", // just outside 192.168.0.0/16
]) {
assert.equal(isValid(url), true, `${url} must stay allowed`);
}
});
test("the non-host validations are unchanged", () => {
assert.deepEqual(validateProxyUrl("https://proxy.example.com"), {
valid: true,
url: "https://proxy.example.com",
});
assert.equal(validateProxyUrl("ftp://proxy.example.com").valid, false);
assert.match(String(validateProxyUrl("not-a-url").error), /Invalid URL/);
assert.match(
String(validateProxyUrl("http://169.254.169.254").error),
/private\/internal address/
);
});

View File

@@ -15,6 +15,8 @@ const {
extractCiGates,
FULL_CI_SKIP,
fullCiTimeoutFor,
curatedEquivalentId,
fullCiKindFor,
} = mod;
const extract = extractCiGates as (
@@ -361,3 +363,117 @@ test("extractCiGates: the REAL ci.yml yields the base-reds that leaked in v3.8.4
}
assert.ok(ids.size >= 20, "the real gate set is substantial (>= 20 static gates)");
});
// ─── Verdict accuracy (review of the #9985 release-green verdict) ────────────
test("firstFailureLine never blames a PASSING line whose test FILE NAME contains 'fail' (#9985)", () => {
// Observed in the 2026-08-23 verdict: the reported "cause" of the unit red was
// ✓ …fail-fast-concurrency-gate.test.ts (4 tests) 203ms
// i.e. a GREEN line, matched only because the unanchored /FAIL/i marker hit the
// substring "fail" inside the file name. The real ✖ line was three lines below.
const out = [
"> omniroute@3.8.50 test:unit",
" ✓ tests/unit/runtime/fail-fast-concurrency-gate.test.ts (4 tests) 203ms",
" ✓ tests/unit/router/failover-budget.test.ts (9 tests) 41ms",
" ✖ tests/unit/router/pricing.test.ts > picks the cheapest candidate",
"AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 2 !== 3",
].join("\n");
const hit = firstFailureLine(out);
assert.doesNotMatch(hit, /fail-fast-concurrency-gate/, "a green line is never the failure cause");
assert.doesNotMatch(hit, /failover-budget/, "a green line is never the failure cause");
assert.match(hit, /pricing\.test\.ts/, "the real failing line must be reported instead");
});
test("firstFailureLine still recognises every legitimate failure marker", () => {
const cases: [string, RegExp][] = [
["ok 1 - warms up\nnot ok 2 - routes to the cheapest key\n", /not ok 2/],
["Test Files 1 failed\nFAIL tests/unit/router/pricing.test.ts\n", /^FAIL /],
["src/x.ts(10,5): error TS2322: Type 'string' is not assignable.", /error TS2322/],
["✗ db-rules: raw sqlite handle left open", /db-rules/],
["Error: ENOENT: no such file or directory, open 'dist/server.js'", /ENOENT/],
["[cognitive-complexity] REGRESSÃO — 801 violações > baseline 797", /REGRESS/],
["[file-size] REGRESSED: open-sse/router.ts 1204 > cap 1100", /REGRESSED/],
];
for (const [out, expected] of cases) {
assert.match(firstFailureLine(out), expected, `marker lost for: ${out.slice(0, 40)}`);
}
});
test("firstFailureLine falls back to the last line when nothing matches", () => {
assert.equal(firstFailureLine("warming up\nall quiet\n"), "all quiet");
assert.equal(firstFailureLine(""), "failed");
});
test("curatedEquivalentId maps a ci.yml gate script onto the curated pass id (#9985)", () => {
assert.equal(curatedEquivalentId("check:file-size"), "file-size");
assert.equal(curatedEquivalentId("check:compression-budget"), "compression-budget");
// Curated ids that are NOT just the script name minus "check:".
assert.equal(curatedEquivalentId("check:workflows"), "workflow-lint");
assert.equal(curatedEquivalentId("check:complexity-ratchets"), "complexity");
assert.equal(curatedEquivalentId("lint"), "lint-errors");
// An uncurated gate keeps a stable, non-colliding identity.
assert.equal(curatedEquivalentId("check:route-validation:t06"), "route-validation:t06");
});
test("fullCiKindFor honours the curated classification of an already-known gate (#9985)", () => {
const curated = [
{ id: "file-size", kind: "drift", ok: false },
{ id: "compression-budget", kind: "drift", ok: false },
{ id: "workflow-lint", kind: "drift", ok: false },
{ id: "docs-all", kind: "hard", ok: true },
{ id: "lint-errors", kind: "hard", ok: true },
];
// Ratchets curated as DRIFT must stay drift when --full-ci re-runs them from ci.yml...
assert.equal(fullCiKindFor("check:file-size", curated), "drift");
assert.equal(fullCiKindFor("check:compression-budget", curated), "drift");
assert.equal(fullCiKindFor("check:workflows", curated), "drift");
// ...real-defect gates stay hard...
assert.equal(fullCiKindFor("check:docs-all", curated), "hard");
assert.equal(fullCiKindFor("lint", curated), "hard");
// ...and a gate the curated pass never ran defaults to hard (the --full-ci contract).
assert.equal(fullCiKindFor("check:bundle-size", curated), "hard");
assert.equal(fullCiKindFor("check:route-validation:t06", curated), "hard");
});
test("one gate can never land in BOTH verdict buckets of the same report (#9985)", () => {
// The 2026-08-23 verdict listed file-size and compression-budget as hard failures
// AND as drift, in the same table, because the --full-ci pass re-recorded every
// ci.yml gate as kind:"hard" and the dedupe only compared raw ids.
const curated = [
{ id: "file-size", kind: "drift", ok: false },
{ id: "compression-budget", kind: "drift", ok: false },
];
const fromCiYaml = ["check:file-size", "check:compression-budget"].map((id) => ({
id,
kind: fullCiKindFor(id, curated),
ok: false,
}));
const v = computeVerdict([...curated, ...fromCiYaml]);
const hardGates = new Set(v.hardFailures.map((r) => curatedEquivalentId(r.id)));
const contradictions = v.drift
.map((r) => curatedEquivalentId(r.id))
.filter((id) => hardGates.has(id));
assert.deepEqual(
contradictions,
[],
"a gate reported as hard must not also be reported as drift"
);
assert.equal(
v.releaseGreen,
true,
"a curated-drift ratchet must not block the release via the --full-ci path"
);
});
test("the --full-ci loop classifies from the curated results, not a hardcoded kind (#9985)", async () => {
const fs = await import("node:fs");
const src = fs.readFileSync(
new URL("../../scripts/quality/validate-release-green.mjs", import.meta.url),
"utf8"
);
assert.match(
src,
/kind:\s*fullCiKindFor\(g\.id,\s*results\)/,
"--full-ci must classify each ci.yml gate through fullCiKindFor()"
);
});