Skip to content

Commit 554c7bd

Browse files
authored
Filter empty text parts when streaming (#8736)
* Filter empty text parts when streaming * Add changeset * Remove unused filterEmptyTextParts() * Move logic into a function * final * dont throw if mock response file isn't found * update responses version to 6 * Only ignore empty text parts in the aggregated response. * review fixes * Throw in `aggregateResponses` if `newPart` has no properties
1 parent c8e5b3e commit 554c7bd

File tree

4 files changed

+84
-4
lines changed

4 files changed

+84
-4
lines changed

.changeset/seven-oranges-care.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/vertexai': patch
3+
---
4+
5+
Filter out empty text parts from streaming responses.

packages/vertexai/src/requests/stream-reader.test.ts

+65-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import {
3333
GenerateContentResponse,
3434
HarmCategory,
3535
HarmProbability,
36-
SafetyRating
36+
SafetyRating,
37+
VertexAIErrorCode
3738
} from '../types';
39+
import { VertexAIError } from '../errors';
3840

3941
use(sinonChai);
4042

@@ -220,6 +222,23 @@ describe('processStream', () => {
220222
}
221223
expect(foundCitationMetadata).to.be.true;
222224
});
225+
it('removes empty text parts', async () => {
226+
const fakeResponse = getMockResponseStreaming(
227+
'streaming-success-empty-text-part.txt'
228+
);
229+
const result = processStream(fakeResponse as Response);
230+
const aggregatedResponse = await result.response;
231+
expect(aggregatedResponse.text()).to.equal('1');
232+
expect(aggregatedResponse.candidates?.length).to.equal(1);
233+
expect(aggregatedResponse.candidates?.[0].content.parts.length).to.equal(1);
234+
235+
// The chunk with the empty text part will still go through the stream
236+
let numChunks = 0;
237+
for await (const _ of result.stream) {
238+
numChunks++;
239+
}
240+
expect(numChunks).to.equal(2);
241+
});
223242
});
224243

225244
describe('aggregateResponses', () => {
@@ -403,4 +422,49 @@ describe('aggregateResponses', () => {
403422
).to.equal(150);
404423
});
405424
});
425+
426+
it('throws if a part has no properties', () => {
427+
const responsesToAggregate: GenerateContentResponse[] = [
428+
{
429+
candidates: [
430+
{
431+
index: 0,
432+
content: {
433+
role: 'user',
434+
parts: [{} as any] // Empty
435+
},
436+
finishReason: FinishReason.STOP,
437+
finishMessage: 'something',
438+
safetyRatings: [
439+
{
440+
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
441+
probability: HarmProbability.NEGLIGIBLE
442+
} as SafetyRating
443+
]
444+
}
445+
],
446+
promptFeedback: {
447+
blockReason: BlockReason.SAFETY,
448+
safetyRatings: [
449+
{
450+
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
451+
probability: HarmProbability.LOW
452+
} as SafetyRating
453+
]
454+
}
455+
}
456+
];
457+
458+
try {
459+
aggregateResponses(responsesToAggregate);
460+
} catch (e) {
461+
expect((e as VertexAIError).code).includes(
462+
VertexAIErrorCode.INVALID_CONTENT
463+
);
464+
expect((e as VertexAIError).message).to.include(
465+
'Part should have at least one property, but there are none. This is likely caused ' +
466+
'by a malformed response from the backend.'
467+
);
468+
}
469+
});
406470
});

packages/vertexai/src/requests/stream-reader.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ async function getResponsePromise(
6262
);
6363
return enhancedResponse;
6464
}
65+
6566
allResponses.push(value);
6667
}
6768
}
@@ -184,14 +185,24 @@ export function aggregateResponses(
184185
}
185186
const newPart: Partial<Part> = {};
186187
for (const part of candidate.content.parts) {
187-
if (part.text) {
188+
if (part.text !== undefined) {
189+
// The backend can send empty text parts. If these are sent back
190+
// (e.g. in chat history), the backend will respond with an error.
191+
// To prevent this, ignore empty text parts.
192+
if (part.text === '') {
193+
continue;
194+
}
188195
newPart.text = part.text;
189196
}
190197
if (part.functionCall) {
191198
newPart.functionCall = part.functionCall;
192199
}
193200
if (Object.keys(newPart).length === 0) {
194-
newPart.text = '';
201+
throw new VertexAIError(
202+
VertexAIErrorCode.INVALID_CONTENT,
203+
'Part should have at least one property, but there are none. This is likely caused ' +
204+
'by a malformed response from the backend.'
205+
);
195206
}
196207
aggregatedResponse.candidates[i].content.parts.push(
197208
newPart as Part

scripts/update_vertexai_responses.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# This script replaces mock response files for Vertex AI unit tests with a fresh
1818
# clone of the shared repository of Vertex AI test data.
1919

20-
RESPONSES_VERSION='v5.*' # The major version of mock responses to use
20+
RESPONSES_VERSION='v6.*' # The major version of mock responses to use
2121
REPO_NAME="vertexai-sdk-test-data"
2222
REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git"
2323

0 commit comments

Comments
 (0)