Skip to content

Commit ee28981

Browse files
vercel-ai-sdk[bot]goyalshivansh2805dancer
authored
Backport: feat(provider/perplexity): Add PDF support (#10130)
This is an automated backport of #10089 to the release-v5.0 branch. FYI @goyalshivansh2805 --------- Co-authored-by: Shivansh Goyal <[email protected]> Co-authored-by: josh <[email protected]>
1 parent 936916e commit ee28981

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: LanguageModelV2Prompt = [
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: LanguageModelV2Prompt = [
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)