@ai-sdk/perplexity 3.0.7 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,442 @@
1
+ import {
2
+ LanguageModelV3,
3
+ LanguageModelV3CallOptions,
4
+ LanguageModelV3Content,
5
+ LanguageModelV3FinishReason,
6
+ LanguageModelV3GenerateResult,
7
+ LanguageModelV3StreamPart,
8
+ LanguageModelV3StreamResult,
9
+ SharedV3Warning,
10
+ } from '@ai-sdk/provider';
11
+ import {
12
+ FetchFunction,
13
+ ParseResult,
14
+ combineHeaders,
15
+ createEventSourceResponseHandler,
16
+ createJsonErrorResponseHandler,
17
+ createJsonResponseHandler,
18
+ postJsonToApi,
19
+ } from '@ai-sdk/provider-utils';
20
+ import { z } from 'zod/v4';
21
+ import { convertPerplexityUsage } from './convert-perplexity-usage';
22
+ import { convertToPerplexityMessages } from './convert-to-perplexity-messages';
23
+ import { mapPerplexityFinishReason } from './map-perplexity-finish-reason';
24
+ import { PerplexityLanguageModelId } from './perplexity-language-model-options';
25
+
26
+ type PerplexityChatConfig = {
27
+ baseURL: string;
28
+ headers: () => Record<string, string | undefined>;
29
+ generateId: () => string;
30
+ fetch?: FetchFunction;
31
+ };
32
+
33
+ export class PerplexityLanguageModel implements LanguageModelV3 {
34
+ readonly specificationVersion = 'v3';
35
+ readonly provider = 'perplexity';
36
+
37
+ readonly modelId: PerplexityLanguageModelId;
38
+
39
+ private readonly config: PerplexityChatConfig;
40
+
41
+ constructor(
42
+ modelId: PerplexityLanguageModelId,
43
+ config: PerplexityChatConfig,
44
+ ) {
45
+ this.modelId = modelId;
46
+ this.config = config;
47
+ }
48
+
49
+ readonly supportedUrls: Record<string, RegExp[]> = {
50
+ // No URLs are supported.
51
+ };
52
+
53
+ private getArgs({
54
+ prompt,
55
+ maxOutputTokens,
56
+ temperature,
57
+ topP,
58
+ topK,
59
+ frequencyPenalty,
60
+ presencePenalty,
61
+ stopSequences,
62
+ responseFormat,
63
+ seed,
64
+ providerOptions,
65
+ }: LanguageModelV3CallOptions) {
66
+ const warnings: SharedV3Warning[] = [];
67
+
68
+ if (topK != null) {
69
+ warnings.push({ type: 'unsupported', feature: 'topK' });
70
+ }
71
+
72
+ if (stopSequences != null) {
73
+ warnings.push({ type: 'unsupported', feature: 'stopSequences' });
74
+ }
75
+
76
+ if (seed != null) {
77
+ warnings.push({ type: 'unsupported', feature: 'seed' });
78
+ }
79
+
80
+ return {
81
+ args: {
82
+ // model id:
83
+ model: this.modelId,
84
+
85
+ // standardized settings:
86
+ frequency_penalty: frequencyPenalty,
87
+ max_tokens: maxOutputTokens,
88
+ presence_penalty: presencePenalty,
89
+ temperature,
90
+ top_k: topK,
91
+ top_p: topP,
92
+
93
+ // response format:
94
+ response_format:
95
+ responseFormat?.type === 'json'
96
+ ? {
97
+ type: 'json_schema',
98
+ json_schema: { schema: responseFormat.schema },
99
+ }
100
+ : undefined,
101
+
102
+ // provider extensions
103
+ ...(providerOptions?.perplexity ?? {}),
104
+
105
+ // messages:
106
+ messages: convertToPerplexityMessages(prompt),
107
+ },
108
+ warnings,
109
+ };
110
+ }
111
+
112
+ async doGenerate(
113
+ options: LanguageModelV3CallOptions,
114
+ ): Promise<LanguageModelV3GenerateResult> {
115
+ const { args: body, warnings } = this.getArgs(options);
116
+
117
+ const {
118
+ responseHeaders,
119
+ value: response,
120
+ rawValue: rawResponse,
121
+ } = await postJsonToApi({
122
+ url: `${this.config.baseURL}/chat/completions`,
123
+ headers: combineHeaders(this.config.headers(), options.headers),
124
+ body,
125
+ failedResponseHandler: createJsonErrorResponseHandler({
126
+ errorSchema: perplexityErrorSchema,
127
+ errorToMessage,
128
+ }),
129
+ successfulResponseHandler: createJsonResponseHandler(
130
+ perplexityResponseSchema,
131
+ ),
132
+ abortSignal: options.abortSignal,
133
+ fetch: this.config.fetch,
134
+ });
135
+
136
+ const choice = response.choices[0];
137
+ const content: Array<LanguageModelV3Content> = [];
138
+
139
+ // text content:
140
+ const text = choice.message.content;
141
+ if (text.length > 0) {
142
+ content.push({ type: 'text', text });
143
+ }
144
+
145
+ // sources:
146
+ if (response.citations != null) {
147
+ for (const url of response.citations) {
148
+ content.push({
149
+ type: 'source',
150
+ sourceType: 'url',
151
+ id: this.config.generateId(),
152
+ url,
153
+ });
154
+ }
155
+ }
156
+
157
+ return {
158
+ content,
159
+ finishReason: {
160
+ unified: mapPerplexityFinishReason(choice.finish_reason),
161
+ raw: choice.finish_reason ?? undefined,
162
+ },
163
+ usage: convertPerplexityUsage(response.usage),
164
+ request: { body },
165
+ response: {
166
+ ...getResponseMetadata(response),
167
+ headers: responseHeaders,
168
+ body: rawResponse,
169
+ },
170
+ warnings,
171
+ providerMetadata: {
172
+ perplexity: {
173
+ images:
174
+ response.images?.map(image => ({
175
+ imageUrl: image.image_url,
176
+ originUrl: image.origin_url,
177
+ height: image.height,
178
+ width: image.width,
179
+ })) ?? null,
180
+ usage: {
181
+ citationTokens: response.usage?.citation_tokens ?? null,
182
+ numSearchQueries: response.usage?.num_search_queries ?? null,
183
+ },
184
+ },
185
+ },
186
+ };
187
+ }
188
+
189
+ async doStream(
190
+ options: LanguageModelV3CallOptions,
191
+ ): Promise<LanguageModelV3StreamResult> {
192
+ const { args, warnings } = this.getArgs(options);
193
+
194
+ const body = { ...args, stream: true };
195
+
196
+ const { responseHeaders, value: response } = await postJsonToApi({
197
+ url: `${this.config.baseURL}/chat/completions`,
198
+ headers: combineHeaders(this.config.headers(), options.headers),
199
+ body,
200
+ failedResponseHandler: createJsonErrorResponseHandler({
201
+ errorSchema: perplexityErrorSchema,
202
+ errorToMessage,
203
+ }),
204
+ successfulResponseHandler: createEventSourceResponseHandler(
205
+ perplexityChunkSchema,
206
+ ),
207
+ abortSignal: options.abortSignal,
208
+ fetch: this.config.fetch,
209
+ });
210
+
211
+ let finishReason: LanguageModelV3FinishReason = {
212
+ unified: 'other',
213
+ raw: undefined,
214
+ };
215
+ let usage:
216
+ | {
217
+ prompt_tokens: number | undefined;
218
+ completion_tokens: number | undefined;
219
+ reasoning_tokens?: number | null | undefined;
220
+ }
221
+ | undefined = undefined;
222
+
223
+ const providerMetadata: {
224
+ perplexity: {
225
+ usage: {
226
+ citationTokens: number | null;
227
+ numSearchQueries: number | null;
228
+ };
229
+ images: Array<{
230
+ imageUrl: string;
231
+ originUrl: string;
232
+ height: number;
233
+ width: number;
234
+ }> | null;
235
+ };
236
+ } = {
237
+ perplexity: {
238
+ usage: {
239
+ citationTokens: null,
240
+ numSearchQueries: null,
241
+ },
242
+ images: null,
243
+ },
244
+ };
245
+ let isFirstChunk = true;
246
+ let isActive = false;
247
+
248
+ const self = this;
249
+
250
+ return {
251
+ stream: response.pipeThrough(
252
+ new TransformStream<
253
+ ParseResult<z.infer<typeof perplexityChunkSchema>>,
254
+ LanguageModelV3StreamPart
255
+ >({
256
+ start(controller) {
257
+ controller.enqueue({ type: 'stream-start', warnings });
258
+ },
259
+
260
+ transform(chunk, controller) {
261
+ // Emit raw chunk if requested (before anything else)
262
+ if (options.includeRawChunks) {
263
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
264
+ }
265
+
266
+ if (!chunk.success) {
267
+ controller.enqueue({ type: 'error', error: chunk.error });
268
+ return;
269
+ }
270
+
271
+ const value = chunk.value;
272
+
273
+ if (isFirstChunk) {
274
+ controller.enqueue({
275
+ type: 'response-metadata',
276
+ ...getResponseMetadata(value),
277
+ });
278
+
279
+ value.citations?.forEach(url => {
280
+ controller.enqueue({
281
+ type: 'source',
282
+ sourceType: 'url',
283
+ id: self.config.generateId(),
284
+ url,
285
+ });
286
+ });
287
+
288
+ isFirstChunk = false;
289
+ }
290
+
291
+ if (value.usage != null) {
292
+ usage = value.usage;
293
+
294
+ providerMetadata.perplexity.usage = {
295
+ citationTokens: value.usage.citation_tokens ?? null,
296
+ numSearchQueries: value.usage.num_search_queries ?? null,
297
+ };
298
+ }
299
+
300
+ if (value.images != null) {
301
+ providerMetadata.perplexity.images = value.images.map(image => ({
302
+ imageUrl: image.image_url,
303
+ originUrl: image.origin_url,
304
+ height: image.height,
305
+ width: image.width,
306
+ }));
307
+ }
308
+
309
+ const choice = value.choices[0];
310
+ if (choice?.finish_reason != null) {
311
+ finishReason = {
312
+ unified: mapPerplexityFinishReason(choice.finish_reason),
313
+ raw: choice.finish_reason,
314
+ };
315
+ }
316
+
317
+ if (choice?.delta == null) {
318
+ return;
319
+ }
320
+
321
+ const delta = choice.delta;
322
+ const textContent = delta.content;
323
+
324
+ if (textContent != null) {
325
+ if (!isActive) {
326
+ controller.enqueue({ type: 'text-start', id: '0' });
327
+ isActive = true;
328
+ }
329
+
330
+ controller.enqueue({
331
+ type: 'text-delta',
332
+ id: '0',
333
+ delta: textContent,
334
+ });
335
+ }
336
+ },
337
+
338
+ flush(controller) {
339
+ if (isActive) {
340
+ controller.enqueue({ type: 'text-end', id: '0' });
341
+ }
342
+
343
+ controller.enqueue({
344
+ type: 'finish',
345
+ finishReason,
346
+ usage: convertPerplexityUsage(usage),
347
+ providerMetadata,
348
+ });
349
+ },
350
+ }),
351
+ ),
352
+ request: { body },
353
+ response: { headers: responseHeaders },
354
+ };
355
+ }
356
+ }
357
+
358
+ function getResponseMetadata({
359
+ id,
360
+ model,
361
+ created,
362
+ }: {
363
+ id: string;
364
+ created: number;
365
+ model: string;
366
+ }) {
367
+ return {
368
+ id,
369
+ modelId: model,
370
+ timestamp: new Date(created * 1000),
371
+ };
372
+ }
373
+
374
+ const perplexityUsageSchema = z.object({
375
+ prompt_tokens: z.number(),
376
+ completion_tokens: z.number(),
377
+ total_tokens: z.number().nullish(),
378
+ citation_tokens: z.number().nullish(),
379
+ num_search_queries: z.number().nullish(),
380
+ reasoning_tokens: z.number().nullish(),
381
+ });
382
+
383
+ export const perplexityImageSchema = z.object({
384
+ image_url: z.string(),
385
+ origin_url: z.string(),
386
+ height: z.number(),
387
+ width: z.number(),
388
+ });
389
+
390
+ // limited version of the schema, focussed on what is needed for the implementation
391
+ // this approach limits breakages when the API changes and increases efficiency
392
+ const perplexityResponseSchema = z.object({
393
+ id: z.string(),
394
+ created: z.number(),
395
+ model: z.string(),
396
+ choices: z.array(
397
+ z.object({
398
+ message: z.object({
399
+ role: z.literal('assistant'),
400
+ content: z.string(),
401
+ }),
402
+ finish_reason: z.string().nullish(),
403
+ }),
404
+ ),
405
+ citations: z.array(z.string()).nullish(),
406
+ images: z.array(perplexityImageSchema).nullish(),
407
+ usage: perplexityUsageSchema.nullish(),
408
+ });
409
+
410
+ // limited version of the schema, focussed on what is needed for the implementation
411
+ // this approach limits breakages when the API changes and increases efficiency
412
+ const perplexityChunkSchema = z.object({
413
+ id: z.string(),
414
+ created: z.number(),
415
+ model: z.string(),
416
+ choices: z.array(
417
+ z.object({
418
+ delta: z.object({
419
+ role: z.literal('assistant'),
420
+ content: z.string(),
421
+ }),
422
+ finish_reason: z.string().nullish(),
423
+ }),
424
+ ),
425
+ citations: z.array(z.string()).nullish(),
426
+ images: z.array(perplexityImageSchema).nullish(),
427
+ usage: perplexityUsageSchema.nullish(),
428
+ });
429
+
430
+ export const perplexityErrorSchema = z.object({
431
+ error: z.object({
432
+ code: z.number(),
433
+ message: z.string().nullish(),
434
+ type: z.string().nullish(),
435
+ }),
436
+ });
437
+
438
+ export type PerplexityErrorData = z.infer<typeof perplexityErrorSchema>;
439
+
440
+ const errorToMessage = (data: PerplexityErrorData) => {
441
+ return data.error.message ?? data.error.type ?? 'unknown error';
442
+ };
@@ -0,0 +1,101 @@
1
+ import {
2
+ LanguageModelV3,
3
+ NoSuchModelError,
4
+ ProviderV3,
5
+ } from '@ai-sdk/provider';
6
+ import {
7
+ FetchFunction,
8
+ generateId,
9
+ loadApiKey,
10
+ withoutTrailingSlash,
11
+ withUserAgentSuffix,
12
+ } from '@ai-sdk/provider-utils';
13
+ import { PerplexityLanguageModel } from './perplexity-language-model';
14
+ import { PerplexityLanguageModelId } from './perplexity-language-model-options';
15
+ import { VERSION } from './version';
16
+
17
+ export interface PerplexityProvider extends ProviderV3 {
18
+ /**
19
+ Creates an Perplexity chat model for text generation.
20
+ */
21
+ (modelId: PerplexityLanguageModelId): LanguageModelV3;
22
+
23
+ /**
24
+ Creates an Perplexity language model for text generation.
25
+ */
26
+ languageModel(modelId: PerplexityLanguageModelId): LanguageModelV3;
27
+
28
+ /**
29
+ * @deprecated Use `embeddingModel` instead.
30
+ */
31
+ textEmbeddingModel(modelId: string): never;
32
+ }
33
+
34
+ export interface PerplexityProviderSettings {
35
+ /**
36
+ Base URL for the perplexity API calls.
37
+ */
38
+ baseURL?: string;
39
+
40
+ /**
41
+ API key for authenticating requests.
42
+ */
43
+ apiKey?: string;
44
+
45
+ /**
46
+ Custom headers to include in the requests.
47
+ */
48
+ headers?: Record<string, string>;
49
+
50
+ /**
51
+ Custom fetch implementation. You can use it as a middleware to intercept requests,
52
+ or to provide a custom fetch implementation for e.g. testing.
53
+ */
54
+ fetch?: FetchFunction;
55
+ }
56
+
57
+ export function createPerplexity(
58
+ options: PerplexityProviderSettings = {},
59
+ ): PerplexityProvider {
60
+ const getHeaders = () =>
61
+ withUserAgentSuffix(
62
+ {
63
+ Authorization: `Bearer ${loadApiKey({
64
+ apiKey: options.apiKey,
65
+ environmentVariableName: 'PERPLEXITY_API_KEY',
66
+ description: 'Perplexity',
67
+ })}`,
68
+ ...options.headers,
69
+ },
70
+ `ai-sdk/perplexity/${VERSION}`,
71
+ );
72
+
73
+ const createLanguageModel = (modelId: PerplexityLanguageModelId) => {
74
+ return new PerplexityLanguageModel(modelId, {
75
+ baseURL: withoutTrailingSlash(
76
+ options.baseURL ?? 'https://api.perplexity.ai',
77
+ )!,
78
+ headers: getHeaders,
79
+ generateId,
80
+ fetch: options.fetch,
81
+ });
82
+ };
83
+
84
+ const provider = (modelId: PerplexityLanguageModelId) =>
85
+ createLanguageModel(modelId);
86
+
87
+ provider.specificationVersion = 'v3' as const;
88
+ provider.languageModel = createLanguageModel;
89
+
90
+ provider.embeddingModel = (modelId: string) => {
91
+ throw new NoSuchModelError({ modelId, modelType: 'embeddingModel' });
92
+ };
93
+ provider.textEmbeddingModel = provider.embeddingModel;
94
+ provider.imageModel = (modelId: string) => {
95
+ throw new NoSuchModelError({ modelId, modelType: 'imageModel' });
96
+ };
97
+
98
+ return provider;
99
+ }
100
+
101
+ export const perplexity = createPerplexity();
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Version string of this package injected at build time.
2
+ declare const __PACKAGE_VERSION__: string | undefined;
3
+ export const VERSION: string =
4
+ typeof __PACKAGE_VERSION__ !== 'undefined'
5
+ ? __PACKAGE_VERSION__
6
+ : '0.0.0-test';