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
20 changes: 19 additions & 1 deletion apps/web/src/lib/rewriteModelResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,25 @@ describe('request log capture', () => {
const reader = result.body?.getReader();
await reader?.cancel();

expect(capture.setReadError).toHaveBeenCalled();
expect(capture.setReadError).toHaveBeenCalledWith(expect.any(Error), undefined);
expect(capture.setBody).not.toHaveBeenCalled();
});

test.each(rewriters)(
'%s: records the chunks received before the response stream is cancelled',
async (_name, rewrite) => {
const capture = makeCapture();
const receivedChunks = 'data: {"id":"gen-1","choices":[]}\n\n';
const { response: upstream } = hangingSseResponse(receivedChunks);

const result = await rewrite(upstream, true, capture, null);
const reader = result.body?.getReader();
await reader?.read();
await reader?.cancel();

expect(capture.setReadError).toHaveBeenCalledTimes(1);
expect(capture.setReadError).toHaveBeenCalledWith(expect.any(Error), receivedChunks);
expect(capture.setBody).not.toHaveBeenCalled();
}
);
});
54 changes: 40 additions & 14 deletions apps/web/src/lib/rewriteModelResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ async function readResponseText(
}
}

function partialCapturedBody(capturedChunks: string[] | null): string | undefined {
return capturedChunks && capturedChunks.length > 0 ? capturedChunks.join('') : undefined;
}

async function rewriteSseStream(
reader: ReadableStreamDefaultReader<Uint8Array>,
parser: ReturnType<typeof createParser>,
Expand All @@ -272,17 +276,12 @@ async function rewriteSseStream(
serializeError: (error: ResponseReadError) => string,
onFinally: () => void,
vercelRequestId: string | null | undefined,
capture: RequestLogCapture | null
capture: RequestLogCapture | null,
capturedChunks: string[] | null
) {
const decoder = new TextDecoder();
// Accumulate the raw upstream text for request logging while the stream is
// being processed anyway, so it doesn't have to be processed a second time.
const capturedChunks: string[] | null = capture ? [] : null;
const settleReadError = (error: unknown) =>
capture?.setReadError(
error,
capturedChunks && capturedChunks.length > 0 ? capturedChunks.join('') : undefined
);
capture?.setReadError(error, partialCapturedBody(capturedChunks));
const settleBody = () => {
if (capturedChunks) {
capturedChunks.push(decoder.decode());
Expand Down Expand Up @@ -398,6 +397,11 @@ export async function rewriteModelResponse_ChatCompletions(
});
}

// Accumulate the raw upstream text for request logging while the stream is
// being processed anyway, so it doesn't have to be processed a second time.
// Shared with the stream's cancel() callback so a client disconnect still
// logs the partially received response body.
const capturedChunks: string[] | null = capture ? [] : null;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body?.getReader();
Expand Down Expand Up @@ -480,11 +484,15 @@ export async function rewriteModelResponse_ChatCompletions(
'\n\n',
progress.stop,
vercelRequestId,
capture
capture,
capturedChunks
);
},
cancel() {
capture?.setReadError(new Error('response stream was cancelled'));
capture?.setReadError(
new Error('response stream was cancelled'),
partialCapturedBody(capturedChunks)
);
},
});

Expand Down Expand Up @@ -559,6 +567,11 @@ export async function rewriteModelResponse_Messages(
});
}

// Accumulate the raw upstream text for request logging while the stream is
// being processed anyway, so it doesn't have to be processed a second time.
// Shared with the stream's cancel() callback so a client disconnect still
// logs the partially received response body.
const capturedChunks: string[] | null = capture ? [] : null;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body?.getReader();
Expand Down Expand Up @@ -648,11 +661,15 @@ export async function rewriteModelResponse_Messages(
'\n\n',
progress.stop,
vercelRequestId,
capture
capture,
capturedChunks
);
},
cancel() {
capture?.setReadError(new Error('response stream was cancelled'));
capture?.setReadError(
new Error('response stream was cancelled'),
partialCapturedBody(capturedChunks)
);
},
});

Expand Down Expand Up @@ -708,6 +725,11 @@ export async function rewriteModelResponse_Responses(
});
}

// Accumulate the raw upstream text for request logging while the stream is
// being processed anyway, so it doesn't have to be processed a second time.
// Shared with the stream's cancel() callback so a client disconnect still
// logs the partially received response body.
const capturedChunks: string[] | null = capture ? [] : null;
const stream = new ReadableStream({
async start(controller) {
const reader = response.body?.getReader();
Expand Down Expand Up @@ -792,11 +814,15 @@ export async function rewriteModelResponse_Responses(
'\n\n',
progress.stop,
vercelRequestId,
capture
capture,
capturedChunks
);
},
cancel() {
capture?.setReadError(new Error('response stream was cancelled'));
capture?.setReadError(
new Error('response stream was cancelled'),
partialCapturedBody(capturedChunks)
);
},
});

Expand Down