From 88e666e59690a3909cf54bead9894dd4580bc27f Mon Sep 17 00:00:00 2001 From: Rafa Martins <146174365+rafacpti23@users.noreply.github.com> Date: Sat, 19 Sep 2026 00:02:30 -0300 Subject: [PATCH] fix(sse): fix double-escaped tabs in Codex JSON tool call arguments (#12841) Fixes #12831. When the Codex upstream model produces string values in tool arguments that contain double-escaped tabs (\t inside the JSON string instead of \t), the parser outputs literal backslash-t characters. This breaks editor patches that rely on proper indentation. This commit adds a fixDoubleEscapedTabs sanitization step before parsing to restore them to single tabs. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- ...841-double-escaped-tabs-codex-tool-args.md | 1 + .../translator/response/openai-responses.ts | 45 +++++++++- tests/unit/bug-12831.test.ts | 90 +++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/12841-double-escaped-tabs-codex-tool-args.md create mode 100644 tests/unit/bug-12831.test.ts diff --git a/changelog.d/fixes/12841-double-escaped-tabs-codex-tool-args.md b/changelog.d/fixes/12841-double-escaped-tabs-codex-tool-args.md new file mode 100644 index 0000000000..289e4ef043 --- /dev/null +++ b/changelog.d/fixes/12841-double-escaped-tabs-codex-tool-args.md @@ -0,0 +1 @@ +- **fix(sse):** stop over-escaped tabs from `gpt-5.6-luna-xhigh` corrupting Codex tool-call arguments — `\\t` is now collapsed back to a real tab instead of a literal `\t` text ([#12841](https://github.com/diegosouzapw/OmniRoute/pull/12841)) — thanks @rafacpti23 diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index cd1310bffd..0a840320c6 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -114,6 +114,49 @@ function escapeJsonStringValues(json: string, escapeState: JsonStringEscapeState return result; } +/** + * Collapse double-escaped tab sequences inside JSON string values. + * Some providers (e.g. gpt-5.6-luna-xhigh, #12831) over-escape a tab when + * emitting tool call argument JSON: instead of the single valid JSON escape + * `\t` (backslash + t), they emit `\\t` (backslash + backslash + t) inside + * the string value. JSON.parse then decodes that to a literal two-character + * `\t` text (backslash followed by the letter t) instead of an actual tab + * character, which breaks consumers (e.g. editor patches) expecting real + * tabs. This only rewrites the over-escaped form and leaves an + * already-correct single escape untouched. + */ +function fixDoubleEscapedTabs(json: string): string { + let result = ""; + let inString = false; + + for (let i = 0; i < json.length; i++) { + const ch = json[i]; + + if (inString && ch === "\\" && json[i + 1] === "\\" && json[i + 2] === "t") { + result += "\\t"; + i += 2; + continue; + } + + // Inside a string, leave any other escape sequence untouched. + if (inString && ch === "\\") { + result += ch + (json[i + 1] ?? ""); + i++; + continue; + } + + if (ch === '"') { + result += ch; + inString = !inString; + continue; + } + + result += ch; + } + + return result; +} + /** * Translate OpenAI chunk to Responses API events * @returns {Array} Array of events with { event, data } structure @@ -589,7 +632,7 @@ function emitToolCall(state, emit, tc) { state.funcArgsEscapeState[tcIdx] = createJsonStringEscapeState(); } const sanitized = escapeJsonStringValues( - tc.function.arguments, + fixDoubleEscapedTabs(tc.function.arguments), state.funcArgsEscapeState[tcIdx] ); const nextArgs = appendToolCallArgumentDelta(existingArgs, sanitized); diff --git a/tests/unit/bug-12831.test.ts b/tests/unit/bug-12831.test.ts new file mode 100644 index 0000000000..2a73c7a903 --- /dev/null +++ b/tests/unit/bug-12831.test.ts @@ -0,0 +1,90 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { openaiToOpenAIResponsesResponse } from "../../open-sse/translator/response/openai-responses.ts"; + +test("Issue #12831: fixes double-escaped tabs in Codex JSON tool call arguments", () => { + const events = []; + const emit = (_name, payload) => events.push(payload); + const state = { + responseId: "res_123", + funcCallIds: {}, + funcNames: {}, + funcArgsBuf: {}, + funcArgsDone: {}, + funcItemAdded: {}, + funcItemDone: {}, + msgItemAdded: {}, + msgContentAdded: {}, + msgTextBuf: {}, + msgItemDone: {}, + }; + + const chunk1 = { + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: "call_123", + function: { + name: "_edit", + // gpt-5.6-luna-xhigh emits literally \ followed by t in the JSON string + // to represent a tab, instead of a JSON escape for tab or a raw tab. + // Wait, in JSON, a tab in a string is encoded as "\t" (two characters: \ and t). + // If it's double-escaped, it emits "\t" (four characters: \, \, t in JSON string? No, two backslashes and a t: "\t") + // Let's assume the string is: {"input": "some code\twith tabs"} + arguments: '{\n "input": "some code\\twith tabs"', + }, + }, + ], + }, + }, + ], + }; + + const chunk2 = { + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + function: { + arguments: "\n}", + }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + }; + + const chunk3 = { + usage: { prompt_tokens: 10, completion_tokens: 10 }, + }; + + function processChunk(chunk) { + const chunkEvents = openaiToOpenAIResponsesResponse(chunk, state); + for (const ev of chunkEvents) { + emit(ev.event, ev.data); + } + } + + processChunk(chunk1); + processChunk(chunk2); + processChunk(chunk3); + + const doneEvent = events.find((e) => e.type === "response.function_call_arguments.done"); + + // Try parsing the arguments + const parsed = JSON.parse(doneEvent.arguments); + assert.strictEqual( + parsed.input, + "some code\twith tabs", + "The double-escaped tab should be unescaped to a single tab character" + ); +});