mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
* fix(responses): escape literal control chars in tool call JSON; emit status=failed on upstream error #6785 Two bugfixes in the Responses API translator: 1. escapeJsonStringValues() sanitizes tool call arguments containing literal 0x0A/0x0D/0x09 bytes (emitted by Gemma4 models) into valid JSON \n/\r/\t escapes, preventing SSE framing corruption. Only escapes inside JSON string contexts — already-escaped sequences and structural JSON pass through unchanged. 2. sendCompleted() checks state.upstreamError and emits status="failed" with error.code + error.message instead of silently hardcoding status="completed" + error=null, so mid-stream errors (e.g. Gemini 503 after partial content) are properly surfaced to the client. 3. stream.ts: calls translateResponse(null,...) before controller.error() so the translator can emit close events (reasoning item done, response.completed) before the stream is terminated. * test(boundary): fix ESLint no-explicit-any warnings and quality gates Green the PR against release/v3.8.47 quality gates without weakening tests: - Replace @typescript-eslint/no-explicit-any in the new boundary/gemma4 tests with proper interfaces (ResponseBody, ToolDef, ToolArgs, SseEvent item accessors) — fixes the "No new ESLint warnings" gate. - Split tests/unit/translator-resp-openai-responses.test.ts (1079 LOC) by extracting the round-trip suite into a sibling file so both stay under the 800-line test cap — fixes check:file-size. - Rename the 5 live boundary tests to *.live.test.ts, gate them behind RUN_BOUNDARY_LIVE=1, add a test:boundary:live npm script and register the glob in check-test-discovery COLLECTORS — fixes check:test-discovery (they hit a live remote and must never run unopted in CI). Co-authored-by: Markus Hartung <mail@hartmark.se> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
141 lines
5.6 KiB
TypeScript
141 lines
5.6 KiB
TypeScript
/**
|
|
* tests/integration/gemini-tool-call-escaping.test.ts
|
|
*
|
|
* Live integration tests verifying tool call argument escaping through
|
|
* the Chat Completions endpoint, the Responses API endpoint, and the
|
|
* OmniRoute combo routing engine — both streaming and non-streaming.
|
|
*
|
|
* Gemma4 models emit literal 0x0A bytes in functionCall.args string values.
|
|
* OmniRoute's translator must escape these into valid JSON \n sequences.
|
|
*
|
|
* Environment:
|
|
* OMNIROUTE_API_KEY — required (else tests skip)
|
|
* OMNIROUTE_URL — defaults to http://localhost:3000
|
|
* TEST_DELAY_MS — delay between tests, defaults to 5000
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
skip,
|
|
MODEL,
|
|
ensureTestEnvironment,
|
|
DELAY_BETWEEN_REQUESTS_MS,
|
|
ToolCall,
|
|
ChatResponse,
|
|
ResponsesToolCall,
|
|
ResponsesResponse,
|
|
TOOL_DEFINITION,
|
|
extractToolCalls,
|
|
extractToolCallsFromResponses,
|
|
validateToolCallArguments,
|
|
sendToolCallChatRequest,
|
|
sendStreamingToolCallChatRequest,
|
|
sendToolCallResponsesRequest,
|
|
sendStreamingToolCallResponsesRequest,
|
|
} from "./liveGeminiShared.ts";
|
|
|
|
const DIRECT_MODEL = "gemini/gemma-4-31b-it";
|
|
|
|
const TOOL_CALL_PROMPT =
|
|
"Use write_file to create /tmp/test.py with Python code that has a function with " +
|
|
"a for loop that prints hello 5 times. Include proper indentation.";
|
|
|
|
test.before(async () => {
|
|
await ensureTestEnvironment();
|
|
});
|
|
|
|
// ── Gemini Direct Model — Chat Completions ────────────────────────────────
|
|
|
|
test("gemini direct: tool call arguments are valid JSON", { skip }, async () => {
|
|
const data = await sendToolCallChatRequest(DIRECT_MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCalls(data);
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(` [OK] gemini direct: ${toolCalls.length} tool calls, model=${data.model}`);
|
|
});
|
|
|
|
test("gemini direct: streaming tool call produces valid JSON", { skip }, async () => {
|
|
const data = await sendStreamingToolCallChatRequest(DIRECT_MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCalls(data);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] gemini direct streaming: model did not produce a tool call");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(
|
|
` [OK] gemini direct streaming: ${toolCalls.length} tool calls, finish=${data.choices[0].finish_reason}`
|
|
);
|
|
});
|
|
|
|
// ── Gemini Direct Model — Responses API ───────────────────────────────────
|
|
|
|
test("gemini direct: responses tool call arguments are valid JSON", { skip }, async () => {
|
|
const data = await sendToolCallResponsesRequest(DIRECT_MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCallsFromResponses(data);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] gemini direct responses: no tool call in response");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(
|
|
` [OK] gemini direct responses: ${toolCalls.length} tool calls, model=${data.model}`
|
|
);
|
|
});
|
|
|
|
test("gemini direct: streaming responses tool call produces valid JSON", { skip }, async () => {
|
|
const toolCalls = await sendStreamingToolCallResponsesRequest(DIRECT_MODEL, TOOL_CALL_PROMPT);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] gemini direct streaming responses: model did not produce a tool call");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(` [OK] gemini direct streaming responses: ${toolCalls.length} tool calls`);
|
|
});
|
|
|
|
// ── OmniRoute Combo — Chat Completions ────────────────────────────────────
|
|
|
|
test("omniroute combo: tool call arguments are valid JSON", { skip }, async () => {
|
|
const data = await sendToolCallChatRequest(MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCalls(data);
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(` [OK] omniroute combo: ${toolCalls.length} tool calls, model=${data.model}`);
|
|
});
|
|
|
|
test("omniroute combo: streaming tool call produces valid JSON", { skip }, async () => {
|
|
const data = await sendStreamingToolCallChatRequest(MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCalls(data);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] omniroute combo streaming: model did not produce a tool call");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(
|
|
` [OK] omniroute combo streaming: ${toolCalls.length} tool calls, finish=${data.choices[0].finish_reason}`
|
|
);
|
|
});
|
|
|
|
// ── OmniRoute Combo — Responses API ───────────────────────────────────────
|
|
|
|
test("omniroute combo: responses tool call arguments are valid JSON", { skip }, async () => {
|
|
const data = await sendToolCallResponsesRequest(MODEL, TOOL_CALL_PROMPT);
|
|
const toolCalls = extractToolCallsFromResponses(data);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] omniroute combo responses: no tool call in response");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(
|
|
` [OK] omniroute combo responses: ${toolCalls.length} tool calls, model=${data.model}`
|
|
);
|
|
});
|
|
|
|
test("omniroute combo: streaming responses tool call produces valid JSON", { skip }, async () => {
|
|
const toolCalls = await sendStreamingToolCallResponsesRequest(MODEL, TOOL_CALL_PROMPT);
|
|
if (toolCalls.length === 0) {
|
|
console.log(" [skip] omniroute combo streaming responses: model did not produce a tool call");
|
|
return;
|
|
}
|
|
validateToolCallArguments(toolCalls);
|
|
console.log(` [OK] omniroute combo streaming responses: ${toolCalls.length} tool calls`);
|
|
});
|