@ai-sdk/perplexity 4.0.52 → 5.0.0

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.
@@ -1,49 +1,183 @@
1
- import type {
2
- LanguageModelV4,
3
- LanguageModelV4CallOptions,
4
- LanguageModelV4Content,
5
- LanguageModelV4FinishReason,
6
- LanguageModelV4GenerateResult,
7
- LanguageModelV4StreamPart,
8
- LanguageModelV4StreamResult,
9
- SharedV4Warning,
1
+ import {
2
+ APICallError,
3
+ type LanguageModelV4,
4
+ type LanguageModelV4CallOptions,
5
+ type LanguageModelV4Content,
6
+ type LanguageModelV4FinishReason,
7
+ type LanguageModelV4GenerateResult,
8
+ type LanguageModelV4StreamPart,
9
+ type LanguageModelV4StreamResult,
10
+ type SharedV4ProviderMetadata,
11
+ type SharedV4Warning,
10
12
  } from '@ai-sdk/provider';
11
13
  import {
12
14
  combineHeaders,
13
15
  createEventSourceResponseHandler,
14
16
  createJsonErrorResponseHandler,
15
17
  createJsonResponseHandler,
16
- createLanguageModelResponseMetadata as getResponseMetadata,
18
+ createLanguageModelResponseMetadata,
17
19
  isCustomReasoning,
18
20
  parseProviderOptions,
19
21
  postJsonToApi,
20
22
  serializeModelOptions,
21
- WORKFLOW_SERIALIZE,
22
23
  WORKFLOW_DESERIALIZE,
24
+ WORKFLOW_SERIALIZE,
23
25
  type FetchFunction,
24
26
  type ParseResult,
25
27
  } from '@ai-sdk/provider-utils';
26
- import { z } from 'zod/v4';
28
+ import type { z } from 'zod/v4';
27
29
  import { convertPerplexityUsage } from './convert-perplexity-usage';
28
- import { convertToPerplexityMessages } from './convert-to-perplexity-messages';
30
+ import { convertToPerplexityInput } from './convert-to-perplexity-input';
29
31
  import { mapPerplexityFinishReason } from './map-perplexity-finish-reason';
32
+ import {
33
+ perplexityAgentChunkSchema,
34
+ perplexityAgentResponseSchema,
35
+ perplexityErrorSchema,
36
+ perplexityErrorToMessage,
37
+ type PerplexityAgentTool,
38
+ type perplexityOutputItemSchema,
39
+ type perplexitySearchResultSchema,
40
+ type perplexityUsageSchema,
41
+ } from './perplexity-agent-api';
30
42
  import { perplexityLanguageModelOptions } from './perplexity-language-model-options';
31
- import type { PerplexityLanguageModelId } from './perplexity-options';
43
+ import type {
44
+ PerplexityAgentPreset,
45
+ PerplexityLanguageModelId,
46
+ } from './perplexity-options';
47
+ import { preparePerplexityTools } from './perplexity-prepare-tools';
32
48
 
33
- type PerplexityChatConfig = {
49
+ type PerplexityAgentConfig = {
34
50
  baseURL: string;
35
51
  headers?: () => Record<string, string | undefined>;
36
52
  generateId: () => string;
37
53
  fetch?: FetchFunction;
38
54
  };
39
55
 
56
+ type PerplexityOutputItem = z.infer<typeof perplexityOutputItemSchema>;
57
+ type PerplexitySearchResult = z.infer<typeof perplexitySearchResultSchema>;
58
+ type PerplexityUsage = z.infer<typeof perplexityUsageSchema>;
59
+ type PerplexityUrlSource = Extract<
60
+ LanguageModelV4Content,
61
+ { type: 'source'; sourceType: 'url' }
62
+ >;
63
+
64
+ const presetIds = new Set<PerplexityAgentPreset>([
65
+ 'fast',
66
+ 'low',
67
+ 'medium',
68
+ 'high',
69
+ 'xhigh',
70
+ ]);
71
+
72
+ function getModelSelection(modelId: PerplexityLanguageModelId): {
73
+ model?: string;
74
+ preset?: PerplexityAgentPreset;
75
+ } {
76
+ if (presetIds.has(modelId as PerplexityAgentPreset)) {
77
+ return { preset: modelId as PerplexityAgentPreset };
78
+ }
79
+
80
+ return { model: modelId };
81
+ }
82
+
83
+ function getResponseMetadata(response: {
84
+ id?: string | null;
85
+ model?: string | null;
86
+ created_at?: number | null;
87
+ }) {
88
+ return createLanguageModelResponseMetadata({
89
+ id: response.id,
90
+ model: response.model,
91
+ created: response.created_at,
92
+ });
93
+ }
94
+
95
+ function getProviderMetadata(
96
+ usage: PerplexityUsage | null | undefined,
97
+ ): SharedV4ProviderMetadata {
98
+ const cost = usage?.cost;
99
+ const numSearchQueries = usage?.tool_calls_details
100
+ ? Object.entries(usage.tool_calls_details)
101
+ .filter(([name]) => name.includes('search'))
102
+ .reduce((total, [, details]) => total + (details.invocation ?? 0), 0)
103
+ : null;
104
+
105
+ return {
106
+ perplexity: {
107
+ usage: {
108
+ citationTokens: null,
109
+ numSearchQueries,
110
+ },
111
+ images: null,
112
+ cost:
113
+ cost == null
114
+ ? null
115
+ : {
116
+ inputTokensCost: cost.input_cost ?? null,
117
+ outputTokensCost: cost.output_cost ?? null,
118
+ requestCost: null,
119
+ totalCost: cost.total_cost ?? null,
120
+ currency: cost.currency ?? null,
121
+ cacheCreationCost: cost.cache_creation_cost ?? null,
122
+ cacheReadCost: cost.cache_read_cost ?? null,
123
+ toolCallsCost: cost.tool_calls_cost ?? null,
124
+ },
125
+ toolCalls:
126
+ usage?.tool_calls_details == null
127
+ ? null
128
+ : Object.fromEntries(
129
+ Object.entries(usage.tool_calls_details).map(
130
+ ([name, details]) => [
131
+ name,
132
+ { invocation: details.invocation ?? null },
133
+ ],
134
+ ),
135
+ ),
136
+ },
137
+ };
138
+ }
139
+
140
+ function createSource(
141
+ result: PerplexitySearchResult,
142
+ generateId: () => string,
143
+ ): PerplexityUrlSource {
144
+ return {
145
+ type: 'source',
146
+ sourceType: 'url',
147
+ id: result.id == null ? generateId() : String(result.id),
148
+ url: result.url,
149
+ title: result.title,
150
+ providerMetadata: {
151
+ perplexity: {
152
+ resultId: result.id ?? null,
153
+ snippet: result.snippet ?? null,
154
+ date: result.date ?? null,
155
+ lastUpdated: result.last_updated ?? null,
156
+ source: result.source ?? null,
157
+ },
158
+ },
159
+ };
160
+ }
161
+
162
+ function hasSearchResultId(source: PerplexityUrlSource): boolean {
163
+ return typeof source.providerMetadata?.perplexity?.resultId === 'number';
164
+ }
165
+
166
+ function getSearchResults(item: PerplexityOutputItem) {
167
+ return item.type === 'search_results' ? (item.results ?? []) : [];
168
+ }
169
+
170
+ function getFetchedSources(item: PerplexityOutputItem) {
171
+ return item.type === 'fetch_url_results' ? (item.contents ?? []) : [];
172
+ }
173
+
40
174
  export class PerplexityLanguageModel implements LanguageModelV4 {
41
175
  readonly specificationVersion = 'v4';
42
176
  readonly provider = 'perplexity';
43
177
 
44
178
  readonly modelId: PerplexityLanguageModelId;
45
179
 
46
- private readonly config: PerplexityChatConfig;
180
+ private readonly config: PerplexityAgentConfig;
47
181
 
48
182
  static [WORKFLOW_SERIALIZE](model: PerplexityLanguageModel) {
49
183
  return serializeModelOptions({
@@ -54,21 +188,21 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
54
188
 
55
189
  static [WORKFLOW_DESERIALIZE](options: {
56
190
  modelId: PerplexityLanguageModelId;
57
- config: PerplexityChatConfig;
191
+ config: PerplexityAgentConfig;
58
192
  }) {
59
193
  return new PerplexityLanguageModel(options.modelId, options.config);
60
194
  }
61
195
 
62
196
  constructor(
63
197
  modelId: PerplexityLanguageModelId,
64
- config: PerplexityChatConfig,
198
+ config: PerplexityAgentConfig,
65
199
  ) {
66
200
  this.modelId = modelId;
67
201
  this.config = config;
68
202
  }
69
203
 
70
204
  readonly supportedUrls: Record<string, RegExp[]> = {
71
- // No URLs are supported.
205
+ 'image/*': [/^https?:\/\/.*$/],
72
206
  };
73
207
 
74
208
  private async getArgs({
@@ -84,6 +218,8 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
84
218
  responseFormat,
85
219
  seed,
86
220
  providerOptions,
221
+ tools,
222
+ toolChoice,
87
223
  }: LanguageModelV4CallOptions) {
88
224
  const warnings: SharedV4Warning[] = [];
89
225
 
@@ -97,53 +233,78 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
97
233
  if (topK != null) {
98
234
  warnings.push({ type: 'unsupported', feature: 'topK' });
99
235
  }
100
-
236
+ if (frequencyPenalty != null) {
237
+ warnings.push({ type: 'unsupported', feature: 'frequencyPenalty' });
238
+ }
239
+ if (presencePenalty != null) {
240
+ warnings.push({ type: 'unsupported', feature: 'presencePenalty' });
241
+ }
101
242
  if (stopSequences != null) {
102
243
  warnings.push({ type: 'unsupported', feature: 'stopSequences' });
103
244
  }
104
-
105
245
  if (seed != null) {
106
246
  warnings.push({ type: 'unsupported', feature: 'seed' });
107
247
  }
108
248
 
109
- if (isCustomReasoning(reasoning)) {
249
+ const { tools: nativeTools, ...agentOptions } = perplexityOptions;
250
+
251
+ const modelSelection = getModelSelection(this.modelId);
252
+
253
+ const { input, warnings: inputWarnings } = convertToPerplexityInput(prompt);
254
+ warnings.push(...inputWarnings);
255
+
256
+ const { tools: functionTools, warnings: toolWarnings } =
257
+ preparePerplexityTools({ tools, toolChoice });
258
+ warnings.push(...toolWarnings);
259
+
260
+ const agentTools = [
261
+ ...((nativeTools ?? []) as PerplexityAgentTool[]),
262
+ ...functionTools,
263
+ ];
264
+
265
+ let reasoningConfig = agentOptions.reasoning;
266
+ if (reasoningConfig == null && isCustomReasoning(reasoning)) {
267
+ if (reasoning === 'none') {
268
+ warnings.push({
269
+ type: 'unsupported',
270
+ feature: 'reasoning "none"',
271
+ });
272
+ } else {
273
+ reasoningConfig = { effort: reasoning };
274
+ }
275
+ }
276
+
277
+ const body: Record<string, unknown> = {
278
+ ...agentOptions,
279
+ ...modelSelection,
280
+ input,
281
+ max_output_tokens: maxOutputTokens,
282
+ temperature,
283
+ top_p: topP,
284
+ reasoning: reasoningConfig,
285
+ response_format:
286
+ responseFormat?.type === 'json' && responseFormat.schema != null
287
+ ? {
288
+ type: 'json_schema',
289
+ json_schema: {
290
+ name: responseFormat.name ?? 'response',
291
+ description: responseFormat.description,
292
+ schema: responseFormat.schema,
293
+ strict: true,
294
+ },
295
+ }
296
+ : undefined,
297
+ tools: agentTools.length > 0 ? agentTools : undefined,
298
+ };
299
+
300
+ if (responseFormat?.type === 'json' && responseFormat.schema == null) {
110
301
  warnings.push({
111
302
  type: 'unsupported',
112
- feature: 'reasoning',
113
- details: 'This provider does not support reasoning configuration.',
303
+ feature: 'JSON response format without a schema',
114
304
  });
115
305
  }
116
306
 
117
- return {
118
- args: {
119
- // model id:
120
- model: this.modelId,
121
-
122
- // standardized settings:
123
- frequency_penalty: frequencyPenalty,
124
- max_tokens: maxOutputTokens,
125
- presence_penalty: presencePenalty,
126
- temperature,
127
- top_k: topK,
128
- top_p: topP,
129
-
130
- // response format:
131
- response_format:
132
- responseFormat?.type === 'json'
133
- ? {
134
- type: 'json_schema',
135
- json_schema: { schema: responseFormat.schema },
136
- }
137
- : undefined,
138
-
139
- // provider extensions
140
- ...perplexityOptions,
141
-
142
- // messages:
143
- messages: convertToPerplexityMessages(prompt),
144
- },
145
- warnings,
146
- };
307
+ return { args: body, warnings };
147
308
  }
148
309
 
149
310
  async doGenerate(
@@ -151,51 +312,125 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
151
312
  ): Promise<LanguageModelV4GenerateResult> {
152
313
  const { args: body, warnings } = await this.getArgs(options);
153
314
 
315
+ const url = `${this.config.baseURL}/v1/agent`;
154
316
  const {
155
317
  responseHeaders,
156
318
  value: response,
157
319
  rawValue: rawResponse,
158
320
  } = await postJsonToApi({
159
- url: `${this.config.baseURL}/chat/completions`,
321
+ url,
160
322
  headers: combineHeaders(this.config.headers?.(), options.headers),
161
323
  body,
162
324
  failedResponseHandler: createJsonErrorResponseHandler({
163
325
  errorSchema: perplexityErrorSchema,
164
- errorToMessage,
326
+ errorToMessage: perplexityErrorToMessage,
165
327
  }),
166
328
  successfulResponseHandler: createJsonResponseHandler(
167
- perplexityResponseSchema,
329
+ perplexityAgentResponseSchema,
168
330
  ),
169
331
  abortSignal: options.abortSignal,
170
332
  fetch: this.config.fetch,
171
333
  });
172
334
 
173
- const choice = response.choices[0];
174
- const content: Array<LanguageModelV4Content> = [];
175
-
176
- // text content:
177
- const text = choice.message.content;
178
- if (text.length > 0) {
179
- content.push({ type: 'text', text });
335
+ if (response.error != null || response.status === 'failed') {
336
+ throw new APICallError({
337
+ message: response.error?.message ?? 'Perplexity response failed',
338
+ url,
339
+ requestBodyValues: body,
340
+ statusCode: 400,
341
+ responseHeaders,
342
+ responseBody: rawResponse as string,
343
+ isRetryable: false,
344
+ });
180
345
  }
181
346
 
182
- // sources:
183
- if (response.citations != null) {
184
- for (const url of response.citations) {
347
+ const content: LanguageModelV4Content[] = [];
348
+ const sourceIndexesByUrl = new Map<string, number>();
349
+ let hasFunctionCall = false;
350
+
351
+ const addSource = (source: PerplexityUrlSource) => {
352
+ const existingIndex = sourceIndexesByUrl.get(source.url);
353
+ if (existingIndex == null) {
354
+ sourceIndexesByUrl.set(source.url, content.length);
355
+ content.push(source);
356
+ } else if (
357
+ hasSearchResultId(source) &&
358
+ !hasSearchResultId(content[existingIndex] as PerplexityUrlSource)
359
+ ) {
360
+ content[existingIndex] = source;
361
+ }
362
+ };
363
+
364
+ for (const item of response.output) {
365
+ if (item.type === 'message') {
366
+ for (const part of item.content ?? []) {
367
+ if (part.type === 'output_text' && part.text != null) {
368
+ content.push({ type: 'text', text: part.text });
369
+ }
370
+ for (const annotation of part.annotations ?? []) {
371
+ if (annotation.url != null) {
372
+ addSource({
373
+ type: 'source',
374
+ sourceType: 'url',
375
+ id: this.config.generateId(),
376
+ url: annotation.url,
377
+ title: annotation.title,
378
+ });
379
+ }
380
+ }
381
+ }
382
+ } else if (item.type === 'search_results') {
383
+ for (const result of getSearchResults(item)) {
384
+ addSource(createSource(result, this.config.generateId));
385
+ }
386
+ } else if (item.type === 'fetch_url_results') {
387
+ for (const result of getFetchedSources(item)) {
388
+ addSource({
389
+ type: 'source',
390
+ sourceType: 'url',
391
+ id: this.config.generateId(),
392
+ url: result.url,
393
+ title: result.title,
394
+ providerMetadata: {
395
+ perplexity: { snippet: result.snippet ?? null },
396
+ },
397
+ });
398
+ }
399
+ } else if (
400
+ item.type === 'function_call' &&
401
+ item.call_id != null &&
402
+ item.name != null &&
403
+ item.arguments != null
404
+ ) {
405
+ hasFunctionCall = true;
185
406
  content.push({
186
- type: 'source',
187
- sourceType: 'url',
188
- id: this.config.generateId(),
189
- url,
407
+ type: 'tool-call',
408
+ toolCallId: item.call_id,
409
+ toolName: item.name,
410
+ input: item.arguments,
411
+ providerMetadata: {
412
+ perplexity: {
413
+ itemId: item.id ?? null,
414
+ ...(item.thought_signature != null && {
415
+ thoughtSignature: item.thought_signature,
416
+ }),
417
+ },
418
+ },
190
419
  });
191
420
  }
192
421
  }
193
422
 
423
+ const finishReason = response.incomplete_details?.reason ?? response.status;
424
+
194
425
  return {
195
426
  content,
196
427
  finishReason: {
197
- unified: mapPerplexityFinishReason(choice.finish_reason),
198
- raw: choice.finish_reason ?? undefined,
428
+ unified: mapPerplexityFinishReason({
429
+ status: response.status,
430
+ incompleteReason: response.incomplete_details?.reason,
431
+ hasFunctionCall,
432
+ }),
433
+ raw: finishReason,
199
434
  },
200
435
  usage: convertPerplexityUsage(response.usage),
201
436
  request: { body },
@@ -205,30 +440,7 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
205
440
  body: rawResponse,
206
441
  },
207
442
  warnings,
208
- providerMetadata: {
209
- perplexity: {
210
- images:
211
- response.images?.map(image => ({
212
- imageUrl: image.image_url,
213
- originUrl: image.origin_url,
214
- height: image.height,
215
- width: image.width,
216
- })) ?? null,
217
- usage: {
218
- citationTokens: response.usage?.citation_tokens ?? null,
219
- numSearchQueries: response.usage?.num_search_queries ?? null,
220
- },
221
- cost: response.usage?.cost
222
- ? {
223
- inputTokensCost: response.usage.cost.input_tokens_cost ?? null,
224
- outputTokensCost:
225
- response.usage.cost.output_tokens_cost ?? null,
226
- requestCost: response.usage.cost.request_cost ?? null,
227
- totalCost: response.usage.cost.total_cost ?? null,
228
- }
229
- : null,
230
- },
231
- },
443
+ providerMetadata: getProviderMetadata(response.usage),
232
444
  };
233
445
  }
234
446
 
@@ -236,19 +448,18 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
236
448
  options: LanguageModelV4CallOptions,
237
449
  ): Promise<LanguageModelV4StreamResult> {
238
450
  const { args, warnings } = await this.getArgs(options);
239
-
240
451
  const body = { ...args, stream: true };
241
452
 
242
453
  const { responseHeaders, value: response } = await postJsonToApi({
243
- url: `${this.config.baseURL}/chat/completions`,
454
+ url: `${this.config.baseURL}/v1/agent`,
244
455
  headers: combineHeaders(this.config.headers?.(), options.headers),
245
456
  body,
246
457
  failedResponseHandler: createJsonErrorResponseHandler({
247
458
  errorSchema: perplexityErrorSchema,
248
- errorToMessage,
459
+ errorToMessage: perplexityErrorToMessage,
249
460
  }),
250
461
  successfulResponseHandler: createEventSourceResponseHandler(
251
- perplexityChunkSchema,
462
+ perplexityAgentChunkSchema,
252
463
  ),
253
464
  abortSignal: options.abortSignal,
254
465
  fetch: this.config.fetch,
@@ -258,52 +469,20 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
258
469
  unified: 'other',
259
470
  raw: undefined,
260
471
  };
261
- let usage:
262
- | {
263
- prompt_tokens: number | undefined;
264
- completion_tokens: number | undefined;
265
- reasoning_tokens?: number | null | undefined;
266
- }
267
- | undefined = undefined;
268
-
269
- const providerMetadata: {
270
- perplexity: {
271
- usage: {
272
- citationTokens: number | null;
273
- numSearchQueries: number | null;
274
- };
275
- cost: {
276
- inputTokensCost: number | null;
277
- outputTokensCost: number | null;
278
- requestCost: number | null;
279
- totalCost: number | null;
280
- } | null;
281
- images: Array<{
282
- imageUrl: string;
283
- originUrl: string;
284
- height: number;
285
- width: number;
286
- }> | null;
287
- };
288
- } = {
289
- perplexity: {
290
- usage: {
291
- citationTokens: null,
292
- numSearchQueries: null,
293
- },
294
- cost: null,
295
- images: null,
296
- },
297
- };
298
- let isFirstChunk = true;
299
- let isActive = false;
300
-
301
- const self = this;
472
+ let usage: PerplexityUsage | undefined;
473
+ let hasFunctionCall = false;
474
+ let hasResponseMetadata = false;
475
+ let activeReasoningId: string | undefined;
476
+ const textStates = new Map<string, { text: string; ended: boolean }>();
477
+ const emittedSourceUrls = new Set<string>();
478
+ const pendingSourcesByUrl = new Map<string, PerplexityUrlSource>();
479
+ const seenFunctionCalls = new Set<string>();
480
+ const generateId = this.config.generateId;
302
481
 
303
482
  return {
304
483
  stream: response.pipeThrough(
305
484
  new TransformStream<
306
- ParseResult<z.infer<typeof perplexityChunkSchema>>,
485
+ ParseResult<z.infer<typeof perplexityAgentChunkSchema>>,
307
486
  LanguageModelV4StreamPart
308
487
  >({
309
488
  start(controller) {
@@ -311,103 +490,354 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
311
490
  },
312
491
 
313
492
  transform(chunk, controller) {
314
- // Emit raw chunk if requested (before anything else)
315
493
  if (options.includeRawChunks) {
316
494
  controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
317
495
  }
318
496
 
319
497
  if (!chunk.success) {
498
+ finishReason = { unified: 'error', raw: undefined };
320
499
  controller.enqueue({ type: 'error', error: chunk.error });
321
500
  return;
322
501
  }
323
502
 
324
503
  const value = chunk.value;
325
504
 
326
- if (isFirstChunk) {
327
- controller.enqueue({
328
- type: 'response-metadata',
329
- ...getResponseMetadata(value),
330
- });
505
+ const getTextId = (
506
+ itemId: string | null | undefined,
507
+ outputIndex: number | undefined,
508
+ contentIndex = 0,
509
+ ) => {
510
+ const id = itemId ?? String(outputIndex ?? 'text');
511
+ return contentIndex === 0 ? id : `${id}:${contentIndex}`;
512
+ };
513
+
514
+ const emitTextDelta = (id: string, delta: string) => {
515
+ let state = textStates.get(id);
516
+ if (state?.ended) {
517
+ return;
518
+ }
519
+ if (state == null) {
520
+ state = { text: '', ended: false };
521
+ textStates.set(id, state);
522
+ controller.enqueue({ type: 'text-start', id });
523
+ }
524
+ state.text += delta;
525
+ controller.enqueue({ type: 'text-delta', id, delta });
526
+ };
527
+
528
+ const finishText = (id: string, text: string | undefined) => {
529
+ const state = textStates.get(id);
530
+ if (state?.ended) {
531
+ return;
532
+ }
533
+ // Terminal output can contain text for which no delta arrived,
534
+ // or the remainder of a partially streamed content part.
535
+ const emittedText = state?.text ?? '';
536
+ if (
537
+ text != null &&
538
+ text.startsWith(emittedText) &&
539
+ text.length > emittedText.length
540
+ ) {
541
+ emitTextDelta(id, text.slice(emittedText.length));
542
+ }
543
+ const finalState = textStates.get(id);
544
+ if (finalState != null) {
545
+ finalState.ended = true;
546
+ controller.enqueue({ type: 'text-end', id });
547
+ }
548
+ };
549
+
550
+ const finishOutputText = (
551
+ item: PerplexityOutputItem,
552
+ outputIndex?: number,
553
+ ) => {
554
+ if (item.type === 'message') {
555
+ for (const [contentIndex, part] of (
556
+ item.content ?? []
557
+ ).entries()) {
558
+ if (part.type === 'output_text') {
559
+ finishText(
560
+ getTextId(item.id, outputIndex, contentIndex),
561
+ part.text,
562
+ );
563
+ }
564
+ }
565
+ }
566
+ };
567
+
568
+ const emitSource = (source: PerplexityUrlSource) => {
569
+ if (emittedSourceUrls.has(source.url)) {
570
+ return;
571
+ }
331
572
 
332
- value.citations?.forEach(url => {
573
+ if (hasSearchResultId(source)) {
574
+ pendingSourcesByUrl.delete(source.url);
575
+ emittedSourceUrls.add(source.url);
576
+ controller.enqueue(source);
577
+ } else if (!pendingSourcesByUrl.has(source.url)) {
578
+ // A later search result can supply the citation ID. Delay
579
+ // sources without one rather than emitting a duplicate update.
580
+ pendingSourcesByUrl.set(source.url, source);
581
+ }
582
+ };
583
+
584
+ const emitReasoningThought = (thought: string | undefined) => {
585
+ if (activeReasoningId != null && thought != null) {
333
586
  controller.enqueue({
587
+ type: 'reasoning-delta',
588
+ id: activeReasoningId,
589
+ delta: thought,
590
+ });
591
+ }
592
+ };
593
+
594
+ const emitFunctionCall = (item: PerplexityOutputItem) => {
595
+ if (
596
+ item.type !== 'function_call' ||
597
+ item.call_id == null ||
598
+ item.name == null ||
599
+ item.arguments == null ||
600
+ seenFunctionCalls.has(item.call_id)
601
+ ) {
602
+ return;
603
+ }
604
+ seenFunctionCalls.add(item.call_id);
605
+ hasFunctionCall = true;
606
+ controller.enqueue({
607
+ type: 'tool-input-start',
608
+ id: item.call_id,
609
+ toolName: item.name,
610
+ });
611
+ controller.enqueue({
612
+ type: 'tool-input-delta',
613
+ id: item.call_id,
614
+ delta: item.arguments,
615
+ });
616
+ controller.enqueue({ type: 'tool-input-end', id: item.call_id });
617
+ controller.enqueue({
618
+ type: 'tool-call',
619
+ toolCallId: item.call_id,
620
+ toolName: item.name,
621
+ input: item.arguments,
622
+ providerMetadata: {
623
+ perplexity: {
624
+ itemId: item.id ?? null,
625
+ ...(item.thought_signature != null && {
626
+ thoughtSignature: item.thought_signature,
627
+ }),
628
+ },
629
+ },
630
+ });
631
+ };
632
+
633
+ const emitOutputSources = (item: PerplexityOutputItem) => {
634
+ if (item.type === 'message') {
635
+ for (const part of item.content ?? []) {
636
+ for (const annotation of part.annotations ?? []) {
637
+ if (annotation.url != null) {
638
+ emitSource({
639
+ type: 'source',
640
+ sourceType: 'url',
641
+ id: generateId(),
642
+ url: annotation.url,
643
+ title: annotation.title,
644
+ });
645
+ }
646
+ }
647
+ }
648
+ }
649
+ for (const result of getSearchResults(item)) {
650
+ emitSource(createSource(result, generateId));
651
+ }
652
+ for (const result of getFetchedSources(item)) {
653
+ emitSource({
334
654
  type: 'source',
335
655
  sourceType: 'url',
336
- id: self.config.generateId(),
337
- url,
656
+ id: generateId(),
657
+ url: result.url,
658
+ title: result.title,
659
+ providerMetadata: {
660
+ perplexity: { snippet: result.snippet ?? null },
661
+ },
338
662
  });
339
- });
663
+ }
664
+ };
665
+
666
+ switch (value.type) {
667
+ case 'response.created':
668
+ case 'response.in_progress': {
669
+ if (!hasResponseMetadata && value.response != null) {
670
+ controller.enqueue({
671
+ type: 'response-metadata',
672
+ ...getResponseMetadata(value.response),
673
+ });
674
+ hasResponseMetadata = true;
675
+ }
676
+ break;
677
+ }
340
678
 
341
- isFirstChunk = false;
342
- }
679
+ case 'response.output_text.delta': {
680
+ if (value.delta != null) {
681
+ emitTextDelta(
682
+ getTextId(
683
+ value.item_id,
684
+ value.output_index,
685
+ value.content_index,
686
+ ),
687
+ value.delta,
688
+ );
689
+ }
690
+ break;
691
+ }
343
692
 
344
- if (value.usage != null) {
345
- usage = value.usage;
346
-
347
- providerMetadata.perplexity.usage = {
348
- citationTokens: value.usage.citation_tokens ?? null,
349
- numSearchQueries: value.usage.num_search_queries ?? null,
350
- };
351
-
352
- providerMetadata.perplexity.cost = value.usage.cost
353
- ? {
354
- inputTokensCost: value.usage.cost.input_tokens_cost ?? null,
355
- outputTokensCost:
356
- value.usage.cost.output_tokens_cost ?? null,
357
- requestCost: value.usage.cost.request_cost ?? null,
358
- totalCost: value.usage.cost.total_cost ?? null,
359
- }
360
- : null;
361
- }
693
+ case 'response.output_text.done': {
694
+ finishText(
695
+ getTextId(
696
+ value.item_id,
697
+ value.output_index,
698
+ value.content_index,
699
+ ),
700
+ value.text,
701
+ );
702
+ break;
703
+ }
362
704
 
363
- if (value.images != null) {
364
- providerMetadata.perplexity.images = value.images.map(image => ({
365
- imageUrl: image.image_url,
366
- originUrl: image.origin_url,
367
- height: image.height,
368
- width: image.width,
369
- }));
370
- }
705
+ case 'response.reasoning.started': {
706
+ if (activeReasoningId != null) {
707
+ controller.enqueue({
708
+ type: 'reasoning-end',
709
+ id: activeReasoningId,
710
+ });
711
+ }
712
+ activeReasoningId = `reasoning-${
713
+ value.sequence_number ?? generateId()
714
+ }`;
715
+ controller.enqueue({
716
+ type: 'reasoning-start',
717
+ id: activeReasoningId,
718
+ });
719
+ emitReasoningThought(value.thought);
720
+ break;
721
+ }
371
722
 
372
- const choice = value.choices[0];
373
- if (choice?.finish_reason != null) {
374
- finishReason = {
375
- unified: mapPerplexityFinishReason(choice.finish_reason),
376
- raw: choice.finish_reason,
377
- };
378
- }
723
+ case 'response.reasoning.search_queries':
724
+ case 'response.reasoning.fetch_url_queries': {
725
+ emitReasoningThought(value.thought);
726
+ break;
727
+ }
379
728
 
380
- if (choice?.delta == null) {
381
- return;
382
- }
729
+ case 'response.reasoning.search_results': {
730
+ emitReasoningThought(value.thought);
731
+ for (const result of value.results ?? []) {
732
+ emitSource(createSource(result, generateId));
733
+ }
734
+ break;
735
+ }
383
736
 
384
- const delta = choice.delta;
385
- const textContent = delta.content;
737
+ case 'response.reasoning.fetch_url_results': {
738
+ emitReasoningThought(value.thought);
739
+ for (const result of value.contents ?? []) {
740
+ emitSource({
741
+ type: 'source',
742
+ sourceType: 'url',
743
+ id: generateId(),
744
+ url: result.url,
745
+ title: result.title,
746
+ providerMetadata: {
747
+ perplexity: { snippet: result.snippet ?? null },
748
+ },
749
+ });
750
+ }
751
+ break;
752
+ }
386
753
 
387
- if (textContent != null) {
388
- if (!isActive) {
389
- controller.enqueue({ type: 'text-start', id: '0' });
390
- isActive = true;
754
+ case 'response.reasoning.stopped': {
755
+ emitReasoningThought(value.thought);
756
+ if (activeReasoningId != null) {
757
+ controller.enqueue({
758
+ type: 'reasoning-end',
759
+ id: activeReasoningId,
760
+ });
761
+ activeReasoningId = undefined;
762
+ }
763
+ break;
391
764
  }
392
765
 
393
- controller.enqueue({
394
- type: 'text-delta',
395
- id: '0',
396
- delta: textContent,
397
- });
766
+ case 'response.output_item.done': {
767
+ if (value.item != null) {
768
+ finishOutputText(value.item, value.output_index);
769
+ emitOutputSources(value.item);
770
+ emitFunctionCall(value.item);
771
+ }
772
+ break;
773
+ }
774
+
775
+ case 'response.completed':
776
+ case 'response.incomplete': {
777
+ if (value.response != null) {
778
+ if (!hasResponseMetadata) {
779
+ controller.enqueue({
780
+ type: 'response-metadata',
781
+ ...getResponseMetadata(value.response),
782
+ });
783
+ hasResponseMetadata = true;
784
+ }
785
+ for (const [
786
+ outputIndex,
787
+ item,
788
+ ] of value.response.output.entries()) {
789
+ finishOutputText(item, outputIndex);
790
+ emitOutputSources(item);
791
+ emitFunctionCall(item);
792
+ }
793
+ usage = value.response.usage ?? undefined;
794
+ const rawFinishReason =
795
+ value.response.incomplete_details?.reason ??
796
+ value.response.status;
797
+ finishReason = {
798
+ unified: mapPerplexityFinishReason({
799
+ status: value.response.status,
800
+ incompleteReason:
801
+ value.response.incomplete_details?.reason,
802
+ hasFunctionCall,
803
+ }),
804
+ raw: rawFinishReason,
805
+ };
806
+ }
807
+ break;
808
+ }
809
+
810
+ case 'response.failed': {
811
+ finishReason = { unified: 'error', raw: 'failed' };
812
+ controller.enqueue({
813
+ type: 'error',
814
+ error: value.error ?? new Error('Perplexity response failed'),
815
+ });
816
+ break;
817
+ }
398
818
  }
399
819
  },
400
820
 
401
821
  flush(controller) {
402
- if (isActive) {
403
- controller.enqueue({ type: 'text-end', id: '0' });
822
+ for (const source of pendingSourcesByUrl.values()) {
823
+ controller.enqueue(source);
824
+ }
825
+ if (activeReasoningId != null) {
826
+ controller.enqueue({
827
+ type: 'reasoning-end',
828
+ id: activeReasoningId,
829
+ });
830
+ }
831
+ for (const [id, state] of textStates) {
832
+ if (!state.ended) {
833
+ controller.enqueue({ type: 'text-end', id });
834
+ }
404
835
  }
405
-
406
836
  controller.enqueue({
407
837
  type: 'finish',
408
838
  finishReason,
409
839
  usage: convertPerplexityUsage(usage),
410
- providerMetadata,
840
+ providerMetadata: getProviderMetadata(usage),
411
841
  });
412
842
  },
413
843
  }),
@@ -418,88 +848,8 @@ export class PerplexityLanguageModel implements LanguageModelV4 {
418
848
  }
419
849
  }
420
850
 
421
- const perplexityCostSchema = z
422
- .object({
423
- input_tokens_cost: z.number().nullish(),
424
- output_tokens_cost: z.number().nullish(),
425
- reasoning_tokens_cost: z.number().nullish(),
426
- request_cost: z.number().nullish(),
427
- citation_tokens_cost: z.number().nullish(),
428
- search_queries_cost: z.number().nullish(),
429
- total_cost: z.number().nullish(),
430
- })
431
- .catchall(z.json());
432
-
433
- const perplexityUsageSchema = z
434
- .object({
435
- prompt_tokens: z.number(),
436
- completion_tokens: z.number(),
437
- total_tokens: z.number().nullish(),
438
- search_context_size: z.enum(['low', 'medium', 'high']).nullish(),
439
- citation_tokens: z.number().nullish(),
440
- num_search_queries: z.number().nullish(),
441
- reasoning_tokens: z.number().nullish(),
442
- cost: perplexityCostSchema.nullish(),
443
- })
444
- .catchall(z.json());
445
-
446
- export const perplexityImageSchema = z.object({
447
- image_url: z.string(),
448
- origin_url: z.string(),
449
- height: z.number(),
450
- width: z.number(),
451
- });
452
-
453
- // limited version of the schema, focussed on what is needed for the implementation
454
- // this approach limits breakages when the API changes and increases efficiency
455
- const perplexityResponseSchema = z.object({
456
- id: z.string(),
457
- created: z.number(),
458
- model: z.string(),
459
- choices: z.array(
460
- z.object({
461
- message: z.object({
462
- role: z.literal('assistant'),
463
- content: z.string(),
464
- }),
465
- finish_reason: z.string().nullish(),
466
- }),
467
- ),
468
- citations: z.array(z.string()).nullish(),
469
- images: z.array(perplexityImageSchema).nullish(),
470
- usage: perplexityUsageSchema.nullish(),
471
- });
472
-
473
- // limited version of the schema, focussed on what is needed for the implementation
474
- // this approach limits breakages when the API changes and increases efficiency
475
- const perplexityChunkSchema = z.object({
476
- id: z.string(),
477
- created: z.number(),
478
- model: z.string(),
479
- choices: z.array(
480
- z.object({
481
- delta: z.object({
482
- role: z.literal('assistant').optional(),
483
- content: z.string().nullish(),
484
- }),
485
- finish_reason: z.string().nullish(),
486
- }),
487
- ),
488
- citations: z.array(z.string()).nullish(),
489
- images: z.array(perplexityImageSchema).nullish(),
490
- usage: perplexityUsageSchema.nullish(),
491
- });
492
-
493
- export const perplexityErrorSchema = z.object({
494
- error: z.object({
495
- code: z.number(),
496
- message: z.string().nullish(),
497
- type: z.string().nullish(),
498
- }),
499
- });
500
-
501
- export type PerplexityErrorData = z.infer<typeof perplexityErrorSchema>;
502
-
503
- const errorToMessage = (data: PerplexityErrorData) => {
504
- return data.error.message ?? data.error.type ?? 'unknown error';
505
- };
851
+ export {
852
+ perplexityErrorSchema,
853
+ perplexityErrorToMessage,
854
+ } from './perplexity-agent-api';
855
+ export type { PerplexityErrorData } from './perplexity-agent-api';