Files
OmniRoute/tests/unit/response-sanitizer.test.ts
Diego Rodrigues de Sa e Souza c315a2394c Release v3.8.21 (#3593)
* chore(release): open v3.8.21 development cycle

* fix: pass through valid max_tokens-truncated responses instead of fake 502 (#3572) (#3595)

* fix: /v1/completions returns legacy text-completion format, not chat (#3571) (#3596)

* fix: z.ai/GLM coding plan no longer shows Monthly 0% when no monthly cap (#3580) (#3597)

* docs: mark DISCOVERY_TOOL_DESIGN endpoints as Phase-2 not-yet-implemented (#3498) (#3599)

* fix(agent-bridge): add validate-only upstream-ca/test route (#3488) (#3600)

* fix(gamification): add level/badges/badges-earned profile routes (#3484)

* security(oauth): migrate 5 public client_ids to resolvePublicCred (#3493)

* fix(mcp): ship MCP server source closure in npm files + coverage gate (#3578)

* fix: add reasoning token buffer for combo routing (fixes #3587) (#3588)

Integrated into release/v3.8.21

* Refactor: Extract chatCore phases into modular files (#3598)

Integrated into release/v3.8.21 — chatCore phase modularization. Adjusted: re-derive idempotencyKey for the save path after the check moved into the module (co-authored). Thanks @oyi77!

* docs(changelog): credit #3598 (chatCore modularization) + #3588 (combo reasoning buffer)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(api): implement GET /api/guardrails + POST /api/guardrails/test, drop shadow/guardrails doc-fiction (#3496) (#3602)

Integrated into release/v3.8.21 — implements GET /api/guardrails + POST /api/guardrails/test, removes shadow/guardrails doc-fiction. TDD-validated (5/5) + check-docs-symbols/typecheck/eslint green.

* fix(gemini): isolate textual reasoning wrappers (#3605)

Split-out PR C from #3584. Isolates textual reasoning wrappers (<think>/<thinking>/<thought>/<internal_thought>, including malformed/open tags) into reasoning_content across both the non-streaming sanitizer and the Gemini streaming translator, with split-chunk buffering. Additive to the existing textual tool-call pipeline; does not touch the #3569 native functionResponse path. Integrated into release/v3.8.21. Thanks @dhaern!

* fix(antigravity): normalize Gemini 3.5 Flash tier IDs (#3603)

Split-out PR A from #3584. Normalizes the Antigravity/agy Gemini 3.5 Flash tier IDs to clean public names (gemini-3.5-flash-low/medium/high), maps them to the live upstream IDs at the executor boundary, and removes Antigravity from the global model resolver so the executor owns wire normalization. Maintainer follow-up: kept gemini-3.5-flash-preview as a hidden backward-compat alias routing to the High tier (so saved combos/configs keep working). Live-validated the tier set via the agy CLI catalog. Integrated into release/v3.8.21. Thanks @dhaern!

* fix(agent-bridge): surface real MITM startup-failure cause, not always port 443 (#3606) (#3608)

Integrated into release/v3.8.21 (#3606)

* fix(oauth): surface real Kiro import-token failure cause, not a bare 500 (#3589) (#3609)

Integrated into release/v3.8.21 (#3589)

* docs(opencode-provider): soft-deprecate in favor of @omniroute/opencode-plugin (#3419) (#3613)

Integrated into release/v3.8.21 (#3419)

* fix(usage): normalize Antigravity and agy provider quotas (#3604)

Split-out PR B from #3584. Normalizes Antigravity/agy provider quotas: prefers retrieveUserQuota for live consumption, falls back to fetchAvailableModels and local usage_history, sanitizes cached Provider Limits so retired upstream IDs are not re-exposed, and schedules a deduplicated post-usage refresh. Maintainer follow-up: decoupled the post-usage refresh via a lightweight usageEvents bus (usageHistory no longer dynamic-imports providerLimits) so it does not pull the executors/translator graph into the typecheck-core surface — typecheck:core stays at 0. Integrated into release/v3.8.21. Thanks @dhaern!

* feat(cli): add autostart on/off/toggle shorthand for headless serve mode (#3331) (#3614)

Integrated into release/v3.8.21 (#3331)

* docs(changelog): credit #3603 (Flash tier IDs) + #3604 (provider quotas) + #3605 (reasoning wrappers)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* fix(review): resolve findings from /review-reviews battery (v3.8.21 hardening) (#3618)

Pre-release hardening from the /review-reviews battery — 15 findings resolved (L1-L13,L15) + L14 live-verified WONTFIX, convergence re-review clean. lint/typecheck:core/test:vitest(146)/build green; zero new test:unit failures vs baseline 797de433f.

* chore(release): v3.8.21 CHANGELOG + i18n + env-doc sync

---------

Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Raxxoor <manker_lol@hotmail.com>
2026-06-11 04:01:24 -03:00

651 lines
21 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const {
extractThinkingFromContent,
sanitizeOpenAIResponse,
sanitizeResponsesApiResponse,
sanitizeStreamingChunk,
} = await import("../../open-sse/handlers/responseSanitizer.ts");
test("extractThinkingFromContent separates think blocks from visible content", () => {
const parsed = extractThinkingFromContent(
"Before<think>reasoning 1</think>middle<thinking>reasoning 2</thinking>after"
);
assert.equal(parsed.content, "Beforemiddleafter");
assert.equal(parsed.thinking, "reasoning 1\n\nreasoning 2");
});
// #3821-review LEDGER-7 — the unclosed-reasoning-tag heuristic (#3605) reclassifies a
// dangling `<thought`-style tail as reasoning. Pin that a REAL visible prefix before such
// a tail is preserved as content (only a whitespace/§marker§ prefix collapses to ""), and
// that a non-reasoning tag like `<thoughtful>` is NOT captured.
test("extractThinkingFromContent preserves a real prefix before a dangling reasoning tag", () => {
const parsed = extractThinkingFromContent("Here is the answer. <thought\nleftover reasoning");
assert.equal(parsed.content, "Here is the answer.");
assert.equal(parsed.thinking, "leftover reasoning");
});
test("extractThinkingFromContent: §marker§-only prefix collapses to empty content", () => {
const parsed = extractThinkingFromContent("§54§ <thought\ninternal planning");
assert.equal(parsed.content, "");
assert.equal(parsed.thinking, "internal planning");
});
test("extractThinkingFromContent does NOT treat <thoughtful> as a reasoning tag", () => {
const parsed = extractThinkingFromContent("See the <thoughtful> approach here");
assert.equal(parsed.content, "See the <thoughtful> approach here");
assert.equal(parsed.thinking, null);
});
test("sanitizeOpenAIResponse strips non-standard fields and preserves required top-level fields", () => {
const sanitized = sanitizeOpenAIResponse({
id: "chatcmpl_existing",
object: "chat.completion",
created: 123,
model: "gpt-4.1",
choices: [],
x_groq: { ignored: true },
service_tier: "premium",
});
assert.deepEqual(sanitized, {
id: "chatcmpl_existing",
object: "chat.completion",
created: 123,
model: "gpt-4.1",
choices: [],
});
});
test("sanitizeOpenAIResponse extracts thinking, collapses newlines, preserves reasoning_content with tool_calls, and preserves tool calls", () => {
const sanitized = sanitizeOpenAIResponse({
id: "chatcmpl_test",
model: "gpt-4.1",
choices: [
{
index: 2,
finish_reason: "tool_calls",
message: {
role: "assistant",
content: "Hello\n\n\n<think>internal chain</think>\n\nworld",
tool_calls: [{ id: "call_1" }],
function_call: { name: "legacy" },
},
},
],
});
assert.equal((sanitized as any).choices[0].index, 2);
assert.equal((sanitized as any).choices[0].finish_reason, "tool_calls");
(assert as any).equal((sanitized as any).choices[0].message.content, "Hello\n\nworld");
assert.equal((sanitized as any).choices[0].message.reasoning_content, "internal chain");
(assert as any).deepEqual((sanitized as any).choices[0].message.tool_calls, [{ id: "call_1" }]);
assert.deepEqual((sanitized as any).choices[0].message.function_call, { name: "legacy" });
});
test("sanitizeOpenAIResponse extracts unclosed reasoning wrappers into reasoning_content", () => {
const sanitized = sanitizeOpenAIResponse({
model: "gpt-4.1",
choices: [
{
message: {
role: "assistant",
content: "§54§ <thought\ninternal planning\n",
},
},
],
});
assert.equal((sanitized as any).choices[0].message.content, "");
assert.equal((sanitized as any).choices[0].message.reasoning_content, "internal planning");
});
test("sanitizeOpenAIResponse preserves native reasoning_content when no visible content remains", () => {
const sanitized = sanitizeOpenAIResponse({
model: "gpt-4.1",
choices: [
{
message: {
role: "assistant",
content: "<think>discard me</think>",
reasoning_content: "provider reasoning",
},
},
],
});
assert.equal(((sanitized as any).choices[0].message as any).content, "");
assert.equal((sanitized as any).choices[0].message.reasoning_content, "provider reasoning");
});
test("sanitizeOpenAIResponse maps Claude-style usage fields and strips extras", () => {
const sanitized = sanitizeOpenAIResponse({
model: "claude-3-7-sonnet",
choices: [],
usage: {
input_tokens: 11,
output_tokens: 7,
service_tier: "ignored",
usage_breakdown: { ignored: true },
},
});
assert.deepEqual((sanitized as any).usage, {
prompt_tokens: 11,
completion_tokens: 7,
total_tokens: 18,
});
});
test("sanitizeOpenAIResponse strips reasoning_details-derived reasoning_content when visible text exists", () => {
const sanitized = sanitizeOpenAIResponse({
model: "openrouter/model",
choices: [
{
message: {
role: "assistant",
content: "Visible",
reasoning_details: [
{ type: "reasoning.text", text: "first " },
{ type: "thinking", content: "second" },
{ type: "other", text: "ignored" },
],
},
},
],
});
assert.equal((sanitized as any).choices[0].message.reasoning_content, undefined);
});
test("sanitizeOpenAIResponse preserves DeepSeek V4 reasoning_content with visible text", () => {
const sanitized = sanitizeOpenAIResponse({
model: "deepseek-v4-pro",
choices: [
{
message: {
role: "assistant",
content: "Visible answer",
reasoning_content: "DeepSeek reasoning",
},
},
],
});
assert.equal((sanitized as any).choices[0].message.content, "Visible answer");
assert.equal((sanitized as any).choices[0].message.reasoning_content, "DeepSeek reasoning");
});
test("sanitizeOpenAIResponse preserves DeepSeek V4 reasoning_details with visible text", () => {
const sanitized = sanitizeOpenAIResponse({
model: "deepseek-v4/reasoner",
choices: [
{
message: {
role: "assistant",
content: "Visible answer",
reasoning_details: [
{ type: "reasoning.text", text: "first " },
{ type: "thinking", content: "second" },
],
},
},
],
});
assert.equal((sanitized as any).choices[0].message.reasoning_content, "first second");
});
test("sanitizeOpenAIResponse still strips non-DeepSeek reasoning_content with visible text", () => {
const sanitized = sanitizeOpenAIResponse({
model: "o3-mini",
choices: [
{
message: {
role: "assistant",
content: "Visible answer",
reasoning_content: "OpenAI reasoning",
},
},
],
});
assert.equal((sanitized as any).choices[0].message.reasoning_content, undefined);
});
test("sanitizeOpenAIResponse keeps reasoning_details-derived reasoning_content for reasoning-only messages", () => {
const sanitized = sanitizeOpenAIResponse({
model: "openrouter/model",
choices: [
{
message: {
role: "assistant",
content: "",
reasoning_details: [
{ type: "reasoning.text", text: "first " },
{ type: "thinking", content: "second" },
],
},
},
],
});
assert.equal((sanitized as any).choices[0].message.reasoning_content, "first second");
});
test("sanitizeResponsesApiResponse converts chat completions tool calls into Responses output items", () => {
const sanitized = sanitizeResponsesApiResponse({
id: "chatcmpl_tool",
object: "chat.completion",
created: 123,
model: "gpt-4.1",
choices: [
{
index: 0,
finish_reason: "tool_calls",
message: {
role: "assistant",
content: "",
reasoning_content: "Check web results first.",
tool_calls: [
{
id: "call_web_search",
type: "function",
function: {
name: "omniroute_web_search",
arguments: '{"query":"omniroute"}',
},
},
],
},
},
],
usage: {
prompt_tokens: 12,
completion_tokens: 5,
prompt_tokens_details: { cached_tokens: 3 },
completion_tokens_details: { reasoning_tokens: 2 },
},
});
assert.equal((sanitized as any).object, "response");
assert.equal((sanitized as any).id, "resp_chatcmpl_tool");
assert.equal((sanitized as any).output[0].type, "reasoning");
(assert as any).equal((sanitized as any).output[1].type, "function_call");
(assert as any).equal((sanitized as any).output[1].call_id, "call_web_search");
(assert as any).equal((sanitized as any).output[1].name, "omniroute_web_search");
assert.equal((sanitized as any).usage.input_tokens, 12);
assert.equal(((sanitized as any).usage as any).output_tokens, 5);
assert.equal((sanitized as any).usage.input_tokens_details.cached_tokens, 3);
assert.equal((sanitized as any).usage.output_tokens_details.reasoning_tokens, 2);
});
test("sanitizeResponsesApiResponse preserves native Responses payloads and usage details", () => {
const sanitized = sanitizeResponsesApiResponse({
id: "resp_native",
object: "response",
created_at: 456,
model: "gpt-5.1-codex",
status: "completed",
output: [
{
id: "msg_1",
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Hello\n\n\nworld", annotations: [] }],
},
{
id: "fc_1",
type: "function_call",
call_id: "call_1",
name: "lookup",
arguments: { path: "/tmp/a" },
},
],
usage: {
input_tokens: 20,
output_tokens: 7,
prompt_tokens_details: { cached_tokens: 4 },
cache_creation_input_tokens: 1,
completion_tokens_details: { reasoning_tokens: 3 },
},
});
assert.equal((sanitized as any).object, "response");
assert.equal(((sanitized as any).output[0] as any).content[0].text, "Hello\n\nworld");
assert.equal((sanitized as any).output[1].arguments, '{"path":"/tmp/a"}');
assert.equal((sanitized as any).output_text, "Hello\n\nworld");
assert.equal((sanitized as any).usage.input_tokens, 20);
(assert as any).equal((sanitized as any).usage.output_tokens, 7);
assert.equal((sanitized as any).usage.input_tokens_details.cached_tokens, 4);
assert.equal((sanitized as any).usage.input_tokens_details.cache_creation_tokens, 1);
assert.equal((sanitized as any).usage.output_tokens_details.reasoning_tokens, 3);
});
test("sanitizeStreamingChunk keeps only safe chunk fields and maps reasoning aliases", () => {
const sanitized = sanitizeStreamingChunk({
id: "chunk_1",
object: "chat.completion.chunk",
created: 456,
model: "gpt-4.1",
choices: [
{
index: 3,
delta: {
role: "assistant",
content: "Line 1\n\n\nLine 2",
reasoning: "stream reasoning",
tool_calls: [{ id: "call_1" }],
},
finish_reason: "stop",
logprobs: { mock: true },
},
],
usage: { input_tokens: 2, output_tokens: 1, secret: true },
system_fingerprint: "fp_123",
provider_debug: "drop-me",
});
assert.deepEqual(sanitized, {
id: "chunk_1",
object: "chat.completion.chunk",
created: 456,
model: "gpt-4.1",
choices: [
{
index: 3,
delta: {
role: "assistant",
content: "Line 1\n\nLine 2",
reasoning_content: "stream reasoning",
tool_calls: [{ id: "call_1" }],
},
finish_reason: "stop",
logprobs: { mock: true },
},
],
usage: {
prompt_tokens: 2,
completion_tokens: 1,
total_tokens: 3,
},
system_fingerprint: "fp_123",
});
});
test("sanitizeStreamingChunk converts reasoning_details arrays in deltas", () => {
const sanitized = sanitizeStreamingChunk({
choices: [
{
delta: {
reasoning_details: [{ type: "reasoning.text", text: "alpha" }, { content: "beta" }],
},
},
],
});
assert.equal((sanitized as any).choices[0].delta.reasoning_content, "alphabeta");
});
test("sanitizeStreamingChunk preserves Copilot reasoning_text deltas", () => {
const sanitized = sanitizeStreamingChunk({
choices: [
{
delta: {
reasoning_text: "copilot reasoning",
},
},
],
});
assert.equal((sanitized as any).choices[0].delta.reasoning_text, "copilot reasoning");
});
test("sanitizeStreamingChunk strips commentary content from Responses completed events", () => {
const sanitized = sanitizeStreamingChunk({
type: "response.completed",
response: {
id: "resp_1",
object: "response",
model: "gpt-5.1-codex",
status: "completed",
output_text: "hiddenshown",
output: [
{
id: "msg_1",
type: "message",
role: "assistant",
content: [
{ type: "output_text", text: "hidden", phase: "commentary" },
{ type: "output_text", text: "shown", phase: "final_answer" },
],
},
],
},
});
assert.equal((sanitized as any).response.output[0].content.length, 1);
assert.equal((sanitized as any).response.output[0].content[0].text, "shown");
assert.equal((sanitized as any).response.output_text, "shown");
});
test("sanitizeStreamingChunk marks internal Responses output_item events for omission", () => {
const sanitized = sanitizeStreamingChunk({
type: "response.output_item.done",
item: {
id: "msg_internal",
type: "message",
role: "assistant",
phase: "commentary",
content: [{ type: "output_text", text: "hidden" }],
},
});
assert.equal((sanitized as any).__omniroute_omit_streaming_chunk, true);
assert.equal("item" in (sanitized as any), false);
});
test("sanitizeOpenAIResponse preserves reasoning_content when tool_calls are present", () => {
// Bug fix: Kimi and other thinking-enabled providers require reasoning_content
// on assistant messages that contain tool_calls. The sanitizer was stripping
// reasoning_content whenever visible content existed, breaking subsequent
// requests with "thinking is enabled but reasoning_content is missing".
const sanitized = sanitizeOpenAIResponse({
model: "kimi-k2.6-thinking",
choices: [
{
message: {
role: "assistant",
content: "Let me search for that.",
reasoning_content: "I need to use the web search tool to find current information.",
tool_calls: [
{
id: "call_search_1",
type: "function",
function: {
name: "web_search",
arguments: '{"query":"latest news"}',
},
},
],
},
},
],
});
const message = (sanitized as any).choices[0].message;
assert.equal(message.content, "Let me search for that.");
assert.equal(
message.reasoning_content,
"I need to use the web search tool to find current information.",
"reasoning_content must be preserved when tool_calls are present"
);
assert.equal(message.tool_calls.length, 1);
assert.equal(message.tool_calls[0].id, "call_search_1");
});
test("sanitizeOpenAIResponse still strips reasoning_content when no tool_calls exist", () => {
// When there are no tool_calls, the original behavior should remain:
// reasoning_content is stripped to avoid client rendering issues.
const sanitized = sanitizeOpenAIResponse({
model: "gpt-4.1",
choices: [
{
message: {
role: "assistant",
content: "Hello world",
reasoning_content: "Some internal reasoning",
},
},
],
});
const message = (sanitized as any).choices[0].message;
assert.equal(message.content, "Hello world");
assert.equal(message.reasoning_content, undefined);
});
test("sanitizeOpenAIResponse preserves reasoning_content when legacy function_call is present", () => {
const sanitized = sanitizeOpenAIResponse({
model: "kimi-k2.6-thinking",
choices: [
{
message: {
role: "assistant",
content: "Let me calculate that.",
reasoning_content: "I need to use the calculator function.",
function_call: { name: "calculate", arguments: '{"expr":"1+1"}' },
},
},
],
});
const message = (sanitized as any).choices[0].message;
assert.equal(message.content, "Let me calculate that.");
assert.equal(
message.reasoning_content,
"I need to use the calculator function.",
"reasoning_content must be preserved when legacy function_call is present"
);
assert.deepEqual(message.function_call, { name: "calculate", arguments: '{"expr":"1+1"}' });
});
test("sanitize functions return non-object inputs unchanged", () => {
assert.equal(sanitizeOpenAIResponse(null), null);
assert.equal(sanitizeStreamingChunk("raw text"), "raw text");
});
test("sanitizeOpenAIResponse converts textual pseudo tool-call content into structured tool_calls", () => {
const sanitized = sanitizeOpenAIResponse({
id: "chatcmpl_textual_tool_call",
object: "chat.completion",
created: 1,
model: "MainAgent",
choices: [
{
index: 0,
finish_reason: "stop",
message: {
role: "assistant",
content:
'Проверю.\n[Tool call: terminal]\nArguments: {"command":"echo hermes_textual_toolcall_guard","timeout":10}',
},
},
],
}) as any;
const choice = sanitized.choices[0];
assert.equal(choice.finish_reason, "tool_calls");
assert.equal(choice.message.content, null);
assert.equal(choice.message.tool_calls[0].type, "function");
assert.equal(choice.message.tool_calls[0].function.name, "terminal");
assert.deepEqual(JSON.parse(choice.message.tool_calls[0].function.arguments), {
command: "echo hermes_textual_toolcall_guard",
timeout: 10,
});
assert.equal(JSON.stringify(sanitized).includes("[Tool call:"), false);
assert.equal(JSON.stringify(sanitized).includes("Arguments:"), false);
});
test("sanitizeOpenAIResponse suppresses malformed textual pseudo tool-call content", () => {
const sanitized = sanitizeOpenAIResponse({
id: "chatcmpl_malformed_textual_tool_call",
object: "chat.completion",
created: 1,
model: "MainAgent",
choices: [
{
index: 0,
finish_reason: "stop",
message: {
role: "assistant",
content: "[Tool call: terminal]\nArguments: {not json",
},
},
],
}) as any;
const choice = sanitized.choices[0];
assert.equal(choice.finish_reason, "stop");
assert.equal(choice.message.content, null);
assert.equal(choice.message.tool_calls, undefined);
assert.equal(JSON.stringify(sanitized).includes("[Tool call:"), false);
assert.equal(JSON.stringify(sanitized).includes("Arguments:"), false);
});
test("sanitizeOpenAIResponse strips leaked internal to=functions tool envelopes from assistant text", () => {
const sanitized = sanitizeOpenAIResponse({
id: "chatcmpl_internal_tool_envelope",
object: "chat.completion",
created: 1,
model: "MainAgent",
choices: [
{
index: 0,
finish_reason: "stop",
message: {
role: "assistant",
content:
'Vou verificar agora.\n\nto=functions.run_in_terminal tokenjson\n{"command":"pwd","explanation":"Teste","goal":"Teste","mode":"sync","isBackground":false,"timeout":120000}\n\nResumo final.',
},
},
],
}) as any;
const message = sanitized.choices[0].message;
assert.equal(message.content, "Vou verificar agora.\n\nResumo final.");
assert.equal(JSON.stringify(sanitized).includes("to=functions.run_in_terminal"), false);
assert.equal(JSON.stringify(sanitized).includes('"command":"pwd"'), false);
});
test("sanitizeResponsesApiResponse strips leaked multi_tool_use envelopes from Responses output_text", () => {
const sanitized = sanitizeResponsesApiResponse({
id: "resp_internal_tool_envelope",
object: "response",
created_at: 1,
model: "gpt-5.1-codex",
status: "completed",
output: [
{
id: "msg_1",
type: "message",
role: "assistant",
content: [
{
type: "output_text",
text: 'Antes.\n\nto=multi_tool_use.parallel junkjson\n{"tool_uses":[{"recipient_name":"functions.read_file","parameters":{"filePath":"/tmp/a","startLine":1,"endLine":10}}]}\n\nDepois.',
annotations: [],
},
],
},
],
}) as any;
assert.equal(sanitized.output[0].content[0].text, "Antes.\n\nDepois.");
assert.equal(sanitized.output_text, "Antes.\n\nDepois.");
assert.equal(JSON.stringify(sanitized).includes("to=multi_tool_use.parallel"), false);
assert.equal(JSON.stringify(sanitized).includes("recipient_name"), false);
});