@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5

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 (112) hide show
  1. package/CHANGELOG.md +86 -43
  2. package/dist/cli.js +3540 -3521
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  14. package/dist/types/launch/spawn-options.d.ts +10 -0
  15. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  16. package/dist/types/modes/components/status-line/component.d.ts +2 -3
  17. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  18. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  19. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  20. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  21. package/dist/types/modes/interactive-mode.d.ts +2 -0
  22. package/dist/types/modes/print-mode.d.ts +4 -0
  23. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  25. package/dist/types/modes/types.d.ts +2 -0
  26. package/dist/types/sdk.d.ts +2 -0
  27. package/dist/types/session/agent-session.d.ts +19 -1
  28. package/dist/types/session/messages.d.ts +6 -0
  29. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  30. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  31. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  32. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  33. package/dist/types/tools/gh.d.ts +5 -3
  34. package/dist/types/tools/hub/index.d.ts +1 -1
  35. package/dist/types/tools/hub/jobs.d.ts +1 -0
  36. package/dist/types/tools/hub/types.d.ts +2 -0
  37. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  38. package/dist/types/utils/git.d.ts +33 -0
  39. package/dist/types/web/search/providers/codex.d.ts +1 -1
  40. package/package.json +12 -12
  41. package/src/advisor/__tests__/advisor.test.ts +150 -0
  42. package/src/advisor/advise-tool.ts +4 -0
  43. package/src/advisor/runtime.ts +38 -14
  44. package/src/async/job-manager.ts +3 -0
  45. package/src/cli/bench-cli.ts +8 -2
  46. package/src/cli/dry-balance-cli.ts +1 -0
  47. package/src/config/model-registry.ts +89 -8
  48. package/src/config/model-resolver.ts +78 -14
  49. package/src/config/models-config-schema.ts +1 -0
  50. package/src/config/settings.ts +3 -1
  51. package/src/dap/client.ts +168 -1
  52. package/src/dap/config.ts +51 -1
  53. package/src/dap/session.ts +575 -234
  54. package/src/dap/types.ts +6 -5
  55. package/src/discovery/agents.ts +2 -2
  56. package/src/extensibility/extensions/runner.ts +6 -4
  57. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  58. package/src/launch/broker.ts +34 -31
  59. package/src/launch/client.ts +7 -1
  60. package/src/launch/spawn-options.test.ts +31 -0
  61. package/src/launch/spawn-options.ts +17 -0
  62. package/src/lsp/types.ts +5 -1
  63. package/src/main.ts +17 -4
  64. package/src/mcp/transports/stdio.test.ts +9 -1
  65. package/src/modes/components/ask-dialog.ts +137 -73
  66. package/src/modes/components/status-line/component.ts +2 -2
  67. package/src/modes/components/status-line/segments.ts +20 -3
  68. package/src/modes/components/status-line/types.ts +3 -1
  69. package/src/modes/components/tool-execution.ts +5 -0
  70. package/src/modes/components/transcript-container.ts +23 -122
  71. package/src/modes/components/tree-selector.ts +10 -4
  72. package/src/modes/controllers/event-controller.ts +1 -2
  73. package/src/modes/controllers/input-controller.ts +1 -1
  74. package/src/modes/controllers/selector-controller.ts +29 -8
  75. package/src/modes/interactive-mode.ts +40 -8
  76. package/src/modes/noninteractive-dispose.test.ts +12 -1
  77. package/src/modes/print-mode.ts +21 -9
  78. package/src/modes/rpc/rpc-input.ts +38 -0
  79. package/src/modes/rpc/rpc-mode.ts +7 -2
  80. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  81. package/src/modes/types.ts +2 -0
  82. package/src/modes/utils/ui-helpers.ts +3 -2
  83. package/src/prompts/tools/browser.md +3 -2
  84. package/src/prompts/tools/debug.md +2 -7
  85. package/src/prompts/tools/github.md +6 -1
  86. package/src/prompts/tools/hub.md +4 -2
  87. package/src/prompts/tools/task-async-contract.md +1 -0
  88. package/src/prompts/tools/task.md +9 -2
  89. package/src/sdk.ts +38 -6
  90. package/src/session/agent-session.ts +395 -162
  91. package/src/session/messages.test.ts +91 -0
  92. package/src/session/messages.ts +248 -110
  93. package/src/slash-commands/builtin-registry.ts +1 -0
  94. package/src/task/executor.ts +59 -33
  95. package/src/task/index.ts +46 -9
  96. package/src/task/worktree.ts +10 -0
  97. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  98. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  99. package/src/tools/browser/cmux/socket-client.ts +139 -3
  100. package/src/tools/browser/run-cancellation.ts +4 -0
  101. package/src/tools/browser/tab-protocol.ts +2 -0
  102. package/src/tools/browser/tab-supervisor.ts +21 -11
  103. package/src/tools/browser/tab-worker.ts +199 -33
  104. package/src/tools/debug.ts +3 -0
  105. package/src/tools/gh.ts +40 -2
  106. package/src/tools/hub/index.ts +4 -1
  107. package/src/tools/hub/jobs.ts +42 -1
  108. package/src/tools/hub/types.ts +2 -0
  109. package/src/tools/tool-timeouts.ts +1 -1
  110. package/src/utils/git.ts +237 -0
  111. package/src/web/search/index.ts +9 -5
  112. package/src/web/search/providers/codex.ts +195 -99
