mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
echo X-OmniRoute-Compression header on completions routes (#6422, 6/6). Reconciled with #6429 body.model echo. Integrated into release/v3.8.46.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(api):** the `X-OmniRoute-Compression` response header is now echoed on `/v1/chat/completions` and `/v1/completions` ([#6422](https://github.com/diegosouzapw/OmniRoute/issues/6422)). Regression guard: `tests/unit/compression-header-echo-6422.test.ts`. (thanks @chirag127)
|
||||
- **fix(api):** concurrent `GET /v1/models` requests are coalesced into a single catalog build ([#6408](https://github.com/diegosouzapw/OmniRoute/issues/6408)). Regression guard: `tests/unit/v1-models-concurrent-6408.test.ts`. (thanks @chirag127)
|
||||
- **fix(api):** `/v1/completions` now echoes the requested `body.model` in its JSON + streamed responses. Regression guard: `tests/unit/completions-body-model-echo.test.ts`. (thanks @chirag127)
|
||||
- **fix(api):** env-var master keys now see the full `/v1/models` catalog ([#6406](https://github.com/diegosouzapw/OmniRoute/issues/6406)). Regression guard: `tests/unit/models-catalog-envkey-6406.test.ts`. (thanks @chirag127)
|
||||
|
||||
@@ -8,6 +8,10 @@ import { acceptHeaderForcesStream } from "@omniroute/open-sse/utils/aiSdkCompat.
|
||||
import { withEarlyStreamKeepalive } from "@omniroute/open-sse/utils/earlyStreamKeepalive";
|
||||
import { resolveKeepaliveThreshold } from "@omniroute/open-sse/utils/keepaliveThreshold";
|
||||
import { checkChatAdmission } from "@/shared/middleware/chatBodyAdmission";
|
||||
import {
|
||||
readCompressionRequestHeader,
|
||||
withCompressionHeaderEcho,
|
||||
} from "@/shared/utils/compressionHeaderEcho";
|
||||
|
||||
let initPromise = null;
|
||||
|
||||
@@ -99,14 +103,26 @@ export async function POST(request) {
|
||||
parsedBodyIsRecord && acceptHeaderForcesStream(acceptHeader, parsedBody.stream);
|
||||
const wantsStreaming = (parsedBodyIsRecord && parsedBody.stream === true) || acceptForcesStream;
|
||||
|
||||
// #6422 — capture the compression request header once so we can echo it back
|
||||
// on the response when internal early-returns (idempotency cache, some combo
|
||||
// paths) drop the meta the docs promise.
|
||||
const compressionRequestHeader = readCompressionRequestHeader(request);
|
||||
|
||||
if (wantsStreaming) {
|
||||
const reqId = generateRequestId();
|
||||
return await withEarlyStreamKeepalive(handleChat(request, null, parsedBody, reqId), {
|
||||
signal: request.signal,
|
||||
thresholdMs: resolveKeepaliveThreshold(parsedBody?.model),
|
||||
extraHeaders: { "X-Correlation-Id": reqId },
|
||||
});
|
||||
const streamedResponse = await withEarlyStreamKeepalive(
|
||||
handleChat(request, null, parsedBody, reqId),
|
||||
{
|
||||
signal: request.signal,
|
||||
thresholdMs: resolveKeepaliveThreshold(parsedBody?.model),
|
||||
extraHeaders: { "X-Correlation-Id": reqId },
|
||||
}
|
||||
);
|
||||
return withCompressionHeaderEcho(streamedResponse, compressionRequestHeader);
|
||||
}
|
||||
|
||||
return await handleChat(request, null, parsedBody);
|
||||
return withCompressionHeaderEcho(
|
||||
await handleChat(request, null, parsedBody),
|
||||
compressionRequestHeader
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { buildClientRawRequest, handleChat } from "@/sse/handlers/chat";
|
||||
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
|
||||
import { createInjectionGuard } from "@/middleware/promptInjectionGuard";
|
||||
import { asTextCompletionResponse } from "./textCompletionTransform.ts";
|
||||
import {
|
||||
readCompressionRequestHeader,
|
||||
withCompressionHeaderEcho,
|
||||
} from "@/shared/utils/compressionHeaderEcho";
|
||||
|
||||
let initPromise = null;
|
||||
const injectionGuard = createInjectionGuard();
|
||||
@@ -40,6 +44,10 @@ export async function OPTIONS() {
|
||||
export async function POST(request: Request) {
|
||||
await ensureInitialized();
|
||||
|
||||
// #6422 — capture the compression request header once so we can echo it back
|
||||
// on the response when internal early-returns drop the meta the docs promise.
|
||||
const compressionRequestHeader = readCompressionRequestHeader(request);
|
||||
|
||||
// Prompt injection guard
|
||||
try {
|
||||
const cloned = request.clone();
|
||||
@@ -78,10 +86,14 @@ export async function POST(request: Request) {
|
||||
// #3571 — translate the chat-pipeline response back to the legacy
|
||||
// text-completion shape so OpenAI Completion clients (e.g. TabbyML) work.
|
||||
// Thread `body.model` so response `body.model` echoes the caller's
|
||||
// requested identifier, matching the `x-omniroute-model` header.
|
||||
return await asTextCompletionResponse(
|
||||
await handleChat(newRequest, buildClientRawRequest(request, body)),
|
||||
typeof body.model === "string" ? body.model : undefined
|
||||
// requested identifier, matching the `x-omniroute-model` header, and
|
||||
// echo the compression header on the way out.
|
||||
return withCompressionHeaderEcho(
|
||||
await asTextCompletionResponse(
|
||||
await handleChat(newRequest, buildClientRawRequest(request, body)),
|
||||
typeof body.model === "string" ? body.model : undefined
|
||||
),
|
||||
compressionRequestHeader
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,5 +113,8 @@ export async function POST(request: Request) {
|
||||
} catch {
|
||||
// ignore — asTextCompletionResponse falls back to upstream body.model
|
||||
}
|
||||
return await asTextCompletionResponse(await handleChat(request), requestedModel);
|
||||
return withCompressionHeaderEcho(
|
||||
await asTextCompletionResponse(await handleChat(request), requestedModel),
|
||||
compressionRequestHeader
|
||||
);
|
||||
}
|
||||
|
||||
59
src/shared/utils/compressionHeaderEcho.ts
Normal file
59
src/shared/utils/compressionHeaderEcho.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Compression response header echo (#6422).
|
||||
*
|
||||
* When a request carries `x-omniroute-compression`, docs promise the response echoes
|
||||
* `X-OmniRoute-Compression: <mode>; source=<source>`. Internal paths (idempotency
|
||||
* cache short-circuit, some combo/fusion assembly paths) build response headers
|
||||
* without threading `compressionResponseMeta` — so the promised echo silently
|
||||
* disappears. This helper is the outermost safety net: if the response is missing
|
||||
* the header and the request supplied one, echo a best-effort value derived
|
||||
* directly from the request header. Existing header values from the inner pipeline
|
||||
* (which carry richer `tokens=...; rules: ...` annotations) are never overwritten.
|
||||
*/
|
||||
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
|
||||
|
||||
const COMPRESSION_REQUEST_HEADER = "x-omniroute-compression";
|
||||
const COMPRESSION_RESPONSE_HEADER = OMNIROUTE_RESPONSE_HEADERS.compression;
|
||||
|
||||
function normalizeRequestValue(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower === "off" || lower === "default") return lower;
|
||||
if (lower.startsWith("engine:")) return lower;
|
||||
// Named combo — preserve the operator's casing on the mode field. Source is always
|
||||
// request-header when the header drove the choice.
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the compression request header (case-insensitive). Returns null on absent/blank.
|
||||
*/
|
||||
export function readCompressionRequestHeader(request: {
|
||||
headers: { get(name: string): string | null };
|
||||
}): string | null {
|
||||
const raw = request.headers.get(COMPRESSION_REQUEST_HEADER);
|
||||
return typeof raw === "string" && raw.trim() ? raw : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a Response so it carries `X-OmniRoute-Compression: <mode>; source=request-header`
|
||||
* when the request supplied `x-omniroute-compression` and the inner pipeline did not
|
||||
* already set it. Never overwrites an existing value — the inner pipeline may have
|
||||
* attached a richer annotation. A best-effort echo covers idempotency-cache,
|
||||
* fusion-envelope, and any other early-return path that dropped the meta.
|
||||
*/
|
||||
export function withCompressionHeaderEcho(
|
||||
response: Response,
|
||||
requestHeaderValue: string | null
|
||||
): Response {
|
||||
if (!requestHeaderValue) return response;
|
||||
if (response.headers.has(COMPRESSION_RESPONSE_HEADER)) return response;
|
||||
const mode = normalizeRequestValue(requestHeaderValue);
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set(COMPRESSION_RESPONSE_HEADER, `${mode}; source=request-header`);
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
89
tests/unit/compression-header-echo-6422.test.ts
Normal file
89
tests/unit/compression-header-echo-6422.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* #6422 — X-OmniRoute-Compression response header echo.
|
||||
*
|
||||
* Docs promise that when a request supplies `x-omniroute-compression`, the response
|
||||
* echoes `X-OmniRoute-Compression: <mode>; source=<source>`. Internal early-returns
|
||||
* (idempotency-cache short-circuit, some combo/fusion assembly paths) omit that
|
||||
* header, so the outermost route layer echoes a best-effort value from the request.
|
||||
* These tests lock the helper's contract so a future refactor cannot silently drop
|
||||
* the echo again.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
readCompressionRequestHeader,
|
||||
withCompressionHeaderEcho,
|
||||
} from "../../src/shared/utils/compressionHeaderEcho";
|
||||
|
||||
function makeRequest(headerValue: string | null): {
|
||||
headers: { get(name: string): string | null };
|
||||
} {
|
||||
return {
|
||||
headers: {
|
||||
get(name: string) {
|
||||
if (name.toLowerCase() === "x-omniroute-compression") return headerValue;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("compressionHeaderEcho (#6422)", () => {
|
||||
it("reads the request header (case-insensitive, trimmed)", () => {
|
||||
assert.equal(readCompressionRequestHeader(makeRequest("engine:rtk")), "engine:rtk");
|
||||
assert.equal(readCompressionRequestHeader(makeRequest(" off ")), " off ");
|
||||
assert.equal(readCompressionRequestHeader(makeRequest(null)), null);
|
||||
assert.equal(readCompressionRequestHeader(makeRequest("")), null);
|
||||
assert.equal(readCompressionRequestHeader(makeRequest(" ")), null);
|
||||
});
|
||||
|
||||
it("echoes the request header value onto the response when missing", () => {
|
||||
const inner = new Response("body", { status: 200 });
|
||||
const wrapped = withCompressionHeaderEcho(inner, "engine:rtk");
|
||||
assert.equal(wrapped.headers.get("X-OmniRoute-Compression"), "engine:rtk; source=request-header");
|
||||
assert.equal(wrapped.status, 200);
|
||||
});
|
||||
|
||||
it("normalizes off / default / engine:* to lowercase", () => {
|
||||
assert.equal(
|
||||
withCompressionHeaderEcho(new Response(""), "OFF").headers.get("X-OmniRoute-Compression"),
|
||||
"off; source=request-header"
|
||||
);
|
||||
assert.equal(
|
||||
withCompressionHeaderEcho(new Response(""), "Default").headers.get("X-OmniRoute-Compression"),
|
||||
"default; source=request-header"
|
||||
);
|
||||
assert.equal(
|
||||
withCompressionHeaderEcho(new Response(""), "Engine:Rtk").headers.get(
|
||||
"X-OmniRoute-Compression"
|
||||
),
|
||||
"engine:rtk; source=request-header"
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves operator casing on named-combo values", () => {
|
||||
const wrapped = withCompressionHeaderEcho(new Response(""), " MyCombo ");
|
||||
assert.equal(wrapped.headers.get("X-OmniRoute-Compression"), "MyCombo; source=request-header");
|
||||
});
|
||||
|
||||
it("never overwrites an existing X-OmniRoute-Compression header set by the inner pipeline", () => {
|
||||
const inner = new Response("", {
|
||||
headers: {
|
||||
"X-OmniRoute-Compression": "stacked; source=routing; tokens=100->42; rules: rtk-nl x2",
|
||||
},
|
||||
});
|
||||
const wrapped = withCompressionHeaderEcho(inner, "engine:rtk");
|
||||
assert.equal(
|
||||
wrapped.headers.get("X-OmniRoute-Compression"),
|
||||
"stacked; source=routing; tokens=100->42; rules: rtk-nl x2"
|
||||
);
|
||||
});
|
||||
|
||||
it("is a no-op when the request did not supply the header", () => {
|
||||
const inner = new Response("", { headers: { "X-Other": "keep" } });
|
||||
const wrapped = withCompressionHeaderEcho(inner, null);
|
||||
assert.equal(wrapped, inner);
|
||||
assert.equal(wrapped.headers.get("X-OmniRoute-Compression"), null);
|
||||
assert.equal(wrapped.headers.get("X-Other"), "keep");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user