Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
21769e9
fix(lint): drain release-green hard failures on release/v3.8.50 (#9985)
jonlwheat2-gif Aug 24, 2026
38514ff
fix(lint): finish #9985 drain — stale suppression + self-review repair
jonlwheat2-gif Aug 24, 2026
51bad41
fix(build): stop staging prune from deleting runtime node_modules (#9…
jonlwheat2-gif Aug 24, 2026
98ce388
chore(lint): prune suppressions consumed by the #9985 fixes
jonlwheat2-gif Aug 24, 2026
adca3b8
fix(kie): map remaining google-imagen Market ids to their real KIE up…
diegosouzapw Aug 24, 2026
40573e9
fix(cli-i18n): add missing pt-BR setup.opencode / serve.tls_cert / se…
jonlwheat2-gif Aug 24, 2026
57d5415
fix(i18n): fill missing dashboard keys across locales (#9985)
jonlwheat2-gif Aug 24, 2026
c3698ee
fix(dashboard): route the Adapta tutorial CTA through the branded sho…
diegosouzapw Aug 24, 2026
8d6f91b
fix(security): refuse proxy-authorization and proxy-authenticate upst…
ntdat812 Aug 24, 2026
24ac714
test(db): make singleton reset survive the full suite and un-skip the…
pacocartones Aug 24, 2026
04b2c47
fix(i18n): restore three placeholders dropped from the pt catalogue (…
ntdat812 Aug 24, 2026
79f8ae9
fix(i18n): add the 3 pt-BR CLI keys that break the locale parity test…
pacocartones Aug 24, 2026
6984676
fix(quality): report the real failure line and stop double-counting c…
pacocartones Aug 24, 2026
5ee646e
fix(github): verify access tokens during health checks (#11320)
RaviTharuma Aug 24, 2026
cb11592
fix(db): judge the proxy URL host by address, not by spelling (#11319)
ntdat812 Aug 24, 2026
29f2629
feat(compression): isolate sync engines in bounded worker pool (#11318)
RaviTharuma Aug 24, 2026
9b14896
feat(api): add Google AI Studio Gemini TTS (#11315)
RaviTharuma Aug 24, 2026
434dc21
Merge remote-tracking branch into fix/release-green-lint-drain
hartmark Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions bin/cli/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions changelog.d/features/10590-google-ai-studio-tts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added Google AI Studio Gemini batch text-to-speech support through `POST /v1/audio/speech`.
3 changes: 3 additions & 0 deletions changelog.d/features/11023-compression-worker-pool.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog.d/fixes/10352-github-access-token-health.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog.d/fixes/11319-upstream-proxy-host-spelling.md
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions changelog.d/fixes/11325-i18n-pt-placeholder-parity.md
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions changelog.d/fixes/11326-kie-market-google-imagen-ids.md
Original file line number Diff line number Diff line change
@@ -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)).
1 change: 1 addition & 0 deletions changelog.d/fixes/11328-upstream-headers-proxy-auth.md
Original file line number Diff line number Diff line change
@@ -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))
19 changes: 8 additions & 11 deletions config/quality/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -3445,7 +3445,7 @@
},
"tests/integration/qdrant-routes.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 19
"count": 3
}
},
"tests/integration/quota-pools-usage.test.ts": {
Expand Down Expand Up @@ -4029,10 +4029,10 @@
},
"tests/unit/cli-combo-suggest-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 16
"count": 14
},
"@typescript-eslint/no-unused-vars": {
"count": 2
"count": 1
}
},
"tests/unit/cli-completion-dynamic.test.ts": {
Expand All @@ -4042,7 +4042,7 @@
},
"tests/unit/cli-compression-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 32
"count": 20
}
},
"tests/unit/cli-context-eng-commands.test.ts": {
Expand Down Expand Up @@ -4099,7 +4099,7 @@
},
"tests/unit/cli-mcp-call-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 16
"count": 10
}
},
"tests/unit/cli-memory-commands.test.ts": {
Expand Down Expand Up @@ -4130,7 +4130,7 @@
},
"tests/unit/cli-oneproxy-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 22
"count": 14
},
"@typescript-eslint/no-unused-vars": {
"count": 1
Expand Down Expand Up @@ -4203,9 +4203,6 @@
"tests/unit/cli-resilience-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 16
},
"@typescript-eslint/no-unused-vars": {
"count": 2
}
},
"tests/unit/cli-runtime-extended.test.ts": {
Expand Down Expand Up @@ -4238,7 +4235,7 @@
},
"tests/unit/cli-skills-commands.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 22
"count": 16
}
},
"tests/unit/cli-stop-supervisor-respawn-9455.test.ts": {
Expand Down Expand Up @@ -6620,4 +6617,4 @@
"count": 2
}
}
}
}
3 changes: 3 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
13 changes: 13 additions & 0 deletions open-sse/config/audioRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 81 additions & 0 deletions open-sse/executors/geminiTts.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
6 changes: 3 additions & 3 deletions open-sse/executors/vertexMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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. */
Expand Down
8 changes: 8 additions & 0 deletions open-sse/handlers/audioSpeech.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions open-sse/handlers/imageGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions open-sse/services/compression/compressionWorker.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
Loading
Loading