Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
"open-sse/services/combo.ts": 3648,
"open-sse/services/compression/strategySelector.ts": 1060,
"open-sse/services/rateLimitManager.ts": 1167,
"open-sse/translator/response/openai-responses.ts": 1224,
"open-sse/translator/response/openai-responses.ts": 1249,
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
"open-sse/utils/stream.ts": 2889,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1388,
Expand Down Expand Up @@ -426,9 +426,6 @@
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.",
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
"_rebaseline_2026_07_25_dario_upstream_proxy_selector": "PR #8523 (Dario embedded service): upstream-proxy mode selector replaces the binary CLIProxyAPI toggle with Native/CLIProxyAPI/Dario/Fallback + a fallback-backend picker. ProviderDetailPageClient.tsx 798->804 (+6, new hook fields threaded through to ConnectionsListPanel), ConnectionRow.tsx 942->958 (+16, the mode <select> + conditional fallback-backend <select> replacing a single pill button), useProviderConnections.ts 954->986 (+32, upstreamProxyMode/upstreamProxyFallbackBackend state + handleSetUpstreamProxyMode, handleToggleCliproxyapiMode kept as a thin backward-compat wrapper for the existing hook-shape test). All additive UI/state for the new modes — no unrelated refactor.",
"_rebaseline_2026_08_02_9242_token_health_transient": "PR #9242 (fix/refresh-circuit-transient): src/lib/tokenHealthCheck.ts 1021 (new file, above cap 1000). The file consolidates token-refresh health checking logic that was previously scattered across auth.ts and tokenRefresh.ts. Cohesive single-responsibility module for refresh circuit state management; not extractable without splitting the refresh state machine. Covered by tests/unit/tokenHealthCheck-transient.test.ts.",
"_rebaseline_2026_07_28_8870_firefly_ref_cap_timeout": "PR #8870 (fix/adobe-firefly-gpt-ref-cap-timeout) own growth: adobeFireflyClient.ts 2322->2385 (+63 = gpt-image subject-ref hard cap at 2 + adaptive poll timeout budget (base 300s + 60s/ref, max 600s) + defensive .slice on referenceBlobs for gpt/nano/generic families). Fixes live 504s on multi-screenshot listing jobs (Featured Promo / Box Art) where 3–4+ subject refs stall colligo until the old 180s poll budget expires. Helpers adobeFireflyMaxImageRefs/adobeFireflyImageTimeoutMs live next to the existing payload/poll chokepoint (not extractable without splitting the wire recipe mid-PR). Covered by tests/unit/adobe-firefly.test.ts (ref-cap + timeout cases). Structural shrink tracked in #3501.",
Expand Down
1 change: 1 addition & 0 deletions open-sse/translator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ export function initState(sourceFormat) {
inThinking: false,
parseTextualReasoningTags: false,
funcArgsBuf: {},
funcArgsEscapeState: {},
funcNames: {},
funcCallIds: {},
funcArgsDone: {},
Expand Down
59 changes: 53 additions & 6 deletions open-sse/translator/response/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,62 @@ import {
// normalizeUpstreamFailure is re-exported for external importers (tests).
export { normalizeUpstreamFailure } from "./openai-responses/pureHelpers.ts";

/** Carries escapeJsonStringValues's scan state (whether we're inside a JSON
* string, and whether the fragment ended mid-escape-sequence) across calls
* for the SAME tool call — see escapeJsonStringValues's own doc comment for
* why this must persist across chunks rather than reset per call. */
interface JsonStringEscapeState {
inString: boolean;
pendingEscape: boolean;
}

function createJsonStringEscapeState(): JsonStringEscapeState {
return { inString: false, pendingEscape: false };
}

/**
* Escape control characters (newlines, tabs, carriage returns) that appear
* inside JSON string values, ensuring the resulting string is valid JSON.
* This handles upstream providers (e.g. Gemini/Gemma) that emit literal
* newlines (0x0A) instead of \n escapes inside tool call argument JSON.
* Only escapes characters inside string contexts to avoid double-escaping
* already-proper JSON or corrupting structural newlines.
*
* `arguments` deltas arrive as arbitrary fragments of one continuous JSON
* string (OpenAI's Chat Completions streaming contract only guarantees each
* `tool_calls[].function.arguments` delta is the next slice, not that it
* starts/ends on a quote or escape boundary) — a large multi-line argument
* value routinely gets split mid-string. `escapeState` must therefore be the
* SAME object passed in on every call for a given tool call index, not a
* fresh `{inString: false}` each time: resetting per call made the
* in-string/out-of-string decision (and therefore whether a raw newline
* gets escaped) depend on where a chunk boundary happened to fall, which
* produced a real, reported bug — a single reassembled arguments string
* with a mix of real newlines and literal two-character `\n` sequences,
* breaking generated code (e.g. Python) that embeds multi-line content.
*/
function escapeJsonStringValues(json: string): string {
function escapeJsonStringValues(json: string, escapeState: JsonStringEscapeState): string {
let result = "";
let inString = false;
let { inString, pendingEscape } = escapeState;

for (let i = 0; i < json.length; i++) {
const ch = json[i];

// Inside a string, skip over escape sequences
// This char is the one immediately following a backslash from a
// previous iteration (possibly in a prior fragment) — it's already
// "consumed" by that escape sequence, pass it through untouched.
if (pendingEscape) {
result += ch;
pendingEscape = false;
continue;
}

// Inside a string, an unescaped backslash starts an escape sequence —
// the char AFTER it (next iteration, possibly in the next fragment)
// must not be reinterpreted as a quote/control-char in its own right.
if (inString && ch === "\\") {
result += ch + (json[i + 1] ?? "");
i++;
result += ch;
pendingEscape = true;
continue;
}

Expand All @@ -69,6 +106,8 @@ function escapeJsonStringValues(json: string): string {
result += ch;
}

escapeState.inString = inString;
escapeState.pendingEscape = pendingEscape;
return result;
}

Expand Down Expand Up @@ -482,6 +521,7 @@ function emitToolCall(state, emit, tc) {
delete state.funcArgsDone[tcIdx];
delete state.funcItemAdded[tcIdx];
delete state.funcItemDone[tcIdx];
delete state.funcArgsEscapeState?.[tcIdx];
}

if (funcName) state.funcNames[tcIdx] = funcName;
Expand Down Expand Up @@ -528,7 +568,14 @@ function emitToolCall(state, emit, tc) {
if (tc.function?.arguments) {
const refCallId = state.funcCallIds[tcIdx] || newCallId;
const existingArgs = state.funcArgsBuf[tcIdx] || "";
const sanitized = escapeJsonStringValues(tc.function.arguments);
if (!state.funcArgsEscapeState) state.funcArgsEscapeState = {};
if (!state.funcArgsEscapeState[tcIdx]) {
state.funcArgsEscapeState[tcIdx] = createJsonStringEscapeState();
}
const sanitized = escapeJsonStringValues(
tc.function.arguments,
state.funcArgsEscapeState[tcIdx]
);
const nextArgs = appendToolCallArgumentDelta(existingArgs, sanitized);
const emittedDelta = nextArgs.slice(existingArgs.length);
state.funcArgsBuf[tcIdx] = nextArgs;
Expand Down
133 changes: 133 additions & 0 deletions tests/unit/translator-resp-openai-responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,139 @@ test("OpenAI -> Responses: Python multi-line content with indentation survives t
assert.ok(newlineCount > 5, "should have many actual newlines in Python code");
});

test("OpenAI -> Responses: a raw newline byte split across two tool-call argument deltas (fragment boundary lands mid-string, not on a quote/escape) is still escaped correctly", () => {
// Real reported bug: escapeJsonStringValues used to track "are we inside a
// JSON string" as a LOCAL variable reset on every call instead of state
// persisted across chunks for the same tool call. A provider that sends a
// raw newline byte (0x0A, not a proper \n escape — Gemini/Gemma-style) mid
// fragment worked fine when the whole arguments string arrived in one
// chunk, but broke the moment the SSE stream happened to split the
// fragment somewhere that wasn't a quote or a complete escape sequence:
// the second fragment's call started fresh with inString=false even
// though the true position was still inside the "content" string value,
// so the raw newline in fragment 2 was never escaped — producing invalid
// JSON that JSON.parse rejects outright ("Bad control character in
// string literal").
const events = collectEvents([
{
id: "chatcmpl-split-nl",
model: "gemma-4-26b-a4b-it",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_split_nl",
type: "function",
function: {
name: "write",
// Fragment 1 ends mid-string (no closing quote, no
// trailing backslash) — this is the boundary that
// exposed the bug.
arguments: '{"path":"/tmp/x.txt","content":"line1',
},
},
],
},
finish_reason: null,
},
],
},
{
id: "chatcmpl-split-nl",
model: "gemma-4-26b-a4b-it",
choices: [
{
index: 0,
delta: {
// Fragment 2 starts with a RAW newline byte (real \n, not the
// two-char escape) while still inside the "content" string.
tool_calls: [{ index: 0, function: { arguments: '\nline2\nline3"}' } }],
},
finish_reason: "tool_calls",
},
],
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
},
]);

const done = events.find(
(e) => e.event === "response.output_item.done" && e.data.item?.type === "function_call"
);
assert.ok(done, "should emit output_item.done for function_call");

const argsStr = done.data.item.arguments;
// The bug produced invalid JSON here (raw control character in a JSON
// string) — JSON.parse must succeed and round-trip the real newlines.
const parsed = JSON.parse(argsStr);
assert.equal(parsed.path, "/tmp/x.txt");
assert.equal(parsed.content, "line1\nline2\nline3");
});

