fix(commandcode): replay raw byte chunks to preserve all NDJSON lines
This commit is contained in:
committed by
decolua
parent
5d2cfbf3c5
commit
c2148179c0
@@ -141,7 +141,7 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
const reader = originalResponse.body.getReader();
|
const reader = originalResponse.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
const bufferedLines = [];
|
const rawChunks = [];
|
||||||
let detectedError = null;
|
let detectedError = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -155,16 +155,15 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
const parsed = JSON.parse(jsonStr);
|
const parsed = JSON.parse(jsonStr);
|
||||||
if (parsed?.type === "error") {
|
if (parsed?.type === "error") {
|
||||||
detectedError = parsed;
|
detectedError = parsed;
|
||||||
} else {
|
|
||||||
bufferedLines.push(trimmed);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
bufferedLines.push(trimmed);
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawChunks.push(value);
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = buffer.split("\n");
|
const lines = buffer.split("\n");
|
||||||
buffer = lines.pop() || "";
|
buffer = lines.pop() || "";
|
||||||
@@ -175,7 +174,6 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
const jsonStr = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
|
const jsonStr = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
|
||||||
if (!jsonStr || jsonStr === "[DONE]") {
|
if (!jsonStr || jsonStr === "[DONE]") {
|
||||||
bufferedLines.push(trimmed);
|
|
||||||
stopLoop = true;
|
stopLoop = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -184,7 +182,6 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
try {
|
try {
|
||||||
event = JSON.parse(jsonStr);
|
event = JSON.parse(jsonStr);
|
||||||
} catch {
|
} catch {
|
||||||
bufferedLines.push(trimmed);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,8 +191,6 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferedLines.push(trimmed);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event?.type === "text-delta" ||
|
event?.type === "text-delta" ||
|
||||||
event?.type === "reasoning-delta" ||
|
event?.type === "reasoning-delta" ||
|
||||||
@@ -238,29 +233,18 @@ export async function inspectAndWrapCommandCodeResponse(originalResponse, model)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const combinedStream = createReplayedStream(bufferedLines, buffer, reader);
|
const combinedStream = createRawReplayedStream(rawChunks, reader);
|
||||||
return wrapNdjsonAsOpenAISse(combinedStream, model, originalResponse);
|
return wrapNdjsonAsOpenAISse(combinedStream, model, originalResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReplayedStream(bufferedLines, remainingBuffer, reader) {
|
function createRawReplayedStream(rawChunks, reader) {
|
||||||
const encoder = new TextEncoder();
|
let chunkIndex = 0;
|
||||||
let replayed = false;
|
|
||||||
|
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
if (!replayed) {
|
if (chunkIndex < rawChunks.length) {
|
||||||
replayed = true;
|
controller.enqueue(rawChunks[chunkIndex++]);
|
||||||
let prefix = bufferedLines.join("\n");
|
return;
|
||||||
if (prefix && remainingBuffer) {
|
|
||||||
prefix += "\n" + remainingBuffer;
|
|
||||||
} else if (remainingBuffer) {
|
|
||||||
prefix = remainingBuffer;
|
|
||||||
} else if (prefix) {
|
|
||||||
prefix += "\n";
|
|
||||||
}
|
|
||||||
if (prefix) {
|
|
||||||
controller.enqueue(encoder.encode(prefix));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -133,6 +133,30 @@ describe("inspectAndWrapCommandCodeResponse", () => {
|
|||||||
expect(text).toContain("data: [DONE]");
|
expect(text).toContain("data: [DONE]");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves all lines in a multi-line packet when inspecting tool-input-start", async () => {
|
||||||
|
const packet = [
|
||||||
|
JSON.stringify({ type: "start" }),
|
||||||
|
JSON.stringify({ type: "start-step" }),
|
||||||
|
JSON.stringify({ type: "tool-input-start", id: "call_1", toolName: "terminal" }),
|
||||||
|
JSON.stringify({ type: "tool-input-delta", id: "call_1", delta: '{"command": "ls"}' }),
|
||||||
|
JSON.stringify({ type: "finish-step", finishReason: "tool-calls" }),
|
||||||
|
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
|
||||||
|
].join("\n") + "\n";
|
||||||
|
|
||||||
|
const ndjsonBody = createNdjsonStream([packet]);
|
||||||
|
|
||||||
|
const fakeResponse = new Response(ndjsonBody, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "text/event-stream" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await inspectAndWrapCommandCodeResponse(fakeResponse, "cmc/deepseek/deepseek-v4.1-flash");
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
const text = await result.text();
|
||||||
|
expect(text).toContain('"name":"terminal"');
|
||||||
|
expect(text).toContain('"arguments":"{\\"command\\": \\"ls\\"}"');
|
||||||
|
});
|
||||||
|
|
||||||
it("retries when initial stream yields an error and succeeds on second attempt", async () => {
|
it("retries when initial stream yields an error and succeeds on second attempt", async () => {
|
||||||
let callCount = 0;
|
let callCount = 0;
|
||||||
const executor = new CommandCodeExecutor();
|
const executor = new CommandCodeExecutor();
|
||||||
|
|||||||
Reference in New Issue
Block a user