@oh-my-pi/pi-coding-agent 16.0.5 → 16.0.6

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.
Files changed (223) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cli.js +1927 -1376
  3. package/dist/types/advisor/advise-tool.d.ts +22 -19
  4. package/dist/types/autoresearch/tools/init-experiment.d.ts +13 -17
  5. package/dist/types/autoresearch/tools/log-experiment.d.ts +17 -19
  6. package/dist/types/autoresearch/tools/run-experiment.d.ts +3 -4
  7. package/dist/types/autoresearch/tools/update-notes.d.ts +4 -5
  8. package/dist/types/cli/ttsr-cli.d.ts +39 -0
  9. package/dist/types/commands/ttsr.d.ts +57 -0
  10. package/dist/types/commit/agentic/tools/analyze-file.d.ts +4 -5
  11. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +4 -5
  12. package/dist/types/commit/agentic/tools/git-hunk.d.ts +5 -6
  13. package/dist/types/commit/agentic/tools/git-overview.d.ts +4 -5
  14. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +23 -24
  15. package/dist/types/commit/agentic/tools/propose-commit.d.ts +11 -32
  16. package/dist/types/commit/agentic/tools/recent-commits.d.ts +3 -4
  17. package/dist/types/commit/agentic/tools/schemas.d.ts +6 -27
  18. package/dist/types/commit/agentic/tools/split-commit.d.ts +28 -49
  19. package/dist/types/commit/changelog/generate.d.ts +12 -13
  20. package/dist/types/commit/shared-llm.d.ts +10 -37
  21. package/dist/types/config/config-file.d.ts +4 -4
  22. package/dist/types/config/keybindings.d.ts +5 -0
  23. package/dist/types/config/models-config-schema.d.ts +625 -990
  24. package/dist/types/config/models-config.d.ts +229 -217
  25. package/dist/types/config/settings-schema.d.ts +53 -23
  26. package/dist/types/edit/hashline/params.d.ts +7 -11
  27. package/dist/types/edit/index.d.ts +2 -1
  28. package/dist/types/edit/modes/apply-patch.d.ts +4 -5
  29. package/dist/types/edit/modes/patch.d.ts +15 -24
  30. package/dist/types/edit/modes/replace.d.ts +16 -17
  31. package/dist/types/eval/js/index.d.ts +1 -0
  32. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  33. package/dist/types/extensibility/custom-tools/types.d.ts +8 -5
  34. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  35. package/dist/types/extensibility/hooks/types.d.ts +7 -4
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +13 -5
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +17 -0
  38. package/dist/types/extensibility/typebox.d.ts +80 -58
  39. package/dist/types/goals/tools/goal-tool.d.ts +11 -24
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/lsp/index.d.ts +11 -26
  42. package/dist/types/lsp/types.d.ts +12 -28
  43. package/dist/types/mcp/client.d.ts +8 -0
  44. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  46. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  47. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +3 -0
  49. package/dist/types/modes/setup-wizard/index.d.ts +1 -0
  50. package/dist/types/modes/setup-wizard/startup-splash.d.ts +7 -0
  51. package/dist/types/modes/theme/theme.d.ts +1 -1
  52. package/dist/types/modes/types.d.ts +3 -0
  53. package/dist/types/sdk.d.ts +5 -0
  54. package/dist/types/session/agent-session.d.ts +4 -0
  55. package/dist/types/startup-splash.d.ts +12 -0
  56. package/dist/types/task/types.d.ts +47 -48
  57. package/dist/types/tools/ask.d.ts +26 -27
  58. package/dist/types/tools/ast-edit.d.ts +17 -17
  59. package/dist/types/tools/ast-grep.d.ts +12 -13
  60. package/dist/types/tools/bash.d.ts +20 -17
  61. package/dist/types/tools/browser.d.ts +46 -71
  62. package/dist/types/tools/checkpoint.d.ts +14 -15
  63. package/dist/types/tools/debug.d.ts +82 -145
  64. package/dist/types/tools/eval.d.ts +30 -40
  65. package/dist/types/tools/find.d.ts +17 -18
  66. package/dist/types/tools/gh.d.ts +49 -78
  67. package/dist/types/tools/image-gen.d.ts +20 -36
  68. package/dist/types/tools/inspect-image.d.ts +10 -11
  69. package/dist/types/tools/irc.d.ts +22 -33
  70. package/dist/types/tools/job.d.ts +11 -12
  71. package/dist/types/tools/learn.d.ts +21 -28
  72. package/dist/types/tools/manage-skill.d.ts +13 -22
  73. package/dist/types/tools/memory-edit.d.ts +15 -24
  74. package/dist/types/tools/memory-recall.d.ts +7 -8
  75. package/dist/types/tools/memory-reflect.d.ts +9 -10
  76. package/dist/types/tools/memory-retain.d.ts +13 -14
  77. package/dist/types/tools/read.d.ts +7 -8
  78. package/dist/types/tools/resolve.d.ts +11 -18
  79. package/dist/types/tools/review.d.ts +9 -15
  80. package/dist/types/tools/search-tool-bm25.d.ts +9 -10
  81. package/dist/types/tools/search.d.ts +16 -17
  82. package/dist/types/tools/ssh.d.ts +14 -15
  83. package/dist/types/tools/todo.d.ts +27 -43
  84. package/dist/types/tools/tts.d.ts +8 -9
  85. package/dist/types/tools/write.d.ts +9 -10
  86. package/dist/types/tui/index.d.ts +1 -0
  87. package/dist/types/tui/width-aware-text.d.ts +23 -0
  88. package/dist/types/utils/markit.d.ts +10 -1
  89. package/dist/types/web/search/index.d.ts +17 -28
  90. package/dist/types/web/search/providers/perplexity.d.ts +0 -2
  91. package/dist/types/web/search/types.d.ts +32 -26
  92. package/package.json +14 -13
  93. package/scripts/omp +1 -1
  94. package/src/advisor/__tests__/advisor.test.ts +44 -1
  95. package/src/advisor/advise-tool.ts +34 -11
  96. package/src/autoresearch/tools/init-experiment.ts +13 -16
  97. package/src/autoresearch/tools/log-experiment.ts +15 -18
  98. package/src/autoresearch/tools/run-experiment.ts +3 -3
  99. package/src/autoresearch/tools/update-notes.ts +4 -4
  100. package/src/cli/ttsr-cli.ts +995 -0
  101. package/src/cli-commands.ts +1 -0
  102. package/src/cli.ts +7 -1
  103. package/src/commands/ttsr.ts +125 -0
  104. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  105. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  106. package/src/commit/agentic/tools/git-hunk.ts +7 -5
  107. package/src/commit/agentic/tools/git-overview.ts +4 -4
  108. package/src/commit/agentic/tools/propose-changelog.ts +18 -15
  109. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  110. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  111. package/src/commit/agentic/tools/schemas.ts +8 -20
  112. package/src/commit/agentic/tools/split-commit.ts +19 -23
  113. package/src/commit/analysis/summary.ts +7 -5
  114. package/src/commit/changelog/generate.ts +15 -11
  115. package/src/commit/shared-llm.ts +17 -24
  116. package/src/config/config-file.ts +13 -15
  117. package/src/config/keybindings.ts +6 -0
  118. package/src/config/models-config-schema.ts +206 -179
  119. package/src/config/settings-schema.ts +34 -0
  120. package/src/discovery/builtin-rules/index.ts +2 -0
  121. package/src/discovery/builtin-rules/ts-import-type.md +2 -2
  122. package/src/discovery/builtin-rules/ts-no-any.md +11 -2
  123. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +55 -0
  124. package/src/edit/hashline/params.ts +12 -11
  125. package/src/edit/index.ts +5 -4
  126. package/src/edit/modes/apply-patch.ts +4 -4
  127. package/src/edit/modes/patch.ts +15 -18
  128. package/src/edit/modes/replace.ts +13 -17
  129. package/src/edit/renderer.ts +0 -1
  130. package/src/eval/agent-bridge.ts +11 -13
  131. package/src/eval/completion-bridge.ts +25 -17
  132. package/src/eval/js/context-manager.ts +17 -2
  133. package/src/eval/js/index.ts +1 -1
  134. package/src/eval/py/executor.ts +2 -2
  135. package/src/extensibility/custom-commands/loader.ts +5 -3
  136. package/src/extensibility/custom-commands/types.ts +6 -3
  137. package/src/extensibility/custom-tools/loader.ts +4 -2
  138. package/src/extensibility/custom-tools/types.ts +8 -5
  139. package/src/extensibility/extensions/loader.ts +4 -2
  140. package/src/extensibility/extensions/types.ts +6 -3
  141. package/src/extensibility/hooks/loader.ts +5 -2
  142. package/src/extensibility/hooks/types.ts +7 -4
  143. package/src/extensibility/legacy-pi-ai-shim.ts +42 -5
  144. package/src/extensibility/legacy-pi-coding-agent-shim.ts +113 -0
  145. package/src/extensibility/plugins/legacy-pi-compat.ts +13 -13
  146. package/src/extensibility/tool-proxy.ts +4 -1
  147. package/src/extensibility/typebox.ts +778 -251
  148. package/src/goals/guided-setup.ts +12 -3
  149. package/src/goals/tools/goal-tool.ts +6 -6
  150. package/src/index.ts +2 -0
  151. package/src/internal-urls/docs-index.generated.ts +11 -9
  152. package/src/lsp/types.ts +13 -27
  153. package/src/main.ts +19 -18
  154. package/src/mcp/client.ts +38 -13
  155. package/src/mcp/render.ts +102 -89
  156. package/src/modes/components/agent-hub.ts +11 -4
  157. package/src/modes/components/btw-panel.ts +5 -1
  158. package/src/modes/components/custom-editor.ts +18 -0
  159. package/src/modes/components/status-line/component.ts +8 -1
  160. package/src/modes/components/tool-execution.ts +17 -10
  161. package/src/modes/controllers/btw-controller.ts +69 -1
  162. package/src/modes/controllers/input-controller.ts +29 -0
  163. package/src/modes/interactive-mode.ts +38 -8
  164. package/src/modes/setup-wizard/index.ts +1 -0
  165. package/src/modes/setup-wizard/scenes/sign-in.ts +77 -5
  166. package/src/modes/setup-wizard/startup-splash.ts +107 -0
  167. package/src/modes/theme/theme.ts +133 -143
  168. package/src/modes/types.ts +3 -0
  169. package/src/modes/utils/context-usage.ts +9 -5
  170. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/sdk.ts +21 -4
  173. package/src/session/agent-session.ts +160 -33
  174. package/src/session/session-history-format.ts +11 -2
  175. package/src/session/snapcompact-inline.ts +1 -1
  176. package/src/slash-commands/builtin-registry.ts +4 -11
  177. package/src/startup-splash.ts +19 -0
  178. package/src/task/executor.ts +11 -6
  179. package/src/task/types.ts +44 -41
  180. package/src/tool-discovery/tool-index.ts +17 -4
  181. package/src/tools/ask.ts +14 -14
  182. package/src/tools/ast-edit.ts +17 -14
  183. package/src/tools/ast-grep.ts +10 -9
  184. package/src/tools/bash.ts +15 -10
  185. package/src/tools/browser/launch.ts +13 -0
  186. package/src/tools/browser.ts +26 -32
  187. package/src/tools/checkpoint.ts +7 -7
  188. package/src/tools/debug.ts +72 -69
  189. package/src/tools/eval.ts +18 -19
  190. package/src/tools/find.ts +20 -13
  191. package/src/tools/gh.ts +29 -49
  192. package/src/tools/image-gen.ts +27 -32
  193. package/src/tools/inspect-image.ts +8 -9
  194. package/src/tools/irc.ts +12 -12
  195. package/src/tools/job.ts +6 -6
  196. package/src/tools/learn.ts +11 -14
  197. package/src/tools/manage-skill.ts +19 -23
  198. package/src/tools/memory-edit.ts +8 -8
  199. package/src/tools/memory-recall.ts +4 -4
  200. package/src/tools/memory-reflect.ts +5 -5
  201. package/src/tools/memory-retain.ts +9 -11
  202. package/src/tools/puppeteer/02_stealth_hairline.txt +1 -1
  203. package/src/tools/puppeteer/04_stealth_iframe.txt +4 -4
  204. package/src/tools/puppeteer/05_stealth_webgl.txt +1 -1
  205. package/src/tools/puppeteer/10_stealth_plugins.txt +6 -4
  206. package/src/tools/puppeteer/12_stealth_codecs.txt +2 -2
  207. package/src/tools/puppeteer/13_stealth_worker.txt +1 -1
  208. package/src/tools/read.ts +169 -13
  209. package/src/tools/report-tool-issue.ts +6 -6
  210. package/src/tools/resolve.ts +6 -6
  211. package/src/tools/review.ts +10 -12
  212. package/src/tools/search-tool-bm25.ts +5 -5
  213. package/src/tools/search.ts +20 -29
  214. package/src/tools/ssh.ts +8 -8
  215. package/src/tools/todo.ts +16 -19
  216. package/src/tools/tts.ts +16 -15
  217. package/src/tools/write.ts +5 -5
  218. package/src/tui/index.ts +1 -0
  219. package/src/tui/width-aware-text.ts +58 -0
  220. package/src/utils/markit.ts +17 -2
  221. package/src/web/search/index.ts +9 -9
  222. package/src/web/search/providers/perplexity.ts +373 -126
  223. package/src/web/search/types.ts +28 -48
