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
1 change: 1 addition & 0 deletions packages/backend/src/ai/ai.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
48 changes: 46 additions & 2 deletions packages/backend/src/ai/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the full Gemini response at info level (including JSON.stringify(response)) will likely include the base64 image payload in inlineData.data. This can leak sensitive content into logs and can generate extremely large log lines. Prefer removing fullResponse, or redacting/truncating binary fields and logging only at debug level (or only when an error/unexpected structure occurs).

Suggested change
fullResponse: JSON.stringify(response, null, 2),

Copilot uses AI. Check for mistakes.
});

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));
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These info logs fire on every non-inline part encountered, which can become noisy in production and make it harder to spot real issues. Consider downgrading to debug and/or logging only once with a summary of part types when the response is unexpected.

Suggested change
this.aiServiceLogger.info('Part does not have inlineData.data:', Object.keys(part));
this.aiServiceLogger.debug('Part does not have inlineData.data:', Object.keys(part));

Copilot uses AI. Check for mistakes.
}
});

Expand Down Expand Up @@ -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,
Comment on lines +241 to +245
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Gemini generateImage response structure" message is logged at info for every image generation, which is likely to be high-volume and may bloat logs. Consider using debug, sampling, or logging this structure only when candidates/parts are missing (i.e., on the warning/error paths).

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This per-part info log in generateImage can be emitted many times and will add noise at scale. Consider moving it to debug and/or including more actionable context (e.g., part type) while avoiding high-cardinality logs.

Suggested change
this.aiServiceLogger.info('GenerateImage part does not have inlineData.data:', Object.keys(part));
this.aiServiceLogger.debug('GenerateImage part does not have inlineData.data:', Object.keys(part));

Copilot uses AI. Check for mistakes.
}
});

Expand Down
Loading