diff --git a/packages/backend/src/ai/ai.service.spec.ts b/packages/backend/src/ai/ai.service.spec.ts index e20e2016..b810e3e1 100644 --- a/packages/backend/src/ai/ai.service.spec.ts +++ b/packages/backend/src/ai/ai.service.spec.ts @@ -60,6 +60,7 @@ const buildAiService = (): AIService => { error: jest.fn(), warn: jest.fn(), info: jest.fn(), + debug: jest.fn(), } as unknown as AIService['aiServiceLogger']; return ai; diff --git a/packages/backend/src/ai/ai.service.ts b/packages/backend/src/ai/ai.service.ts index e1dd61ab..ba7ebaaf 100644 --- a/packages/backend/src/ai/ai.service.ts +++ b/packages/backend/src/ai/ai.service.ts @@ -34,6 +34,7 @@ import type { ResponseOutputText, ResponseOutputRefusal, } from 'openai/resources/responses/responses'; +import type { Part } from '@google/genai'; import { GoogleGenAI } from '@google/genai'; interface ExtractionResult { @@ -155,10 +156,32 @@ export class AIService { }, }) .then((response) => { + this.aiServiceLogger.info('Gemini response structure:', { + hasCandidates: !!response.candidates, + candidatesLength: response.candidates?.length, + firstCandidateKeys: response.candidates?.[0] ? Object.keys(response.candidates[0]) : null, + contentKeys: response.candidates?.[0]?.content ? Object.keys(response.candidates[0].content) : null, + partsLength: response.candidates?.[0]?.content?.parts?.length, + fullResponse: JSON.stringify(response, null, 2), + }); + let imageBytes = Buffer.from([]); - response.candidates?.[0].content?.parts?.forEach((part) => { + if (!response.candidates || response.candidates.length === 0) { + this.aiServiceLogger.warn('No candidates in Gemini response'); + return ''; + } + + const parts = response.candidates[0].content?.parts; + if (!parts || parts.length === 0) { + this.aiServiceLogger.warn('No parts in first candidate'); + return ''; + } + + parts.forEach((part: Part) => { if (part.inlineData?.data) { imageBytes = Buffer.concat([imageBytes, Buffer.from(part.inlineData.data, 'base64')]); + } else { + this.aiServiceLogger.info('Part does not have inlineData.data:', Object.keys(part)); } }); @@ -215,10 +238,31 @@ export class AIService { }, }) .then((response) => { + this.aiServiceLogger.info('Gemini generateImage response structure:', { + hasCandidates: !!response.candidates, + candidatesLength: response.candidates?.length, + firstCandidateKeys: response.candidates?.[0] ? Object.keys(response.candidates[0]) : null, + contentKeys: response.candidates?.[0]?.content ? Object.keys(response.candidates[0].content) : null, + partsLength: response.candidates?.[0]?.content?.parts?.length, + }); + let imageBytes = Buffer.from([]); - response.candidates?.[0].content?.parts?.forEach((part) => { + if (!response.candidates || response.candidates.length === 0) { + this.aiServiceLogger.warn('No candidates in Gemini response for generateImage'); + return ''; + } + + const parts = response.candidates[0].content?.parts; + if (!parts || parts.length === 0) { + this.aiServiceLogger.warn('No parts in first candidate for generateImage'); + return ''; + } + + parts.forEach((part: Part) => { if (part.inlineData?.data) { imageBytes = Buffer.concat([imageBytes, Buffer.from(part.inlineData.data, 'base64')]); + } else { + this.aiServiceLogger.info('GenerateImage part does not have inlineData.data:', Object.keys(part)); } });