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>
This commit is contained in:
Rafa Martins
2026-09-19 00:02:30 -03:00
committed by GitHub
parent c193595db6
commit 88e666e596
3 changed files with 135 additions and 1 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
);
});