@ai-sdk/perplexity 3.0.63 → 3.1.1

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