@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2

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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -7,9 +7,12 @@
7
7
  * SQLite store, never POSTs the broker sentinel to an OpenAI token endpoint.
8
8
  */
9
9
  import * as os from "node:os";
10
- import { type AuthStorage, type FetchImpl, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
10
+ import { type AuthStorage, type FetchImpl, type Model, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
11
11
  import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
12
+ import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
13
+ import { createOpenAICodexCompatibilityMetadata } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
12
14
  import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
15
+ import { CODEX_CLIENT_VERSION, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "@oh-my-pi/pi-catalog/wire/codex";
13
16
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
14
17
  import packageJson from "../../../../package.json" with { type: "json" };
15
18
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
@@ -22,6 +25,9 @@ const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
22
25
  const CODEX_RESPONSES_PATH = "/codex/responses";
23
26
  const FALLBACK_MODEL = "gpt-5.5";
24
27
  const DEFAULT_MODEL_PREFERENCES = [
28
+ "gpt-5.6-luna",
29
+ "gpt-5.6-terra",
30
+ "gpt-5.6-sol",
25
31
  "gpt-5.5",
26
32
  "gpt-5.4",
27
33
  "gpt-5-codex",
@@ -35,15 +41,38 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
35
41
  const DEFAULT_INSTRUCTIONS =
36
42
  "You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
37
43
 
38
- function getConfiguredModel(): string | undefined {
44
+ type CodexSearchModel = Model<"openai-codex-responses">;
45
+
46
+ interface CodexModelCandidate {
47
+ modelId: string;
48
+ catalogModel?: CodexSearchModel;
49
+ }
50
+
51
+ function getBundledCodexModels(): CodexSearchModel[] {
52
+ const models: CodexSearchModel[] = [];
53
+ for (const model of getBundledModels("openai-codex")) {
54
+ if (model.api === "openai-codex-responses") {
55
+ models.push(model as CodexSearchModel);
56
+ }
57
+ }
58
+ return models;
59
+ }
60
+
61
+ function getConfiguredModel(): CodexModelCandidate | undefined {
39
62
  const configuredModel = $env.PI_CODEX_WEB_SEARCH_MODEL?.trim();
40
- return configuredModel ? configuredModel : undefined;
63
+ if (!configuredModel) return undefined;
64
+
65
+ const catalogModel = getBundledCodexModels().find(model => model.id === configuredModel);
66
+ return { modelId: configuredModel, ...(catalogModel ? { catalogModel } : {}) };
41
67
  }
42
68
 
43
- function getDefaultModelCandidates(): string[] {
44
- const bundledModels = getBundledModels("openai-codex");
45
- const bundledIds = new Set(bundledModels.map(model => model.id));
46
- const candidates = DEFAULT_MODEL_PREFERENCES.filter(modelId => bundledIds.has(modelId));
69
+ function getDefaultModelCandidates(): CodexModelCandidate[] {
70
+ const bundledModels = getBundledCodexModels();
71
+ const candidates: CodexModelCandidate[] = [];
72
+ for (const modelId of DEFAULT_MODEL_PREFERENCES) {
73
+ const catalogModel = bundledModels.find(model => model.id === modelId);
74
+ if (catalogModel) candidates.push({ modelId, catalogModel });
75
+ }
47
76
 
48
77
  if (candidates.length > 0) {
49
78
  return candidates;
@@ -51,10 +80,11 @@ function getDefaultModelCandidates(): string[] {
51
80
 
52
81
  const nonMini = bundledModels.find(model => !model.id.includes("mini") && !model.id.includes("spark"));
53
82
  if (nonMini) {
54
- return [nonMini.id];
83
+ return [{ modelId: nonMini.id, catalogModel: nonMini }];
55
84
  }
56
85
 
57
- return bundledModels[0]?.id ? [bundledModels[0].id] : [FALLBACK_MODEL];
86
+ const fallbackModel = bundledModels[0];
87
+ return fallbackModel ? [{ modelId: fallbackModel.id, catalogModel: fallbackModel }] : [{ modelId: FALLBACK_MODEL }];
58
88
  }
59
89
 
60
90
  function shouldRetryWithNextDefaultModel(error: unknown): boolean {
@@ -301,9 +331,10 @@ async function findCodexAuth(
301
331
  function buildCodexHeaders(accessToken: string, accountId: string): Record<string, string> {
302
332
  return {
303
333
  Authorization: `Bearer ${accessToken}`,
304
- "chatgpt-account-id": accountId,
305
- "OpenAI-Beta": "responses=experimental",
306
- originator: "pi",
334
+ [OPENAI_HEADERS.ACCOUNT_ID]: accountId,
335
+ [OPENAI_HEADERS.BETA]: OPENAI_HEADER_VALUES.BETA_RESPONSES,
336
+ [OPENAI_HEADERS.ORIGINATOR]: OPENAI_HEADER_VALUES.ORIGINATOR_CODEX,
337
+ [OPENAI_HEADERS.VERSION]: CODEX_CLIENT_VERSION,
307
338
  "User-Agent": `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`,
308
339
  Accept: "text/event-stream",
309
340
  "Content-Type": "application/json",
@@ -323,7 +354,8 @@ async function callCodexSearch(
323
354
  signal?: AbortSignal;
324
355
  systemPrompt?: string;
325
356
  searchContextSize?: "low" | "medium" | "high";
326
- modelId: string;
357
+ model: CodexModelCandidate;
358
+ sessionId?: string;
327
359
  fetch?: FetchImpl;
328
360
  },
329
361
  ): Promise<{
@@ -336,7 +368,8 @@ async function callCodexSearch(
336
368
  const url = `${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}`;
337
369
  const headers = buildCodexHeaders(auth.accessToken, auth.accountId);
338
370
 
339
- const requestedModel = options.modelId;
371
+ const requestedModel = options.model.modelId;
372
+ const usesResponsesLite = options.model.catalogModel?.useResponsesLite === true;
340
373
 
341
374
  const body: Record<string, unknown> = {
342
375
  model: requestedModel,
@@ -358,6 +391,18 @@ async function callCodexSearch(
358
391
  tool_choice: { type: "web_search" },
359
392
  instructions: options.systemPrompt ?? DEFAULT_INSTRUCTIONS,
360
393
  };
394
+ if (usesResponsesLite) {
395
+ const metadata = createOpenAICodexCompatibilityMetadata({
396
+ sessionId: options.sessionId,
397
+ requestKind: "turn",
398
+ startNewTurn: true,
399
+ });
400
+ Object.assign(headers, metadata.headers);
401
+ headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
402
+ body.client_metadata = metadata.clientMetadata;
403
+ body.reasoning = { context: "all_turns" };
404
+ applyCodexResponsesLiteShape(body);
405
+ }
361
406
 
362
407
  const fetchImpl = options.fetch ?? fetch;
363
408
  const response = await fetchImpl(url, {
@@ -489,10 +534,11 @@ async function callCodexSearch(
489
534
  * Default-model behavior:
490
535
  * - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
491
536
  * upstream error verbatim.
492
- * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.4, GPT-5
493
- * Codex, GPT-5, …) and retry the next candidate only when Codex returns the
494
- * known 400 "model is not supported" family. This avoids selecting
495
- * `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
537
+ * - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.6 Luna,
538
+ * Terra, Sol, GPT-5.5, …) and retry the next candidate only when Codex
539
+ * returns the known 400 "model is not supported" family. This avoids
540
+ * selecting `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI
541
+ * rejects.
496
542
  */
497
543
  export async function searchCodex(params: SearchParams): Promise<SearchResponse> {
498
544
  const seed = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
@@ -520,15 +566,16 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
520
566
 
521
567
  let lastError: unknown;
522
568
  for (let index = 0; index < modelCandidates.length; index += 1) {
523
- const modelId = modelCandidates[index];
524
- if (!modelId) continue;
569
+ const candidate = modelCandidates[index];
570
+ if (!candidate) continue;
525
571
 
526
572
  try {
527
573
  return await callCodexSearch(auth, params.query, {
528
574
  signal: params.signal,
529
575
  systemPrompt: params.systemPrompt,
530
576
  searchContextSize: "high",
531
- modelId,
577
+ model: candidate,
578
+ sessionId: params.sessionId,
532
579
  fetch: params.fetch,
533
580
  });
534
581
  } catch (error) {
@@ -44,17 +44,26 @@ export async function getApiConfigs(
44
44
  const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
45
45
  const configs: ApiConfig[] = [];
46
46
 
47
- const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, options);
48
- if (perplexityKey) {
49
- configs.push({
50
- type: "api_key",
51
- apiKey: perplexityKey,
52
- provider: "perplexity",
53
- chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
54
- responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
55
- modelPrefix: "",
56
- useResponses,
57
- });
47
+ // A Perplexity OAuth session and a real API key are mutually exclusive here:
48
+ // when the active credential origin is OAuth, `getApiKey("perplexity")`
49
+ // returns the OAuth session JWT (OAuth wins in AuthStorage.getApiKey), not an
50
+ // api.perplexity.ai key. Emitting it as a direct api-key config makes the
51
+ // search loop send the session token as a Bearer to the direct API endpoint,
52
+ // which rejects it with 401 and masks the real (transport) failure — see #5315.
53
+ // Skip the direct config in that case; the OAuth ask-endpoint method covers it.
54
+ if (authStorage.getCredentialOrigin("perplexity")?.kind !== "oauth") {
55
+ const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, options);
56
+ if (perplexityKey) {
57
+ configs.push({
58
+ type: "api_key",
59
+ apiKey: perplexityKey,
60
+ provider: "perplexity",
61
+ chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
62
+ responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
63
+ modelPrefix: "",
64
+ useResponses,
65
+ });
66
+ }
58
67
  }
59
68
 
60
69
  const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, options);
@@ -604,7 +604,7 @@ async function callPerplexityAsk(
604
604
  requestParams.send_back_text_in_streaming_api = true;
605
605
  }
606
606
 
607
- const response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, {
607
+ const requestInit = {
608
608
  method: "POST",
609
609
  headers,
610
610
  body: JSON.stringify({
@@ -612,7 +612,19 @@ async function callPerplexityAsk(
612
612
  params: requestParams,
613
613
  }),
614
614
  signal: withHardTimeout(params.signal),
615
- });
615
+ };
616
+
617
+ // The consumer ask endpoint intermittently drops the socket before sending an
618
+ // HTTP response (#5315). Retry the transport exactly once; once we hold an
619
+ // HTTP response (handled below) the outcome — including non-2xx — is final and
620
+ // never retried, so a real 401/429 is never papered over by a second attempt.
621
+ let response: Response;
622
+ try {
623
+ response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, requestInit);
624
+ } catch (error) {
625
+ if (params.signal?.aborted) throw error;
626
+ response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, requestInit);
627
+ }
616
628
 
617
629
  if (!response.ok) {
618
630
  const errorText = await response.text();