test("OpenAI -> Responses: a properly-escaped \\n split exactly between its backslash and the 'n' across two deltas is not corrupted", () => {
// Second half of the same bug class as the test above, exercising the
// OTHER new state field (pendingEscape, not just inString): a model that
// correctly escaped a newline as the two characters `\` + `n` can still
// have that pair split across an SSE chunk boundary — fragment 1 ends
// with the lone backslash, fragment 2 starts with the "n". The old code's
// per-call reset meant fragment 2 saw a bare "n" with no idea it was the
// second half of an escape sequence; a naive re-implementation could
// easily re-escape or mis-handle it. This must reassemble to exactly one
// real newline, not a literal backslash-n or a doubled escape.
const events = collectEvents([
{
id: "chatcmpl-split-esc",
model: "gemma-4-26b-a4b-it",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_split_esc",
type: "function",
function: {
name: "write",
// Ends right after the backslash of a "\n" escape — the "n"
// itself is not yet in this fragment.
arguments: '{"path":"/tmp/y.txt","content":"before\\',
},
},
],
},
finish_reason: null,
},
],
},
{
id: "chatcmpl-split-esc",
model: "gemma-4-26b-a4b-it",
choices: [
{
index: 0,
delta: {
tool_calls: [{ index: 0, function: { arguments: 'nafter"}' } }],
},
finish_reason: "tool_calls",
},
],
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
},
]);

const done = events.find(
(e) => e.event === "response.output_item.done" && e.data.item?.type === "function_call"
);
assert.ok(done, "should emit output_item.done for function_call");

const parsed = JSON.parse(done.data.item.arguments);
assert.equal(parsed.path, "/tmp/y.txt");
assert.equal(parsed.content, "before\nafter");
});

test("OpenAI -> Responses: parallel tool calls with mixed content survive translation", () => {
const events = collectEvents([
{
Expand Down
Loading