@@ -1,28 +1,40 @@
1
1
  /**
2
2
  * OpenAI Codex Web Search Provider
3
3
  *
4
- * Uses Codex's built-in web_search tool via the Responses API.
5
- * Auth is resolved through `AuthStorage.getOAuthAccess("openai-codex")` so the
6
- * broker is the sole refresh authority — this module never opens a sibling
7
- * SQLite store, never POSTs the broker sentinel to an OpenAI token endpoint.
4
+ * Uses the configured Codex Responses transport for proxy/API-key setups and
5
+ * the official ChatGPT backend for OAuth logins.
8
6
  */
9
7
  import * as os from "node:os";
10
- import { type AuthStorage, type FetchImpl, type Model, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
11
- import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
8
+ import {
9
+ type AuthStorage,
10
+ type FetchImpl,
11
+ type Model,
12
+ type OAuthAccess,
13
+ withAuth,
14
+ withOAuthAccess,
15
+ } from "@oh-my-pi/pi-ai";
12
16
  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";
17
+ import {
18
+ createOpenAICodexCompatibilityMetadata,
19
+ resolveCodexResponsesUrl,
20
+ } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
14
21
  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";
22
+ import {
23
+ CODEX_BASE_URL,
24
+ CODEX_CLIENT_VERSION,
25
+ getCodexAccountId,
26
+ OPENAI_HEADER_VALUES,
27
+ OPENAI_HEADERS,
28
+ } from "@oh-my-pi/pi-catalog/wire/codex";
16
29
  import { $env, readSseJson } from "@oh-my-pi/pi-utils";
17
30
  import packageJson from "../../../../package.json" with { type: "json" };
31
+ import type { ModelRegistry } from "../../../config/model-registry";
18
32
  import type { SearchResponse, SearchSource } from "../../../web/search/types";
19
33
  import { SearchProviderError } from "../../../web/search/types";
20
34
  import type { SearchParams } from "./base";
21
35
  import { SearchProvider } from "./base";
22
36
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
23
37
 
24
- const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
25
- const CODEX_RESPONSES_PATH = "/codex/responses";
26
38
  const FALLBACK_MODEL = "gpt-5.5";
27
39
  const DEFAULT_MODEL_PREFERENCES = [
28
40
  "gpt-5.6-luna",
@@ -37,7 +49,6 @@ const DEFAULT_MODEL_PREFERENCES = [
37
49
  "gpt-5.1-codex",
38
50
  "gpt-5-codex-mini",
39
51
  ];
40
- const JWT_CLAIM_PATH = "https://api.openai.com/auth";
41
52
  const DEFAULT_INSTRUCTIONS =
42
53
  "You are a helpful assistant with web search capabilities. Search the web to answer the user's question accurately and cite your sources.";
43
54
 
@@ -48,6 +59,21 @@ interface CodexModelCandidate {
48
59
  catalogModel?: CodexSearchModel;
49
60
  }
50
61
 
62
+ interface CodexSearchTransport {
63
+ baseUrl: string;
64
+ url: string;
65
+ headers: Record<string, string>;
66
+ customEndpoint: boolean;
67
+ }
68
+
69
+ interface CodexSearchResult {
70
+ answer: string;
71
+ sources: SearchSource[];
72
+ model: string;
73
+ requestId: string;
74
+ usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
75
+ }
76
+
51
77
  function getBundledCodexModels(): CodexSearchModel[] {
52
78
  const models: CodexSearchModel[] = [];
53
79
  for (const model of getBundledModels("openai-codex")) {
@@ -295,18 +321,6 @@ function extractTextSources(text: string): SearchSource[] {
295
321
  return sources;
296
322
  }
297
323
 
298
- /**
299
- * Extracts account ID from a Codex access token.
300
- * @param accessToken - JWT access token
301
- * @returns Account ID string, or null if not found
302
- */
303
- function getAccountIdFromJwt(accessToken: string): string | null {
304
- const payload = decodeJwt(accessToken);
305
- const auth = payload?.[JWT_CLAIM_PATH] as { chatgpt_account_id?: string } | undefined;
306
- const accountId = auth?.chatgpt_account_id;
307
- return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
308
- }
309
-
310
324
  /**
311
325
  * Resolve a Codex bearer + accountId through {@link AuthStorage} — the single
312
326
  * refresh authority. Returns `null` when no OAuth credential is configured,
@@ -320,25 +334,55 @@ async function findCodexAuth(
320
334
  ): Promise<{ access: OAuthAccess; accountId: string } | null> {
321
335
  const access = await authStorage.getOAuthAccess("openai-codex", sessionId, { signal });
322
336
  if (!access) return null;
323
- const accountId = access.accountId ?? getAccountIdFromJwt(access.accessToken);
337
+ const accountId = access.accountId ?? getCodexAccountId(access.accessToken);
324
338
  if (!accountId) return null;
325
339
  return { access, accountId };
326
340
  }
327
341
 
342
+ function resolveCodexSearchTransport(modelRegistry: ModelRegistry | undefined, modelId: string): CodexSearchTransport {
343
+ const registryModel = modelRegistry?.find("openai-codex", modelId);
344
+ const bundledModel = getBundledCodexModels().find(model => model.id === modelId);
345
+ const providerBaseUrl = modelRegistry?.getProviderBaseUrl("openai-codex");
346
+ let baseUrl = providerBaseUrl ?? registryModel?.baseUrl ?? CODEX_BASE_URL;
347
+ if (registryModel?.baseUrl && registryModel.baseUrl !== (bundledModel?.baseUrl ?? CODEX_BASE_URL)) {
348
+ baseUrl = registryModel.baseUrl;
349
+ }
350
+
351
+ const url = resolveCodexResponsesUrl(baseUrl);
352
+ return {
353
+ baseUrl,
354
+ url,
355
+ headers: {
356
+ ...(modelRegistry?.getProviderHeaders("openai-codex") ?? {}),
357
+ ...(registryModel?.headers ?? {}),
358
+ },
359
+ customEndpoint: url !== resolveCodexResponsesUrl(CODEX_BASE_URL),
360
+ };
361
+ }
362
+
328
363
  /**
329
364
  * Builds HTTP headers for Codex API requests.
330
365
  */
331
- function buildCodexHeaders(accessToken: string, accountId: string): Record<string, string> {
332
- return {
333
- Authorization: `Bearer ${accessToken}`,
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,
338
- "User-Agent": `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`,
339
- Accept: "text/event-stream",
340
- "Content-Type": "application/json",
341
- };
366
+ function buildCodexHeaders(
367
+ accessToken: string,
368
+ accountId: string | undefined,
369
+ configuredHeaders: Record<string, string>,
370
+ ): Headers {
371
+ const headers = new Headers(configuredHeaders);
372
+ headers.delete("x-api-key");
373
+ headers.set("Authorization", `Bearer ${accessToken}`);
374
+ if (accountId) {
375
+ headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
376
+ } else {
377
+ headers.delete(OPENAI_HEADERS.ACCOUNT_ID);
378
+ }
379
+ headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
380
+ headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
381
+ headers.set(OPENAI_HEADERS.VERSION, CODEX_CLIENT_VERSION);
382
+ headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
383
+ headers.set("Accept", "text/event-stream");
384
+ headers.set("Content-Type", "application/json");
385
+ return headers;
342
386
  }
343
387
 
344
388
  /**
@@ -348,7 +392,7 @@ function buildCodexHeaders(accessToken: string, accountId: string): Record<strin
348
392
  * overrides from the default ChatGPT-account model-selection path.
349
393
  */
350
394
  async function callCodexSearch(
351
- auth: { accessToken: string; accountId: string },
395
+ auth: { accessToken: string; accountId?: string },
352
396
  query: string,
353
397
  options: {
354
398
  signal?: AbortSignal;
@@ -357,16 +401,10 @@ async function callCodexSearch(
357
401
  model: CodexModelCandidate;
358
402
  sessionId?: string;
359
403
  fetch?: FetchImpl;
404
+ transport: CodexSearchTransport;
360
405
  },
361
- ): Promise<{
362
- answer: string;
363
- sources: SearchSource[];
364
- model: string;
365
- requestId: string;
366
- usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
367
- }> {
368
- const url = `${CODEX_BASE_URL}${CODEX_RESPONSES_PATH}`;
369
- const headers = buildCodexHeaders(auth.accessToken, auth.accountId);
406
+ ): Promise<CodexSearchResult> {
407
+ const headers = buildCodexHeaders(auth.accessToken, auth.accountId, options.transport.headers);
370
408
 
371
409
  const requestedModel = options.model.modelId;
372
410
  const usesResponsesLite = options.model.catalogModel?.useResponsesLite === true;
@@ -397,15 +435,18 @@ async function callCodexSearch(
397
435
  requestKind: "turn",
398
436
  startNewTurn: true,
399
437
  });
400
- Object.assign(headers, metadata.headers);
401
- headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
438
+ for (const name in metadata.headers) {
439
+ const value = metadata.headers[name];
440
+ if (value !== undefined) headers.set(name, value);
441
+ }
442
+ headers.set(OPENAI_HEADERS.RESPONSES_LITE, "true");
402
443
  body.client_metadata = metadata.clientMetadata;
403
444
  body.reasoning = { context: "all_turns" };
404
445
  applyCodexResponsesLiteShape(body);
405
446
  }
406
447
 
407
448
  const fetchImpl = options.fetch ?? fetch;
408
- const response = await fetchImpl(url, {
449
+ const response = await fetchImpl(options.transport.url, {
409
450
  method: "POST",
410
451
  headers,
411
452
  body: JSON.stringify(body),
@@ -528,6 +569,39 @@ async function callCodexSearch(
528
569
  };
529
570
  }
530
571
 
572
+ async function runCodexSearchCandidates(options: {
573
+ auth: { accessToken: string; accountId?: string };
574
+ params: SearchParams;
575
+ modelCandidates: CodexModelCandidate[];
576
+ modelWasConfigured: boolean;
577
+ transport: CodexSearchTransport;
578
+ }): Promise<CodexSearchResult> {
579
+ let lastError: unknown;
580
+ for (let index = 0; index < options.modelCandidates.length; index += 1) {
581
+ const candidate = options.modelCandidates[index];
582
+ if (!candidate) continue;
583
+
584
+ try {
585
+ return await callCodexSearch(options.auth, options.params.query, {
586
+ signal: options.params.signal,
587
+ systemPrompt: options.params.systemPrompt,
588
+ searchContextSize: "high",
589
+ model: candidate,
590
+ sessionId: options.params.sessionId,
591
+ fetch: options.params.fetch,
592
+ transport: options.transport,
593
+ });
594
+ } catch (error) {
595
+ lastError = error;
596
+ const isLastCandidate = index === options.modelCandidates.length - 1;
597
+ if (options.modelWasConfigured || isLastCandidate || !shouldRetryWithNextDefaultModel(error)) {
598
+ throw error;
599
+ }
600
+ }
601
+ }
602
+ throw lastError ?? new Error("Codex search failed without returning a result");
603
+ }
604
+
531
605
  /**
532
606
  * Executes a web search using OpenAI Codex's built-in web search tool.
533
607
  *
@@ -541,55 +615,81 @@ async function callCodexSearch(
541
615
  * rejects.
542
616
  */
543
617
  export async function searchCodex(params: SearchParams): Promise<SearchResponse> {
544
- const seed = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
545
- if (!seed) {
546
- throw new Error(
547
- "No Codex OAuth credentials found. Login with 'omp /login openai-codex' to enable Codex web search.",
548
- );
549
- }
550
-
551
618
  const configuredModel = getConfiguredModel();
552
619
  const modelCandidates = configuredModel ? [configuredModel] : getDefaultModelCandidates();
620
+ const firstCandidate = modelCandidates[0];
621
+ if (!firstCandidate) {
622
+ throw new SearchProviderError("codex", "No Codex web search model is configured.");
623
+ }
624
+ const transport = resolveCodexSearchTransport(params.modelRegistry, firstCandidate.modelId);
625
+
626
+ let result: CodexSearchResult;
627
+ if (transport.customEndpoint) {
628
+ // ModelRegistry resolves command-backed provider keys before consulting
629
+ // its AuthStorage, so a lower-priority OAuth origin is irrelevant when
630
+ // that command source is configured.
631
+ const credentialSource = params.modelRegistry?.authStorage ?? params.authStorage;
632
+ const credentialOrigin = credentialSource.getCredentialOrigin("openai-codex");
633
+ const hasCommandBackedKey = params.modelRegistry?.hasCommandBackedApiKey("openai-codex") === true;
634
+ if (!hasCommandBackedKey && (credentialOrigin?.kind === "oauth" || credentialOrigin?.kind === "env")) {
635
+ throw new SearchProviderError(
636
+ "codex",
637
+ `Refusing to send official Codex OAuth credentials to custom endpoint ${transport.baseUrl}. Configure an API key for provider "openai-codex".`,
638
+ );
639
+ }
553
640
 
554
- const result = await withOAuthAccess(
555
- params.authStorage,
556
- "openai-codex",
557
- async access => {
558
- // Derive ALL auth material from the access this attempt received —
559
- // a refreshed/rotated credential carries a different bearer and
560
- // ChatGPT account id than the seed.
561
- const accountId = access.accountId ?? getAccountIdFromJwt(access.accessToken);
562
- if (!accountId) {
563
- throw new Error("Codex OAuth credential is missing a ChatGPT account id");
564
- }
565
- const auth = { accessToken: access.accessToken, accountId };
566
-
567
- let lastError: unknown;
568
- for (let index = 0; index < modelCandidates.length; index += 1) {
569
- const candidate = modelCandidates[index];
570
- if (!candidate) continue;
571
-
572
- try {
573
- return await callCodexSearch(auth, params.query, {
574
- signal: params.signal,
575
- systemPrompt: params.systemPrompt,
576
- searchContextSize: "high",
577
- model: candidate,
578
- sessionId: params.sessionId,
579
- fetch: params.fetch,
580
- });
581
- } catch (error) {
582
- lastError = error;
583
- const isLastCandidate = index === modelCandidates.length - 1;
584
- if (configuredModel || isLastCandidate || !shouldRetryWithNextDefaultModel(error)) {
585
- throw error;
586
- }
641
+ const resolverOptions = {
642
+ sessionId: params.sessionId,
643
+ baseUrl: transport.baseUrl,
644
+ modelId: firstCandidate.modelId,
645
+ };
646
+ const keyOrResolver = params.modelRegistry
647
+ ? params.modelRegistry.resolver("openai-codex", resolverOptions)
648
+ : params.authStorage.resolver("openai-codex", resolverOptions);
649
+ result = await withAuth(
650
+ keyOrResolver,
651
+ accessToken =>
652
+ runCodexSearchCandidates({
653
+ auth: { accessToken },
654
+ params,
655
+ modelCandidates,
656
+ modelWasConfigured: configuredModel !== undefined,
657
+ transport,
658
+ }),
659
+ {
660
+ signal: params.signal,
661
+ missingKeyMessage: 'Codex credentials not found. Configure an API key for provider "openai-codex".',
662
+ },
663
+ );
664
+ } else {
665
+ const seed = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
666
+ if (!seed) {
667
+ throw new Error(
668
+ "No Codex OAuth credentials found. Login with 'omp /login openai-codex' to enable Codex web search.",
669
+ );
670
+ }
671
+
672
+ result = await withOAuthAccess(
673
+ params.authStorage,
674
+ "openai-codex",
675
+ access => {
676
+ // A refreshed/rotated credential can carry a different bearer and
677
+ // ChatGPT account id than the seed used to select the first attempt.
678
+ const accountId = access.accountId ?? getCodexAccountId(access.accessToken);
679
+ if (!accountId) {
680
+ throw new Error("Codex OAuth credential is missing a ChatGPT account id");
587
681
  }
588
- }
589
- throw lastError ?? new Error("Codex search failed without returning a result");
590
- },
591
- { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
592
- );
682
+ return runCodexSearchCandidates({
683
+ auth: { accessToken: access.accessToken, accountId },
684
+ params,
685
+ modelCandidates,
686
+ modelWasConfigured: configuredModel !== undefined,
687
+ transport,
688
+ });
689
+ },
690
+ { sessionId: params.sessionId, signal: params.signal, seed: seed.access },
691
+ );
692
+ }
593
693
 
594
694
  let sources = result.sources;
595
695
 
@@ -615,14 +715,10 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
615
715
  }
616
716
 
617
717
  /**
618
- * Checks if Codex web search is available.
718
+ * Checks whether Codex web search has an API key or OAuth credential.
619
719
  */
620
720
  export async function hasCodexSearch(authStorage: AuthStorage): Promise<boolean> {
621
- // `isAvailable` runs before every request — keep the probe cheap.
622
- // `hasOAuth(...)` is a synchronous in-memory check that returns true as soon
623
- // as a Codex OAuth credential is loaded, without driving the refresh
624
- // pipeline. The actual refresh happens lazily in `searchCodex`.
625
- return authStorage.hasOAuth("openai-codex");
721
+ return authStorage.hasAuth("openai-codex");
626
722
  }
627
723
 
628
724
  /** Search provider for OpenAI Codex web search. */