Skip to content

Commit 7b2df7f

Browse files
feat: allow image uploads for AI invoice analysis
Modified `LLMService` to detect image attachments (PNG, JPEG, etc.) and send them as base64-encoded `image_url` content to OpenAI, instead of uploading them as files. This resolves the 400 error for unsupported MIME types when uploading PNGs. Maintained existing behavior for PDF files. Added unit tests to cover both image and PDF scenarios.
1 parent dcd8f23 commit 7b2df7f

File tree

2 files changed

+131
-19
lines changed

2 files changed

+131
-19
lines changed

src/backend/services/llm.service.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,102 @@ describe('LLMService', () => {
116116
'OpenAI API key not configured',
117117
);
118118
});
119+
120+
it('should handle image attachments correctly using image_url', async () => {
121+
mockCreate.mockResolvedValueOnce({
122+
choices: [{ message: { content: 'Image analysis' } }],
123+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
124+
});
125+
const config: LLMConfig = {
126+
provider: 'openai',
127+
apiKey: 'test-key',
128+
model: 'gpt-4-vision',
129+
};
130+
const service = new LLMService(
131+
new MockConfigService(config) as unknown as ConfigService,
132+
);
133+
134+
const request: LLMRequest = {
135+
prompt: 'Analyze this image',
136+
files: [
137+
{
138+
name: 'test.png',
139+
content: Buffer.from('fake-image-data'),
140+
mimeType: 'image/png',
141+
},
142+
],
143+
};
144+
145+
await service.sendRequest(request);
146+
147+
expect(mockCreate).toHaveBeenCalledWith(
148+
expect.objectContaining({
149+
messages: expect.arrayContaining([
150+
expect.objectContaining({
151+
role: 'user',
152+
content: [
153+
{
154+
type: 'image_url',
155+
image_url: {
156+
url: `data:image/png;base64,${Buffer.from('fake-image-data').toString('base64')}`,
157+
},
158+
},
159+
],
160+
}),
161+
]),
162+
}),
163+
);
164+
expect(mockFilesCreate).not.toHaveBeenCalled();
165+
});
166+
167+
it('should handle PDF attachments correctly using file upload', async () => {
168+
mockCreate.mockResolvedValueOnce({
169+
choices: [{ message: { content: 'PDF analysis' } }],
170+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
171+
});
172+
mockFilesCreate.mockResolvedValueOnce({ id: 'file-123' });
173+
174+
const config: LLMConfig = {
175+
provider: 'openai',
176+
apiKey: 'test-key',
177+
model: 'gpt-4',
178+
};
179+
const service = new LLMService(
180+
new MockConfigService(config) as unknown as ConfigService,
181+
);
182+
183+
const request: LLMRequest = {
184+
prompt: 'Analyze this PDF',
185+
files: [
186+
{
187+
name: 'test.pdf',
188+
content: Buffer.from('fake-pdf-data'),
189+
mimeType: 'application/pdf',
190+
},
191+
],
192+
};
193+
194+
await service.sendRequest(request);
195+
196+
expect(mockFilesCreate).toHaveBeenCalled();
197+
expect(mockCreate).toHaveBeenCalledWith(
198+
expect.objectContaining({
199+
messages: expect.arrayContaining([
200+
expect.objectContaining({
201+
role: 'user',
202+
content: [
203+
{
204+
type: 'file',
205+
file: {
206+
file_id: 'file-123',
207+
},
208+
},
209+
],
210+
}),
211+
]),
212+
}),
213+
);
214+
});
119215
});
120216

121217
describe('Anthropic provider', () => {

src/backend/services/llm.service.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,26 +147,42 @@ export class LLMService {
147147

148148
if (request.files?.length) {
149149
for (const attachment of request.files) {
150-
const file = new File([attachment.content], attachment.name, {
151-
type: attachment.mimeType,
152-
});
153-
154-
const fileID = await openai.files.create({
155-
file,
156-
purpose: 'user_data',
157-
});
158-
159-
messages.push({
160-
role: 'user',
161-
content: [
162-
{
163-
type: 'file',
164-
file: {
165-
file_id: fileID.id,
150+
if (attachment.mimeType.startsWith('image/')) {
151+
messages.push({
152+
role: 'user',
153+
content: [
154+
{
155+
type: 'image_url',
156+
image_url: {
157+
url: `data:${attachment.mimeType};base64,${attachment.content.toString(
158+
'base64',
159+
)}`,
160+
},
166161
},
167-
},
168-
],
169-
});
162+
],
163+
});
164+
} else {
165+
const file = new File([attachment.content], attachment.name, {
166+
type: attachment.mimeType,
167+
});
168+
169+
const fileID = await openai.files.create({
170+
file,
171+
purpose: 'user_data',
172+
});
173+
174+
messages.push({
175+
role: 'user',
176+
content: [
177+
{
178+
type: 'file',
179+
file: {
180+
file_id: fileID.id,
181+
},
182+
},
183+
],
184+
});
185+
}
170186
}
171187
}
172188

0 commit comments

Comments
 (0)