@@ -8,12 +8,24 @@
8
8
  * - Anonymous via `www.perplexity.ai/rest/sse/perplexity_ask`
9
9
  */
10
10
 
11
- import { type AuthStorage, type FetchImpl, getEnvApiKey, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
11
+ import {
12
+ type AssistantMessage,
13
+ type AssistantMessageEventStream,
14
+ type AuthStorage,
15
+ type Context,
16
+ type FetchImpl,
17
+ type OAuthAccess,
18
+ type Usage,
19
+ withOAuthAccess,
20
+ } from "@oh-my-pi/pi-ai";
21
+ import { streamOpenAICompletions } from "@oh-my-pi/pi-ai/providers/openai-completions";
22
+ import { streamOpenAIResponses } from "@oh-my-pi/pi-ai/providers/openai-responses";
23
+ import { buildModel } from "@oh-my-pi/pi-catalog/build";
24
+ import type { Model, ModelSpec } from "@oh-my-pi/pi-catalog/types";
12
25
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
13
26
  import type {
14
- PerplexityMessageOutput,
15
27
  PerplexityRequest,
16
- PerplexityResponse,
28
+ PerplexitySearchResult,
17
29
  SearchCitation,
18
30
  SearchResponse,
19
31
  SearchSource,
@@ -24,7 +36,9 @@ import type { SearchParams } from "./base";
24
36
  import { SearchProvider } from "./base";
25
37
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
26
38
 
27
- const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
39
+ const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
40
+ const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
41
+ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
28
42
  const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask";
29
43
 
30
44
  const DEFAULT_MAX_TOKENS = 8192;
@@ -37,10 +51,7 @@ const ANONYMOUS_USER_AGENT =
37
51
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
38
52
 
39
53
  type PerplexityAuth =
40
- | {
41
- type: "api_key";
42
- token: string;
43
- }
54
+ | ApiConfig
44
55
  | {
45
56
  type: "oauth";
46
57
  access: OAuthAccess;
@@ -278,9 +289,52 @@ export interface PerplexitySearchParams {
278
289
  fetch?: FetchImpl;
279
290
  }
280
291
 
281
- /** Find PERPLEXITY_API_KEY from environment or .env files (also checks PPLX_API_KEY) */
282
- export function findApiKey(): string | null {
283
- return getEnvApiKey("perplexity") ?? null;
292
+ interface ApiConfig {
293
+ type: "api_key";
294
+ apiKey: string;
295
+ provider: "perplexity" | "openrouter";
296
+ chatBaseUrl: string;
297
+ responsesBaseUrl: string;
298
+ modelPrefix: string;
299
+ useResponses: boolean;
300
+ }
301
+
302
+ /** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
303
+ async function getApiConfigs(
304
+ authStorage: AuthStorage,
305
+ sessionId: string | undefined,
306
+ signal: AbortSignal | undefined,
307
+ ): Promise<ApiConfig[]> {
308
+ const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
309
+ const configs: ApiConfig[] = [];
310
+
311
+ const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, { signal });
312
+ if (perplexityKey) {
313
+ configs.push({
314
+ type: "api_key",
315
+ apiKey: perplexityKey,
316
+ provider: "perplexity",
317
+ chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
318
+ responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
319
+ modelPrefix: "",
320
+ useResponses,
321
+ });
322
+ }
323
+
324
+ const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, { signal });
325
+ if (openrouterKey) {
326
+ configs.push({
327
+ type: "api_key",
328
+ apiKey: openrouterKey,
329
+ provider: "openrouter",
330
+ chatBaseUrl: OPENROUTER_BASE_URL,
331
+ responsesBaseUrl: OPENROUTER_BASE_URL,
332
+ modelPrefix: "perplexity/",
333
+ useResponses,
334
+ });
335
+ }
336
+
337
+ return configs;
284
338
  }
285
339
 
286
340
  /**
@@ -302,86 +356,236 @@ function jwtExpiryMs(token: string): number | undefined {
302
356
  }
303
357
  }
304
358
 
305
- async function findOAuthAccess(
359
+ /** Collect all available auth methods to try in priority order */
360
+ async function getAvailableAuthMethods(
306
361
  authStorage: AuthStorage,
307
362
  sessionId: string | undefined,
308
363
  signal: AbortSignal | undefined,
309
- ): Promise<OAuthAccess | null> {
364
+ ): Promise<PerplexityAuth[]> {
365
+ const methods: PerplexityAuth[] = [];
366
+
367
+ // 1. Perplexity OAuth & Cookies (same priority - highest)
310
368
  try {
311
- // `getOAuthAccess` returns the raw OAuth bearer only — runtime/config
312
- // api_key overrides and stored api_key credentials are intentionally
313
- // suppressed so we don't POST an `api.perplexity.ai` key to the
314
- // `www.perplexity.ai` session/SSE endpoint.
315
369
  const access = await authStorage.getOAuthAccess("perplexity", sessionId, { signal });
316
370
  const token = access?.accessToken;
317
- if (!access || !token) return null;
318
- // Trust the JWT's own `exp` claim if it has one; otherwise treat as
319
- // non-expiring. Perplexity session JWTs commonly omit `exp`.
320
- const jwtExpiry = jwtExpiryMs(token);
321
- if (jwtExpiry !== undefined && jwtExpiry <= Date.now() + OAUTH_EXPIRY_BUFFER_MS) return null;
322
- return access;
371
+ if (access && token) {
372
+ const jwtExpiry = jwtExpiryMs(token);
373
+ if (jwtExpiry === undefined || jwtExpiry > Date.now() + OAUTH_EXPIRY_BUFFER_MS) {
374
+ methods.push({ type: "oauth", access });
375
+ }
376
+ }
323
377
  } catch {
324
- return null;
378
+ // ignored
325
379
  }
326
- }
327
380
 
328
- async function findPerplexityAuth(
329
- authStorage: AuthStorage,
330
- sessionId: string | undefined,
331
- signal: AbortSignal | undefined,
332
- ): Promise<PerplexityAuth> {
333
- // 1. PERPLEXITY_COOKIES env var
334
381
  const cookies = $env.PERPLEXITY_COOKIES?.trim();
335
382
  if (cookies) {
336
- return { type: "cookies", cookies };
383
+ methods.push({ type: "cookies", cookies });
337
384
  }
338
385
 
339
- const apiKey = findApiKey();
386
+ const apiConfigs = await getApiConfigs(authStorage, sessionId, signal);
387
+ methods.push(...apiConfigs);
340
388
 
341
- // 2. OAuth/session bearer from AuthStorage.
342
- const oauthAccess = await findOAuthAccess(authStorage, sessionId, signal);
343
- if (oauthAccess) {
344
- return { type: "oauth", access: oauthAccess };
389
+ // 5. Fallback to Perplexity free (anonymous)
390
+ if (methods.length === 0) {
391
+ methods.push({ type: "anonymous" });
345
392
  }
346
393
 
347
- // 3. PERPLEXITY_API_KEY env var
348
- if (apiKey) {
349
- return { type: "api_key", token: apiKey };
394
+ return methods;
395
+ }
396
+
397
+ interface PerplexityApiStreamMetadata {
398
+ id?: string;
399
+ model?: string;
400
+ citations?: unknown;
401
+ search_results?: unknown;
402
+ related_questions?: unknown;
403
+ }
404
+
405
+ function buildPerplexityCompletionsModel(config: ApiConfig, request: PerplexityRequest): Model<"openai-completions"> {
406
+ const model = config.modelPrefix ? `${config.modelPrefix}${request.model}` : request.model;
407
+ const spec: ModelSpec<"openai-completions"> = {
408
+ id: model,
409
+ name: model,
410
+ api: "openai-completions",
411
+ provider: config.provider,
412
+ baseUrl: config.chatBaseUrl,
413
+ reasoning: false,
414
+ input: ["text"],
415
+ supportsTools: false,
416
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
417
+ contextWindow: null,
418
+ maxTokens: null,
419
+ compat: {
420
+ supportsStore: false,
421
+ supportsMultipleSystemMessages: true,
422
+ supportsReasoningParams: false,
423
+ supportsUsageInStreaming: true,
424
+ maxTokensField: "max_tokens",
425
+ },
426
+ };
427
+ return buildModel(spec);
428
+ }
429
+
430
+ function buildPerplexityResponsesModel(config: ApiConfig, request: PerplexityRequest): Model<"openai-responses"> {
431
+ const model = config.modelPrefix ? `${config.modelPrefix}${request.model}` : request.model;
432
+ const spec: ModelSpec<"openai-responses"> = {
433
+ id: model,
434
+ name: model,
435
+ api: "openai-responses",
436
+ provider: config.provider,
437
+ baseUrl: config.responsesBaseUrl,
438
+ reasoning: false,
439
+ input: ["text"],
440
+ supportsTools: false,
441
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
442
+ contextWindow: null,
443
+ maxTokens: null,
444
+ compat: {
445
+ alwaysSendMaxTokens: true,
446
+ supportsReasoningParams: false,
447
+ },
448
+ };
449
+ return buildModel(spec);
450
+ }
451
+
452
+ function buildPerplexityContext(request: PerplexityRequest): Context {
453
+ const systemPrompt: string[] = [];
454
+ const messages: Context["messages"] = [];
455
+ for (const message of request.messages) {
456
+ if (typeof message.content !== "string" || message.content.length === 0) continue;
457
+ if (message.role === "system") {
458
+ systemPrompt.push(message.content);
459
+ continue;
460
+ }
461
+ if (message.role === "user") {
462
+ messages.push({ role: "user", content: message.content, timestamp: 0 });
463
+ }
464
+ }
465
+ return { systemPrompt: systemPrompt.length > 0 ? systemPrompt : undefined, messages };
466
+ }
467
+
468
+ function buildPerplexityExtraBody(request: PerplexityRequest): Record<string, unknown> {
469
+ return {
470
+ search_mode: request.search_mode,
471
+ num_search_results: request.num_search_results,
472
+ web_search_options: request.web_search_options,
473
+ enable_search_classifier: request.enable_search_classifier,
474
+ reasoning_effort: request.reasoning_effort,
475
+ language_preference: request.language_preference,
476
+ return_related_questions: request.return_related_questions,
477
+ search_recency_filter: request.search_recency_filter,
478
+ };
479
+ }
480
+
481
+ function applyPerplexityExtraBody(payload: unknown, request: PerplexityRequest): void {
482
+ const record = asRecord(payload);
483
+ if (!record) return;
484
+ Object.assign(record, buildPerplexityExtraBody(request));
485
+ }
486
+
487
+ function collectPerplexityOutputMetadata(metadata: PerplexityApiStreamMetadata, output: unknown): void {
488
+ if (!Array.isArray(output)) return;
489
+ for (const item of output) {
490
+ const record = asRecord(item);
491
+ if (!record) continue;
492
+ if (Array.isArray(record.search_results)) metadata.search_results = record.search_results;
493
+ if (Array.isArray(record.results)) metadata.search_results = record.results;
494
+ if (Array.isArray(record.citations)) metadata.citations = record.citations;
495
+ if (Array.isArray(record.related_questions)) metadata.related_questions = record.related_questions;
496
+ collectPerplexityOutputMetadata(metadata, record.content);
350
497
  }
498
+ }
351
499
 
352
- // 4. The consumer ask endpoint currently accepts unauthenticated browser-style requests.
353
- return { type: "anonymous" };
500
+ function collectPerplexityMetadataFromRecord(
501
+ metadata: PerplexityApiStreamMetadata,
502
+ record: Record<string, unknown>,
503
+ ): void {
504
+ const id = record.id;
505
+ if (typeof id === "string" && id.length > 0) metadata.id = id;
506
+ const model = record.model;
507
+ if (typeof model === "string" && model.length > 0) metadata.model = model;
508
+ if (Array.isArray(record.citations)) metadata.citations = record.citations;
509
+ if (Array.isArray(record.search_results)) metadata.search_results = record.search_results;
510
+ if (Array.isArray(record.related_questions)) metadata.related_questions = record.related_questions;
511
+ if (Array.isArray(record.results)) metadata.search_results = record.results;
512
+ collectPerplexityOutputMetadata(metadata, record.output);
513
+ const response = asRecord(record.response);
514
+ if (response) {
515
+ collectPerplexityOutputMetadata(metadata, response.output);
516
+ collectPerplexityMetadataFromRecord(metadata, response);
517
+ }
354
518
  }
355
519
 
356
- /** Call Perplexity API-key endpoint. */
520
+ function collectPerplexityMetadata(metadata: PerplexityApiStreamMetadata, data: string): void {
521
+ if (data === "[DONE]") return;
522
+ const record = asRecord(parseJson(data));
523
+ if (record) collectPerplexityMetadataFromRecord(metadata, record);
524
+ }
525
+
526
+ async function drainAssistantStream(stream: AssistantMessageEventStream): Promise<AssistantMessage> {
527
+ let finalMessage: AssistantMessage | undefined;
528
+ for await (const event of stream) {
529
+ if (event.type === "done") {
530
+ finalMessage = event.message;
531
+ } else if (event.type === "error") {
532
+ finalMessage = event.error;
533
+ }
534
+ }
535
+ return finalMessage ?? stream.result();
536
+ }
537
+
538
+ function throwPerplexityStreamError(message: AssistantMessage): never {
539
+ const status = message.errorStatus ?? 500;
540
+ const details = message.errorMessage ?? "Perplexity API stream failed";
541
+ const classified = classifyProviderHttpError("perplexity", status, details);
542
+ if (classified) throw classified;
543
+ throw new SearchProviderError("perplexity", `Perplexity API error (${status}): ${details}`, status);
544
+ }
545
+
546
+ /** Call Perplexity API-key endpoint (or OpenRouter) through the shared OpenAI streaming providers. */
357
547
  async function callPerplexityApi(
358
- apiKey: string,
548
+ config: ApiConfig,
359
549
  request: PerplexityRequest,
360
550
  fetchImpl: FetchImpl | undefined,
361
551
  signal?: AbortSignal,
362
- ): Promise<PerplexityResponse> {
363
- const response = await (fetchImpl ?? fetch)(PERPLEXITY_API_URL, {
364
- method: "POST",
365
- headers: {
366
- Authorization: `Bearer ${apiKey}`,
367
- "Content-Type": "application/json",
368
- },
369
- body: JSON.stringify(request),
370
- signal: withHardTimeout(signal),
371
- });
372
-
373
- if (!response.ok) {
374
- const errorText = await response.text();
375
- const classified = classifyProviderHttpError("perplexity", response.status, errorText);
376
- if (classified) throw classified;
377
- throw new SearchProviderError(
378
- "perplexity",
379
- `Perplexity API error (${response.status}): ${errorText}`,
380
- response.status,
381
- );
382
- }
552
+ ): Promise<SearchResponse> {
553
+ const metadata: PerplexityApiStreamMetadata = {};
554
+ const context = buildPerplexityContext(request);
555
+ const requestSignal = withHardTimeout(signal);
556
+ const onSseEvent = (event: { data: string }): void => {
557
+ collectPerplexityMetadata(metadata, event.data);
558
+ };
383
559
 
384
- return response.json() as Promise<PerplexityResponse>;
560
+ const message = config.useResponses
561
+ ? await drainAssistantStream(
562
+ streamOpenAIResponses(buildPerplexityResponsesModel(config, request), context, {
563
+ apiKey: config.apiKey,
564
+ maxTokens: request.max_tokens ?? undefined,
565
+ temperature: request.temperature ?? undefined,
566
+ signal: requestSignal,
567
+ fetch: fetchImpl,
568
+ extraBody: buildPerplexityExtraBody(request),
569
+ onSseEvent,
570
+ }),
571
+ )
572
+ : await drainAssistantStream(
573
+ streamOpenAICompletions(buildPerplexityCompletionsModel(config, request), context, {
574
+ apiKey: config.apiKey,
575
+ maxTokens: request.max_tokens ?? undefined,
576
+ temperature: request.temperature ?? undefined,
577
+ signal: requestSignal,
578
+ fetch: fetchImpl,
579
+ onPayload: payload => applyPerplexityExtraBody(payload, request),
580
+ onSseEvent,
581
+ }),
582
+ );
583
+
584
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
585
+ throwPerplexityStreamError(message);
586
+ }
587
+
588
+ return parseStreamedApiResponse(message, metadata);
385
589
  }
386
590
 
387
591
  function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
@@ -570,26 +774,49 @@ async function callPerplexityAsk(
570
774
  };
571
775
  }
572
776
 
573
- function messageContentToText(content: PerplexityMessageOutput["content"]): string {
574
- if (!content) return "";
575
- if (typeof content === "string") return content;
576
- return content.map(chunk => (chunk.type === "text" ? chunk.text : "")).join("");
777
+ function assistantText(message: AssistantMessage): string {
778
+ let text = "";
779
+ for (const block of message.content) {
780
+ if (block.type === "text") text += block.text;
781
+ }
782
+ return text;
783
+ }
784
+
785
+ function isPerplexitySearchResult(value: unknown): value is PerplexitySearchResult {
786
+ const record = asRecord(value);
787
+ return typeof record?.url === "string" && record.url.length > 0;
788
+ }
789
+
790
+ function searchResultsFromMetadata(metadata: PerplexityApiStreamMetadata): PerplexitySearchResult[] {
791
+ return Array.isArray(metadata.search_results) ? metadata.search_results.filter(isPerplexitySearchResult) : [];
792
+ }
793
+
794
+ function citationUrlsFromMetadata(metadata: PerplexityApiStreamMetadata): string[] {
795
+ return Array.isArray(metadata.citations)
796
+ ? metadata.citations.filter((url): url is string => typeof url === "string" && url.length > 0)
797
+ : [];
577
798
  }
578
799
 
579
- /** Parse API response into unified SearchResponse */
580
- function parseResponse(response: PerplexityResponse): SearchResponse {
581
- const messageContent = response.choices[0]?.message?.content ?? null;
582
- const answer = messageContentToText(messageContent);
800
+ function relatedQuestionsFromMetadata(metadata: PerplexityApiStreamMetadata): string[] {
801
+ return Array.isArray(metadata.related_questions)
802
+ ? metadata.related_questions.filter(
803
+ (question): question is string => typeof question === "string" && question.trim().length > 0,
804
+ )
805
+ : [];
806
+ }
583
807
 
808
+ function buildApiSources(metadata: PerplexityApiStreamMetadata): {
809
+ sources: SearchSource[];
810
+ citations: SearchCitation[];
811
+ } {
584
812
  const sources: SearchSource[] = [];
585
813
  const citations: SearchCitation[] = [];
586
-
587
- const citationUrls = response.citations ?? [];
588
- const searchResults = response.search_results ?? [];
814
+ const searchResults = searchResultsFromMetadata(metadata);
815
+ const citationUrls = citationUrlsFromMetadata(metadata);
589
816
 
590
817
  if (citationUrls.length > 0) {
591
818
  for (const url of citationUrls) {
592
- const searchResult = searchResults.find(r => r.url === url);
819
+ const searchResult = searchResults.find(result => result.url === url);
593
820
  sources.push({
594
821
  title: searchResult?.title ?? url,
595
822
  url,
@@ -597,10 +824,7 @@ function parseResponse(response: PerplexityResponse): SearchResponse {
597
824
  publishedDate: searchResult?.date ?? undefined,
598
825
  ageSeconds: dateToAgeSeconds(searchResult?.date),
599
826
  });
600
- citations.push({
601
- url,
602
- title: searchResult?.title ?? url,
603
- });
827
+ citations.push({ url, title: searchResult?.title ?? url });
604
828
  }
605
829
  } else {
606
830
  for (const searchResult of searchResults) {
@@ -614,7 +838,22 @@ function parseResponse(response: PerplexityResponse): SearchResponse {
614
838
  }
615
839
  }
616
840
 
617
- const relatedQuestions = (response.related_questions ?? []).filter(q => q.trim().length > 0);
841
+ return { sources, citations };
842
+ }
843
+
844
+ function usageFromAssistant(usage: Usage): SearchResponse["usage"] | undefined {
845
+ if (usage.input === 0 && usage.output === 0 && usage.totalTokens === 0) return undefined;
846
+ return {
847
+ inputTokens: usage.input,
848
+ outputTokens: usage.output,
849
+ totalTokens: usage.totalTokens,
850
+ };
851
+ }
852
+
853
+ function parseStreamedApiResponse(message: AssistantMessage, metadata: PerplexityApiStreamMetadata): SearchResponse {
854
+ const { sources, citations } = buildApiSources(metadata);
855
+ const relatedQuestions = relatedQuestionsFromMetadata(metadata);
856
+ const answer = assistantText(message);
618
857
 
619
858
  return {
620
859
  provider: "perplexity",
@@ -622,15 +861,9 @@ function parseResponse(response: PerplexityResponse): SearchResponse {
622
861
  sources,
623
862
  citations: citations.length > 0 ? citations : undefined,
624
863
  relatedQuestions: relatedQuestions.length > 0 ? relatedQuestions : undefined,
625
- usage: response.usage
626
- ? {
627
- inputTokens: response.usage.prompt_tokens,
628
- outputTokens: response.usage.completion_tokens,
629
- totalTokens: response.usage.total_tokens,
630
- }
631
- : undefined,
632
- model: response.model,
633
- requestId: response.id,
864
+ usage: usageFromAssistant(message.usage),
865
+ model: metadata.model ?? message.model,
866
+ requestId: metadata.id ?? message.responseId,
634
867
  };
635
868
  }
636
869
 
@@ -643,35 +876,6 @@ function applySourceLimit(result: SearchResponse, limit?: number): SearchRespons
643
876
 
644
877
  /** Execute Perplexity web search */
645
878
  export async function searchPerplexity(params: PerplexitySearchParams): Promise<SearchResponse> {
646
- const auth = await findPerplexityAuth(params.authStorage, params.sessionId, params.signal);
647
-
648
- if (auth.type !== "api_key") {
649
- // OAuth bearer mode routes the whole authenticated unit (the ask
650
- // session/SSE request) through the central auth-retry policy so a 401 or
651
- // usage-limit force-refreshes, then rotates to a sibling credential.
652
- // Cookie/env/anonymous modes have no rotatable credential — untouched.
653
- const askResult =
654
- auth.type === "oauth"
655
- ? await withOAuthAccess(
656
- params.authStorage,
657
- "perplexity",
658
- access => callPerplexityAsk({ type: "oauth", token: access.accessToken }, params),
659
- { sessionId: params.sessionId, signal: params.signal, seed: auth.access },
660
- )
661
- : await callPerplexityAsk(auth, params);
662
- return applySourceLimit(
663
- {
664
- provider: "perplexity",
665
- answer: askResult.answer || undefined,
666
- sources: askResult.sources,
667
- model: askResult.model,
668
- requestId: askResult.requestId,
669
- authMode: auth.type === "anonymous" ? "anonymous" : "oauth",
670
- },
671
- params.num_results,
672
- );
673
- }
674
-
675
879
  const systemPrompt = params.system_prompt;
676
880
  const messages: PerplexityRequest["messages"] = [];
677
881
  if (systemPrompt) {
@@ -700,10 +904,51 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
700
904
  request.search_recency_filter = params.search_recency_filter;
701
905
  }
702
906
 
703
- const response = await callPerplexityApi(auth.token, request, params.fetch, params.signal);
704
- const result = parseResponse(response);
705
- result.authMode = "api_key";
706
- return applySourceLimit(result, params.num_results);
907
+ const authMethods = await getAvailableAuthMethods(params.authStorage, params.sessionId, params.signal);
908
+ let lastError: unknown;
909
+
910
+ for (const auth of authMethods) {
911
+ if (auth.type === "api_key") {
912
+ try {
913
+ const result = await callPerplexityApi(auth, request, params.fetch, params.signal);
914
+ result.authMode = "api_key";
915
+ return applySourceLimit(result, params.num_results);
916
+ } catch (error) {
917
+ if (params.signal?.aborted) throw error;
918
+ lastError = error;
919
+ }
920
+ } else {
921
+ // Use OAuth/cookies/anonymous path
922
+ try {
923
+ const askResult =
924
+ auth.type === "oauth"
925
+ ? await withOAuthAccess(
926
+ params.authStorage,
927
+ "perplexity",
928
+ access => callPerplexityAsk({ type: "oauth", token: access.accessToken }, params),
929
+ { sessionId: params.sessionId, signal: params.signal, seed: auth.access },
930
+ )
931
+ : await callPerplexityAsk(auth, params);
932
+ return applySourceLimit(
933
+ {
934
+ provider: "perplexity",
935
+ answer: askResult.answer || undefined,
936
+ sources: askResult.sources,
937
+ model: askResult.model,
938
+ requestId: askResult.requestId,
939
+ authMode: auth.type === "anonymous" ? "anonymous" : "oauth",
940
+ },
941
+ params.num_results,
942
+ );
943
+ } catch (error) {
944
+ if (params.signal?.aborted) throw error;
945
+ lastError = error;
946
+ }
947
+ }
948
+ }
949
+
950
+ if (lastError) throw lastError;
951
+ throw new SearchProviderError("perplexity", "No authentication method available.", 401);
707
952
  }
708
953
 
709
954
  /** Search provider for Perplexity. */
@@ -712,7 +957,9 @@ export class PerplexityProvider extends SearchProvider {
712
957
  readonly label = "Perplexity";
713
958
 
714
959
  isAvailable(authStorage: AuthStorage): boolean {
715
- return !!$env.PERPLEXITY_COOKIES?.trim() || authStorage.hasAuth("perplexity") || !!findApiKey();
960
+ return (
961
+ !!$env.PERPLEXITY_COOKIES?.trim() || authStorage.hasAuth("perplexity") || authStorage.hasAuth("openrouter")
962
+ );
716
963
  }
717
964
 
718
965
  /**