Skip to content

Commit 4b06776

Browse files
feat(provider/perplexity): Add PDF support (#10089)
**Background** The Perplexity provider currently doesn't support PDF upload functionality, although the direct Perplexity API supports this feature. Other providers in the AI SDK like OpenAI, Gemini, and Anthropic already have this capability, making the Perplexity implementation incomplete compared to these providers. **Summary** Added PDF support to the Perplexity provider, allowing users to: - Upload PDF files directly using base64 encoding - Reference PDF files via URLs - Include optional filename parameter Implementation includes: - Updated message conversion logic to handle PDF files - Modified PerplexityMessageContent type to include file_url type - Added documentation for PDF support - Created example files demonstrating PDF usage Checklist - [x] Tests have been added / updated (for bug fixes / features) - [x] Documentation has been added / updated (for bug fixes / features) - [x] A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root) - [x] I have reviewed this pull request (self-review) Related Issues Fixes: #9803
1 parent da9b6e3 commit 4b06776

File tree

8 files changed

+291
-21
lines changed

8 files changed

+291
-21
lines changed

.changeset/chilly-bears-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/perplexity': patch
3+
---
4+
5+
Add PDF support to Perplexity provider

content/providers/01-ai-sdk-providers/70-perplexity.mdx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,48 @@ The metadata includes:
133133

134134
You can enable image responses by setting `return_images: true` in the provider options. This feature is only available to Perplexity Tier-2 users and above.
135135

136+
### PDF Support
137+
138+
The Perplexity provider supports reading PDF files.
139+
You can pass PDF files as part of the message content using the `file` type:
140+
141+
```ts
142+
const result = await generateText({
143+
model: perplexity('sonar-pro'),
144+
messages: [
145+
{
146+
role: 'user',
147+
content: [
148+
{
149+
type: 'text',
150+
text: 'What is this document about?',
151+
},
152+
{
153+
type: 'file',
154+
data: fs.readFileSync('./data/ai.pdf'),
155+
mediaType: 'application/pdf',
156+
filename: 'ai.pdf', // optional
157+
},
158+
],
159+
},
160+
],
161+
});
162+
```
163+
164+
You can also pass the URL of a PDF:
165+
166+
```ts
167+
{
168+
type: 'file',
169+
data: new URL('https://example.com/document.pdf'),
170+
mediaType: 'application/pdf',
171+
filename: 'document.pdf', // optional
172+
}
173+
```
174+
175+
The model will have access to the contents of the PDF file and
176+
respond to questions about it.
177+
136178
<Note>
137179
For more details about Perplexity's capabilities, see the [Perplexity chat
138180
completion docs](https://docs.perplexity.ai/api-reference/chat-completions).
@@ -152,3 +194,4 @@ You can enable image responses by setting `return_images: true` in the provider
152194
Please see the [Perplexity docs](https://docs.perplexity.ai) for detailed API
153195
documentation and the latest updates.
154196
</Note>
197+
```
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { perplexity } from '@ai-sdk/perplexity';
2+
import { generateText } from 'ai';
3+
import 'dotenv/config';
4+
5+
async function main() {
6+
const result = await generateText({
7+
model: perplexity('sonar-pro'),
8+
messages: [
9+
{
10+
role: 'user',
11+
content: [
12+
{
13+
type: 'text',
14+
text: 'What is this document about? Provide a brief summary.',
15+
},
16+
{
17+
type: 'file',
18+
data: new URL('https://example.com/path/to/document.pdf'),
19+
mediaType: 'application/pdf',
20+
filename: 'document.pdf',
21+
},
22+
],
23+
},
24+
],
25+
});
26+
27+
console.log(result.text);
28+
}
29+
30+
main().catch(console.error);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { perplexity } from '@ai-sdk/perplexity';
2+
import { generateText } from 'ai';
3+
import 'dotenv/config';
4+
import fs from 'fs';
5+
6+
async function main() {
7+
const result = await generateText({
8+
model: perplexity('sonar-pro'),
9+
messages: [
10+
{
11+
role: 'user',
12+
content: [
13+
{
14+
type: 'text',
15+
text: 'What is this document about? Provide a brief summary.',
16+
},
17+
{
18+
type: 'file',
19+
data: fs.readFileSync('./data/ai.pdf'),
20+
mediaType: 'application/pdf',
21+
filename: 'ai.pdf',
22+
},
23+
],
24+
},
25+
],
26+
});
27+
28+
console.log(result.text);
29+
}
30+
31+
main().catch(console.error);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { perplexity } from '@ai-sdk/perplexity';
2+
import { streamText } from 'ai';
3+
import 'dotenv/config';
4+
import fs from 'fs';
5+
6+
async function main() {
7+
const result = streamText({
8+
model: perplexity('sonar-pro'),
9+
messages: [
10+
{
11+
role: 'user',
12+
content: [
13+
{
14+
type: 'text',
15+
text: 'What is this document about? Provide a brief summary.',
16+
},
17+
{
18+
type: 'file',
19+
data: fs.readFileSync('./data/ai.pdf'),
20+
mediaType: 'application/pdf',
21+
filename: 'ai.pdf',
22+
},
23+
],
24+
},
25+
],
26+
});
27+
28+
for await (const textPart of result.textStream) {
29+
process.stdout.write(textPart);
30+
}
31+
}
32+
33+
main().catch(console.error);

packages/perplexity/src/convert-to-perplexity-messages.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export function convertToPerplexityMessages(
2222

2323
case 'user':
2424
case 'assistant': {
25-
const hasImage = content.some(
26-
part => part.type === 'file' && part.mediaType.startsWith('image/'),
25+
const hasMultipartContent = content.some(
26+
part =>
27+
(part.type === 'file' && part.mediaType.startsWith('image/')) ||
28+
(part.type === 'file' && part.mediaType === 'application/pdf'),
2729
);
2830

2931
const messageContent = content
30-
.map(part => {
32+
.map((part, index) => {
3133
switch (part.type) {
3234
case 'text': {
3335
return {
@@ -36,30 +38,51 @@ export function convertToPerplexityMessages(
3638
};
3739
}
3840
case 'file': {
39-
return part.data instanceof URL
40-
? {
41-
type: 'image_url',
42-
image_url: {
43-
url: part.data.toString(),
44-
},
45-
}
46-
: {
47-
type: 'image_url',
48-
image_url: {
49-
url: `data:${part.mediaType ?? 'image/jpeg'};base64,${
50-
typeof part.data === 'string'
51-
? part.data
52-
: convertUint8ArrayToBase64(part.data)
53-
}`,
54-
},
55-
};
41+
if (part.mediaType === 'application/pdf') {
42+
return part.data instanceof URL
43+
? {
44+
type: 'file_url',
45+
file_url: {
46+
url: part.data.toString(),
47+
},
48+
file_name: part.filename,
49+
}
50+
: {
51+
type: 'file_url',
52+
file_url: {
53+
url:
54+
typeof part.data === 'string'
55+
? part.data
56+
: convertUint8ArrayToBase64(part.data),
57+
},
58+
file_name: part.filename || `document-${index}.pdf`,
59+
};
60+
} else if (part.mediaType.startsWith('image/')) {
61+
return part.data instanceof URL
62+
? {
63+
type: 'image_url',
64+
image_url: {
65+
url: part.data.toString(),
66+
},
67+
}
68+
: {
69+
type: 'image_url',
70+
image_url: {
71+
url: `data:${part.mediaType ?? 'image/jpeg'};base64,${
72+
typeof part.data === 'string'
73+
? part.data
74+
: convertUint8ArrayToBase64(part.data)
75+
}`,
76+
},
77+
};
78+
}
5679
}
5780
}
5881
})
5982
.filter(Boolean) as PerplexityMessageContent[];
6083
messages.push({
6184
role,
62-
content: hasImage
85+
content: hasMultipartContent
6386
? messageContent
6487
: messageContent
6588
.filter(part => part.type === 'text')

packages/perplexity/src/perplexity-language-model-prompt.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,11 @@ export type PerplexityMessageContent =
1515
image_url: {
1616
url: string;
1717
};
18+
}
19+
| {
20+
type: 'file_url';
21+
file_url: {
22+
url: string;
23+
};
24+
file_name?: string;
1825
};

packages/perplexity/src/perplexity-language-model.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,104 @@ describe('PerplexityLanguageModel', () => {
149149
});
150150
});
151151

152+
it('should handle PDF files with base64 encoding', async () => {
153+
const mockPdfData = 'mock-pdf-data';
154+
const prompt: LanguageModelV3Prompt = [
155+
{
156+
role: 'user',
157+
content: [
158+
{ type: 'text', text: 'Analyze this PDF' },
159+
{
160+
type: 'file',
161+
mediaType: 'application/pdf',
162+
data: mockPdfData,
163+
filename: 'test.pdf',
164+
},
165+
],
166+
},
167+
];
168+
169+
prepareJsonResponse({
170+
content: 'This is an analysis of the PDF',
171+
});
172+
173+
const result = await perplexityModel.doGenerate({ prompt });
174+
175+
// Verify the request contains the correct PDF format
176+
const requestBody =
177+
await jsonServer.calls[jsonServer.calls.length - 1].requestBodyJson;
178+
expect(requestBody.messages[0].content).toEqual([
179+
{
180+
type: 'text',
181+
text: 'Analyze this PDF',
182+
},
183+
{
184+
type: 'file_url',
185+
file_url: {
186+
url: expect.stringContaining(mockPdfData),
187+
},
188+
file_name: 'test.pdf',
189+
},
190+
]);
191+
192+
// Verify the response is processed correctly
193+
expect(result.content).toEqual([
194+
{
195+
type: 'text',
196+
text: 'This is an analysis of the PDF',
197+
},
198+
]);
199+
});
200+
201+
it('should handle PDF files with URLs', async () => {
202+
const pdfUrl = 'https://example.com/test.pdf';
203+
const prompt: LanguageModelV3Prompt = [
204+
{
205+
role: 'user',
206+
content: [
207+
{ type: 'text', text: 'Analyze this PDF' },
208+
{
209+
type: 'file',
210+
mediaType: 'application/pdf',
211+
data: new URL(pdfUrl),
212+
filename: 'test.pdf',
213+
},
214+
],
215+
},
216+
];
217+
218+
prepareJsonResponse({
219+
content: 'This is an analysis of the PDF from URL',
220+
});
221+
222+
const result = await perplexityModel.doGenerate({ prompt });
223+
224+
// Verify the request contains the correct PDF URL format
225+
const requestBody =
226+
await jsonServer.calls[jsonServer.calls.length - 1].requestBodyJson;
227+
expect(requestBody.messages[0].content).toEqual([
228+
{
229+
type: 'text',
230+
text: 'Analyze this PDF',
231+
},
232+
{
233+
type: 'file_url',
234+
file_url: {
235+
url: pdfUrl,
236+
},
237+
file_name: 'test.pdf',
238+
},
239+
]);
240+
241+
// Verify the response is processed correctly
242+
expect(result.content).toEqual([
243+
{
244+
type: 'text',
245+
text: 'This is an analysis of the PDF from URL',
246+
},
247+
]);
248+
});
249+
152250
it('should extract citations as sources', async () => {
153251
prepareJsonResponse({
154252
citations: ['http://example.com/123', 'https://example.com/456'],

0 commit comments

Comments
 (0)