feat(sse): x-omniroute-strip-reasoning header to drop reasoning_content (#4678)

Integrated into release/v3.8.37 — x-omniroute-strip-reasoning header. Cherry-picked onto release tip (resolved chatCore.ts/headers.ts adjacency conflict, kept resolveCompressionHeader + isStripReasoningRequested); tests 8/8 green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 21:30:17 -03:00
committed by GitHub
parent 33ac3f6b7d
commit 5b1bb72483
4 changed files with 171 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ import {
getHeaderValueCaseInsensitive,
isNoMemoryRequested,
resolveCompressionHeader,
isStripReasoningRequested,
} from "./chatCore/headers.ts";
import { markCodexScopeRateLimited } from "./chatCore/codexFailover.ts";
import { getCombosCached } from "./chatCore/comboContextCache.ts";
@@ -3493,7 +3494,13 @@ export async function handleChatCore({
if (clientResponseFormat === FORMATS.OPENAI_RESPONSES) {
translatedResponse = sanitizeResponsesApiResponse(translatedResponse);
} else if (clientResponseFormat === FORMATS.OPENAI) {
translatedResponse = sanitizeOpenAIResponse(translatedResponse);
// Port of decolua/9router#517: opt-in `x-omniroute-strip-reasoning` header
// unconditionally drops `reasoning_content` from the final non-streaming
// JSON for clients (e.g. Firecrawl AI SDK) whose JSON parsers break on
// that non-standard field. Reasoning replay cache is captured above this
// sanitize step, so the cache feature is unaffected.
const stripReasoning = isStripReasoningRequested(clientRawRequest?.headers ?? null);
translatedResponse = sanitizeOpenAIResponse(translatedResponse, { stripReasoning });
}
applyClientUsageBuffer(translatedResponse, body, clientResponseFormat);

View File

@@ -44,3 +44,22 @@ export function resolveCompressionHeader(
const value = (getHeaderValueCaseInsensitive(headers, "x-omniroute-compression") || "").trim();
return value || null;
}
/**
* Per-request opt-in to unconditionally strip `reasoning_content` from the
* non-streaming JSON response via the `x-omniroute-strip-reasoning` header.
* Some clients (e.g. Firecrawl AI SDK) have JSON parsers that break on this
* non-standard OpenAI extension even though it's syntactically valid, and even
* on reasoning-only messages that the default sanitizer keeps. Truthy values:
* `true` / `1` / `yes` (case-insensitive). Ported from upstream 9router#517
* (closes upstream #509). Reasoning is still captured for the replay cache
* before this header is consulted, so the cache feature is unaffected.
*/
export function isStripReasoningRequested(
headers: Record<string, unknown> | Headers | null | undefined
): boolean {
const value = (getHeaderValueCaseInsensitive(headers, "x-omniroute-strip-reasoning") || "")
.trim()
.toLowerCase();
return value === "true" || value === "1" || value === "yes";
}

View File

@@ -269,10 +269,26 @@ export function extractThinkingFromContent(text: string): {
* Sanitize a non-streaming OpenAI ChatCompletion response.
* Strips non-standard fields and normalizes required fields.
*/
export function sanitizeOpenAIResponse(body: unknown): unknown {
export interface SanitizeOpenAIResponseOptions {
/**
* When true, unconditionally remove `reasoning_content` from every choice
* message in the final payload — including reasoning-only messages and
* DeepSeek V4 — even though the default sanitizer keeps it in those cases.
* Wired to the `x-omniroute-strip-reasoning` request header for clients whose
* JSON parsers cannot tolerate the non-standard field (e.g. Firecrawl AI SDK).
* Ported from upstream 9router#517 (closes upstream #509).
*/
stripReasoning?: boolean;
}
export function sanitizeOpenAIResponse(
body: unknown,
options: SanitizeOpenAIResponseOptions = {}
): unknown {
const bodyRecord = toRecord(body);
if (!bodyRecord) return body;
const isDeepSeekV4 = isDeepSeekV4Model(bodyRecord.model);
const stripReasoning = options.stripReasoning === true;
// Build sanitized response with only allowed top-level fields
const sanitized: JsonRecord = {};
@@ -296,6 +312,9 @@ export function sanitizeOpenAIResponse(body: unknown): unknown {
) {
sanitizedChoice.finish_reason = "tool_calls";
}
if (stripReasoning && message && "reasoning_content" in message) {
delete message.reasoning_content;
}
return sanitizedChoice;
});
} else {

View File

@@ -0,0 +1,124 @@
import test from "node:test";
import assert from "node:assert/strict";
// Port of decolua/9router#517: per-request opt-in via the
// `x-omniroute-strip-reasoning` header to unconditionally strip
// `reasoning_content` from non-streaming JSON responses. Some clients
// (Firecrawl AI SDK) have JSON parsers that break on this non-standard
// extension even when there is no visible content, so the default
// "keep reasoning-only messages" behavior is not sufficient.
const { isStripReasoningRequested, getHeaderValueCaseInsensitive } = await import(
"../../open-sse/handlers/chatCore/headers.ts"
);
const { sanitizeOpenAIResponse } = await import(
"../../open-sse/handlers/responseSanitizer.ts"
);
test("isStripReasoningRequested is true for truthy header values", () => {
for (const v of ["true", "1", "yes", "TRUE", "Yes", " true "]) {
assert.equal(
isStripReasoningRequested({ "x-omniroute-strip-reasoning": v }),
true,
`expected true for ${JSON.stringify(v)}`
);
}
});
test("isStripReasoningRequested is case-insensitive on the header NAME", () => {
assert.equal(isStripReasoningRequested({ "X-OmniRoute-Strip-Reasoning": "true" }), true);
});
test("isStripReasoningRequested works with a Headers instance", () => {
const h = new Headers();
h.set("x-omniroute-strip-reasoning", "1");
assert.equal(isStripReasoningRequested(h), true);
});
test("isStripReasoningRequested is false when absent / empty / falsy", () => {
assert.equal(isStripReasoningRequested(null), false);
assert.equal(isStripReasoningRequested(undefined), false);
assert.equal(isStripReasoningRequested({}), false);
assert.equal(isStripReasoningRequested({ "x-omniroute-strip-reasoning": "" }), false);
assert.equal(isStripReasoningRequested({ "x-omniroute-strip-reasoning": "false" }), false);
assert.equal(isStripReasoningRequested({ "x-omniroute-strip-reasoning": "0" }), false);
assert.equal(isStripReasoningRequested({ "x-omniroute-strip-reasoning": "no" }), false);
});
test("getHeaderValueCaseInsensitive still resolves the header (sanity)", () => {
assert.equal(
getHeaderValueCaseInsensitive(
{ "x-omniroute-strip-reasoning": "true" },
"x-omniroute-strip-reasoning"
),
"true"
);
});
// Default behavior (regression guard): reasoning-only messages keep
// reasoning_content. This matches existing logic — non-streaming responses
// only drop reasoning_content when there is ALSO visible content.
test("default: reasoning-only message keeps reasoning_content", () => {
const out = sanitizeOpenAIResponse({
id: "chatcmpl-x",
object: "chat.completion",
created: 1,
model: "deepseek-reasoner",
choices: [
{
index: 0,
message: { role: "assistant", content: "", reasoning_content: "internal thoughts" },
finish_reason: "stop",
},
],
}) as { choices: Array<{ message: { reasoning_content?: string } }> };
assert.equal(out.choices[0].message.reasoning_content, "internal thoughts");
});
// Opt-in port of PR#517: when stripReasoning=true, reasoning_content is
// always removed from the final non-streaming JSON, even on reasoning-only
// messages. Firecrawl AI SDK and similar JSON parsers cannot tolerate it.
test("stripReasoning=true: reasoning-only message has reasoning_content removed", () => {
const out = sanitizeOpenAIResponse(
{
id: "chatcmpl-x",
object: "chat.completion",
created: 1,
model: "deepseek-reasoner",
choices: [
{
index: 0,
message: { role: "assistant", content: "", reasoning_content: "internal thoughts" },
finish_reason: "stop",
},
],
},
{ stripReasoning: true }
) as { choices: Array<{ message: Record<string, unknown> }> };
assert.equal(out.choices[0].message.reasoning_content, undefined);
assert.equal("reasoning_content" in out.choices[0].message, false);
});
test("stripReasoning=true: message with both content and reasoning_content has reasoning stripped", () => {
const out = sanitizeOpenAIResponse(
{
id: "chatcmpl-x",
object: "chat.completion",
created: 1,
model: "deepseek-v4",
choices: [
{
index: 0,
message: {
role: "assistant",
content: "visible answer",
reasoning_content: "internal",
},
finish_reason: "stop",
},
],
},
{ stripReasoning: true }
) as { choices: Array<{ message: Record<string, unknown> }> };
assert.equal(out.choices[0].message.reasoning_content, undefined);
assert.equal(out.choices[0].message.content, "visible answer");
});