diff --git a/src/providers/google-vertex-ai/chatComplete.ts b/src/providers/google-vertex-ai/chatComplete.ts index eda2f61f3..91a5c113e 100644 --- a/src/providers/google-vertex-ai/chatComplete.ts +++ b/src/providers/google-vertex-ai/chatComplete.ts @@ -708,7 +708,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( ) : null; let message: any = { role: 'assistant', content: '' }; - if (generation.content?.parts[0]?.text) { + if (generation.content?.parts?.[0]?.text) { const contentBlocks = []; let content = ''; for (const part of generation.content.parts) { @@ -732,7 +732,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( ...(!strictOpenAiCompliance && contentBlocks.length && { content_blocks: contentBlocks }), }; - } else if (generation.content?.parts[0]?.functionCall) { + } else if (generation.content?.parts?.[0]?.functionCall) { message = { role: 'assistant', tool_calls: generation.content.parts.map((part, idx) => { @@ -753,7 +753,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( } }), }; - } else if (generation.content?.parts[0]?.inlineData) { + } else if (generation.content?.parts?.[0]?.inlineData) { const part = generation.content.parts[0]; const contentBlocks = [ { diff --git a/src/providers/google/chatComplete.test.ts b/src/providers/google/chatComplete.test.ts new file mode 100644 index 000000000..9f04a0ec3 --- /dev/null +++ b/src/providers/google/chatComplete.test.ts @@ -0,0 +1,37 @@ +import { GoogleChatCompleteStreamChunkTransform } from './chatComplete'; + +describe('GoogleChatCompleteStreamChunkTransform', () => { + // Gemini emits a terminating stream chunk whose candidate carries a + // `finishReason` and a `content` object that has NO `parts` array + // (e.g. finishReason "SAFETY" / "RECITATION"). The transform must not crash. + it('does not throw when candidate content has no parts array', () => { + const chunk = + 'data: ' + + JSON.stringify({ + candidates: [ + { content: { role: 'model' }, finishReason: 'SAFETY', index: 0 }, + ], + usageMetadata: { + promptTokenCount: 10, + candidatesTokenCount: 0, + totalTokenCount: 10, + }, + modelVersion: 'gemini-1.5-pro', + }); + + const streamState: any = {}; + let output: string = ''; + expect(() => { + output = GoogleChatCompleteStreamChunkTransform( + chunk, + 'fb-id', + streamState, + false + ); + }).not.toThrow(); + + const json = JSON.parse(output.replace(/^data: /, '').trim()); + expect(json.choices[0].finish_reason).toBe('SAFETY'); + expect(json.choices[0].delta.content).toBe(''); + }); +}); diff --git a/src/providers/google/chatComplete.ts b/src/providers/google/chatComplete.ts index b550b05f5..567a7973a 100644 --- a/src/providers/google/chatComplete.ts +++ b/src/providers/google/chatComplete.ts @@ -812,7 +812,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( strictOpenAiCompliance ) : null; - if (generation.content?.parts[0]?.text) { + if (generation.content?.parts?.[0]?.text) { const contentBlocks = []; let content = ''; for (const part of generation.content.parts) { @@ -836,7 +836,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( ...(!strictOpenAiCompliance && contentBlocks.length && { content_blocks: contentBlocks }), }; - } else if (generation.content?.parts[0]?.functionCall) { + } else if (generation.content?.parts?.[0]?.functionCall) { message = { role: 'assistant', tool_calls: generation.content.parts.map((part, idx) => { @@ -857,7 +857,7 @@ export const GoogleChatCompleteStreamChunkTransform: ( } }), }; - } else if (generation.content?.parts[0]?.inlineData) { + } else if (generation.content?.parts?.[0]?.inlineData) { const part = generation.content.parts[0]; const contentBlocks = [ {