@sayknow-cli/ai 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/package.json +24 -25
  2. package/src/auth-gateway/server.ts +164 -22
  3. package/src/auth-storage.ts +76 -43
  4. package/src/context-cap-policy.ts +59 -0
  5. package/src/index.ts +2 -0
  6. package/src/model-cache.ts +10 -0
  7. package/src/model-manager.ts +30 -17
  8. package/src/model-thinking.ts +14 -8
  9. package/src/providers/amazon-bedrock.ts +9 -1
  10. package/src/providers/anthropic-messages-server.ts +178 -17
  11. package/src/providers/anthropic.ts +199 -213
  12. package/src/providers/azure-openai-responses.ts +6 -1
  13. package/src/providers/google-gemini-cli.ts +26 -13
  14. package/src/providers/google-shared.ts +7 -1
  15. package/src/providers/ollama.ts +7 -1
  16. package/src/providers/openai-chat-server.ts +93 -5
  17. package/src/providers/openai-codex/response-handler.ts +11 -3
  18. package/src/providers/openai-codex-responses.ts +107 -20
  19. package/src/providers/openai-responses-server.ts +111 -36
  20. package/src/providers/openai-responses-shared.ts +71 -12
  21. package/src/providers/openai-responses.ts +26 -3
  22. package/src/providers/pi-native-client.ts +24 -12
  23. package/src/providers/pi-native-server.ts +275 -21
  24. package/src/types.ts +53 -4
  25. package/src/utils/discovery/codex.ts +3 -12
  26. package/src/utils/event-stream.ts +146 -58
  27. package/src/utils/fallback-transport.ts +269 -0
  28. package/src/utils/overflow.ts +72 -31
  29. package/src/utils/retry.ts +2 -2
  30. package/src/utils/validation.ts +6 -1
  31. package/src/utils.ts +120 -12
  32. package/dist/types/api-registry.d.ts +0 -30
  33. package/dist/types/auth-broker/client.d.ts +0 -67
  34. package/dist/types/auth-broker/index.d.ts +0 -5
  35. package/dist/types/auth-broker/refresher.d.ts +0 -25
  36. package/dist/types/auth-broker/remote-store.d.ts +0 -99
  37. package/dist/types/auth-broker/server.d.ts +0 -32
  38. package/dist/types/auth-broker/types.d.ts +0 -110
  39. package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
  40. package/dist/types/auth-gateway/http.d.ts +0 -40
  41. package/dist/types/auth-gateway/index.d.ts +0 -3
  42. package/dist/types/auth-gateway/server.d.ts +0 -17
  43. package/dist/types/auth-gateway/types.d.ts +0 -115
  44. package/dist/types/auth-storage.d.ts +0 -695
  45. package/dist/types/cli.d.ts +0 -2
  46. package/dist/types/index.d.ts +0 -51
  47. package/dist/types/model-cache.d.ts +0 -17
  48. package/dist/types/model-manager.d.ts +0 -62
  49. package/dist/types/model-retirements.d.ts +0 -6
  50. package/dist/types/model-thinking.d.ts +0 -74
  51. package/dist/types/models.d.ts +0 -21
  52. package/dist/types/provider-details.d.ts +0 -24
  53. package/dist/types/provider-models/bundled-references.d.ts +0 -4
  54. package/dist/types/provider-models/descriptors.d.ts +0 -48
  55. package/dist/types/provider-models/google.d.ts +0 -20
  56. package/dist/types/provider-models/index.d.ts +0 -5
  57. package/dist/types/provider-models/ollama.d.ts +0 -7
  58. package/dist/types/provider-models/openai-compat.d.ts +0 -256
  59. package/dist/types/provider-models/special.d.ts +0 -19
  60. package/dist/types/providers/amazon-bedrock.d.ts +0 -60
  61. package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
  62. package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
  63. package/dist/types/providers/anthropic.d.ts +0 -208
  64. package/dist/types/providers/aws-credential-config.d.ts +0 -19
  65. package/dist/types/providers/aws-credentials.d.ts +0 -43
  66. package/dist/types/providers/aws-eventstream.d.ts +0 -38
  67. package/dist/types/providers/aws-sigv4.d.ts +0 -55
  68. package/dist/types/providers/azure-openai-responses.d.ts +0 -15
  69. package/dist/types/providers/composer-discipline.d.ts +0 -26
  70. package/dist/types/providers/cursor/client-version.d.ts +0 -10
  71. package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
  72. package/dist/types/providers/cursor.d.ts +0 -45
  73. package/dist/types/providers/error-message.d.ts +0 -27
  74. package/dist/types/providers/github-copilot-headers.d.ts +0 -40
  75. package/dist/types/providers/gitlab-duo.d.ts +0 -27
  76. package/dist/types/providers/google-auth.d.ts +0 -24
  77. package/dist/types/providers/google-gemini-cli.d.ts +0 -75
  78. package/dist/types/providers/google-gemini-headers.d.ts +0 -43
  79. package/dist/types/providers/google-shared.d.ts +0 -179
  80. package/dist/types/providers/google-types.d.ts +0 -138
  81. package/dist/types/providers/google-vertex.d.ts +0 -7
  82. package/dist/types/providers/google.d.ts +0 -4
  83. package/dist/types/providers/grammar.d.ts +0 -1
  84. package/dist/types/providers/kimi.d.ts +0 -27
  85. package/dist/types/providers/mock.d.ts +0 -177
  86. package/dist/types/providers/ollama.d.ts +0 -41
  87. package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
  88. package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
  89. package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
  90. package/dist/types/providers/openai-chat-server.d.ts +0 -16
  91. package/dist/types/providers/openai-codex/constants.d.ts +0 -26
  92. package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
  93. package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
  94. package/dist/types/providers/openai-codex-responses.d.ts +0 -67
  95. package/dist/types/providers/openai-completions-compat.d.ts +0 -27
  96. package/dist/types/providers/openai-completions.d.ts +0 -33
  97. package/dist/types/providers/openai-request-transform.d.ts +0 -4
  98. package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
  99. package/dist/types/providers/openai-responses-server.d.ts +0 -17
  100. package/dist/types/providers/openai-responses-shared.d.ts +0 -104
  101. package/dist/types/providers/openai-responses.d.ts +0 -32
  102. package/dist/types/providers/pi-native-client.d.ts +0 -13
  103. package/dist/types/providers/pi-native-server.d.ts +0 -68
  104. package/dist/types/providers/register-builtins.d.ts +0 -31
  105. package/dist/types/providers/synthetic.d.ts +0 -26
  106. package/dist/types/providers/transform-messages.d.ts +0 -14
  107. package/dist/types/providers/vision-guard.d.ts +0 -8
  108. package/dist/types/rate-limit-utils.d.ts +0 -19
  109. package/dist/types/stream.d.ts +0 -43
  110. package/dist/types/types.d.ts +0 -831
  111. package/dist/types/usage/claude.d.ts +0 -3
  112. package/dist/types/usage/gemini.d.ts +0 -2
  113. package/dist/types/usage/github-copilot.d.ts +0 -7
  114. package/dist/types/usage/google-antigravity.d.ts +0 -2
  115. package/dist/types/usage/grok-cli.d.ts +0 -10
  116. package/dist/types/usage/kimi.d.ts +0 -2
  117. package/dist/types/usage/minimax-code.d.ts +0 -2
  118. package/dist/types/usage/openai-codex.d.ts +0 -3
  119. package/dist/types/usage/shared.d.ts +0 -1
  120. package/dist/types/usage/zai.d.ts +0 -2
  121. package/dist/types/usage.d.ts +0 -258
  122. package/dist/types/utils/abort.d.ts +0 -19
  123. package/dist/types/utils/anthropic-auth.d.ts +0 -31
  124. package/dist/types/utils/discovery/antigravity.d.ts +0 -67
  125. package/dist/types/utils/discovery/codex.d.ts +0 -38
  126. package/dist/types/utils/discovery/cursor.d.ts +0 -23
  127. package/dist/types/utils/discovery/gemini.d.ts +0 -25
  128. package/dist/types/utils/discovery/index.d.ts +0 -4
  129. package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
  130. package/dist/types/utils/event-stream.d.ts +0 -33
  131. package/dist/types/utils/fireworks-model-id.d.ts +0 -10
  132. package/dist/types/utils/foundry.d.ts +0 -1
  133. package/dist/types/utils/h2-fetch.d.ts +0 -22
  134. package/dist/types/utils/http-inspector.d.ts +0 -35
  135. package/dist/types/utils/idle-iterator.d.ts +0 -67
  136. package/dist/types/utils/json-parse.d.ts +0 -18
  137. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
  138. package/dist/types/utils/oauth/anthropic.d.ts +0 -22
  139. package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
  140. package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
  141. package/dist/types/utils/oauth/callback-server.d.ts +0 -60
  142. package/dist/types/utils/oauth/cerebras.d.ts +0 -1
  143. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
  144. package/dist/types/utils/oauth/cursor.d.ts +0 -15
  145. package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
  146. package/dist/types/utils/oauth/deepseek.d.ts +0 -10
  147. package/dist/types/utils/oauth/firepass.d.ts +0 -1
  148. package/dist/types/utils/oauth/fireworks.d.ts +0 -1
  149. package/dist/types/utils/oauth/fugu.d.ts +0 -1
  150. package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
  151. package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
  152. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
  153. package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
  154. package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
  155. package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
  156. package/dist/types/utils/oauth/huggingface.d.ts +0 -19
  157. package/dist/types/utils/oauth/index.d.ts +0 -39
  158. package/dist/types/utils/oauth/kagi.d.ts +0 -17
  159. package/dist/types/utils/oauth/kilo.d.ts +0 -5
  160. package/dist/types/utils/oauth/kimi.d.ts +0 -21
  161. package/dist/types/utils/oauth/litellm.d.ts +0 -18
  162. package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
  163. package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
  164. package/dist/types/utils/oauth/moonshot.d.ts +0 -1
  165. package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
  166. package/dist/types/utils/oauth/nvidia.d.ts +0 -18
  167. package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
  168. package/dist/types/utils/oauth/ollama.d.ts +0 -18
  169. package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
  170. package/dist/types/utils/oauth/opencode.d.ts +0 -18
  171. package/dist/types/utils/oauth/parallel.d.ts +0 -17
  172. package/dist/types/utils/oauth/perplexity.d.ts +0 -9
  173. package/dist/types/utils/oauth/pkce.d.ts +0 -8
  174. package/dist/types/utils/oauth/qianfan.d.ts +0 -17
  175. package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
  176. package/dist/types/utils/oauth/synthetic.d.ts +0 -1
  177. package/dist/types/utils/oauth/tavily.d.ts +0 -17
  178. package/dist/types/utils/oauth/together.d.ts +0 -1
  179. package/dist/types/utils/oauth/types.d.ts +0 -45
  180. package/dist/types/utils/oauth/venice.d.ts +0 -18
  181. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
  182. package/dist/types/utils/oauth/vllm.d.ts +0 -16
  183. package/dist/types/utils/oauth/xai.d.ts +0 -30
  184. package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
  185. package/dist/types/utils/oauth/zai.d.ts +0 -18
  186. package/dist/types/utils/oauth/zenmux.d.ts +0 -1
  187. package/dist/types/utils/overflow.d.ts +0 -64
  188. package/dist/types/utils/parse-bind.d.ts +0 -23
  189. package/dist/types/utils/provider-response.d.ts +0 -3
  190. package/dist/types/utils/retry-after.d.ts +0 -3
  191. package/dist/types/utils/retry-budget.d.ts +0 -1
  192. package/dist/types/utils/retry.d.ts +0 -26
  193. package/dist/types/utils/schema/adapt.d.ts +0 -24
  194. package/dist/types/utils/schema/compatibility.d.ts +0 -30
  195. package/dist/types/utils/schema/dereference.d.ts +0 -11
  196. package/dist/types/utils/schema/draft.d.ts +0 -10
  197. package/dist/types/utils/schema/equality.d.ts +0 -4
  198. package/dist/types/utils/schema/fields.d.ts +0 -49
  199. package/dist/types/utils/schema/index.d.ts +0 -14
  200. package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
  201. package/dist/types/utils/schema/meta-validator.d.ts +0 -2
  202. package/dist/types/utils/schema/normalize.d.ts +0 -93
  203. package/dist/types/utils/schema/root-combinator.d.ts +0 -12
  204. package/dist/types/utils/schema/spill.d.ts +0 -8
  205. package/dist/types/utils/schema/stamps.d.ts +0 -25
  206. package/dist/types/utils/schema/types.d.ts +0 -4
  207. package/dist/types/utils/schema/wire.d.ts +0 -54
  208. package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
  209. package/dist/types/utils/sse-debug.d.ts +0 -10
  210. package/dist/types/utils/tool-call-healing.d.ts +0 -71
  211. package/dist/types/utils/tool-choice-capability.d.ts +0 -41
  212. package/dist/types/utils/tool-choice.d.ts +0 -50
  213. package/dist/types/utils/validation.d.ts +0 -17
  214. package/dist/types/utils.d.ts +0 -35
@@ -20,6 +20,7 @@ import type {
20
20
  } from "../types";
21
21
  import { normalizeSystemPrompts } from "../utils";
22
22
  import { AssistantMessageEventStream } from "../utils/event-stream";
23
+ import { transportFailureFacts } from "../utils/fallback-transport";
23
24
  import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
24
25
  import { resolveRetryBudget } from "../utils/retry-budget";
25
26
  // Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
@@ -130,6 +131,22 @@ function extractErrorMessage(errorText: string): string {
130
131
  return errorText;
131
132
  }
132
133
 
134
+ function createGeminiCliHttpError(response: Response, errorText: string, formatErrorMessage = true): Error {
135
+ const message = formatErrorMessage ? extractErrorMessage(errorText) : errorText;
136
+ const error = withHttpStatus(
137
+ new Error(`Cloud Code Assist API error (${response.status}): ${message}`),
138
+ response.status,
139
+ ) as Error & { code?: string; headers?: Headers };
140
+ error.headers = response.headers;
141
+ try {
142
+ const code = (JSON.parse(errorText) as { error?: { code?: unknown } }).error?.code;
143
+ if (typeof code === "string") error.code = code;
144
+ } catch {
145
+ // The response body is not JSON.
146
+ }
147
+ return error;
148
+ }
149
+
133
150
  interface GeminiCliApiKeyPayload {
134
151
  token?: unknown;
135
152
  projectId?: unknown;
@@ -380,11 +397,12 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
380
397
  );
381
398
  if (!response.ok && sentForcedToolChoice) {
382
399
  const errorText = await response.text();
383
- const error = withHttpStatus(
384
- new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`),
385
- response.status,
386
- );
387
- if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
400
+ const error = createGeminiCliHttpError(response, errorText);
401
+ if (
402
+ !options?.fallbackManaged &&
403
+ firstTokenTime === undefined &&
404
+ isForcedToolChoiceUnsupportedError(error, true)
405
+ ) {
388
406
  const beforeMark = resolveToolChoice(model, options?.toolChoice);
389
407
  markToolChoiceIncapability(model, "auto", error.message);
390
408
  stream.push({
@@ -423,10 +441,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
423
441
  }
424
442
  if (!response.ok) {
425
443
  const errorText = await response.text();
426
- throw withHttpStatus(
427
- new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`),
428
- response.status,
429
- );
444
+ throw createGeminiCliHttpError(response, errorText);
430
445
  }
431
446
  const requestUrl = response.url;
432
447
 
@@ -629,10 +644,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
629
644
 
630
645
  if (!currentResponse.ok) {
631
646
  const retryErrorText = await currentResponse.text();
632
- throw withHttpStatus(
633
- new Error(`Cloud Code Assist API error (${currentResponse.status}): ${retryErrorText}`),
634
- currentResponse.status,
635
- );
647
+ throw createGeminiCliHttpError(currentResponse, retryErrorText, false);
636
648
  }
637
649
  }
638
650
 
@@ -671,6 +683,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
671
683
  }
672
684
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
673
685
  output.errorStatus = extractHttpStatusFromError(error);
686
+ output.transportFailure = transportFailureFacts(error);
674
687
  output.errorMessage = await appendRawHttpRequestDumpFor400(
675
688
  error instanceof Error ? error.message : JSON.stringify(error),
676
689
  error,
@@ -20,6 +20,7 @@ import type {
20
20
  } from "../types";
21
21
  import { normalizeSystemPrompts, sanitizeJsonStrings } from "../utils";
22
22
  import { AssistantMessageEventStream } from "../utils/event-stream";
23
+ import { transportFailureFacts } from "../utils/fallback-transport";
23
24
  import { finalizeErrorMessage, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
24
25
  import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
25
26
  import {
@@ -868,7 +869,11 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
868
869
  new Error(`Google API error (${response.status}): ${extractGoogleErrorMessage(errorText)}`),
869
870
  response.status,
870
871
  );
871
- if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
872
+ if (
873
+ !options?.fallbackManaged &&
874
+ firstTokenTime === undefined &&
875
+ isForcedToolChoiceUnsupportedError(error, true)
876
+ ) {
872
877
  const beforeMark = resolveToolChoice(model, options?.toolChoice);
873
878
  markToolChoiceIncapability(model, "auto", error.message);
874
879
  stream.push({
@@ -938,6 +943,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
938
943
  }
939
944
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
940
945
  output.errorStatus = extractHttpStatusFromError(error);
946
+ output.transportFailure = transportFailureFacts(error);
941
947
  output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
942
948
  output.duration = Date.now() - startTime;
943
949
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
@@ -16,6 +16,7 @@ import type {
16
16
  } from "../types";
17
17
  import { normalizeSystemPrompts } from "../utils";
18
18
  import { AssistantMessageEventStream } from "../utils/event-stream";
19
+ import { transportFailureFacts } from "../utils/fallback-transport";
19
20
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
20
21
  import { parseStreamingJson } from "../utils/json-parse";
21
22
  import { resolveRetryBudget } from "../utils/retry-budget";
@@ -431,7 +432,11 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
431
432
  `HTTP ${response.status} from ${baseUrl}/api/chat: ${await response.text().catch(() => "")}`,
432
433
  );
433
434
  (error as Error & { status?: number }).status = response.status;
434
- if (firstTokenTime === undefined && isForcedToolChoiceUnsupportedError(error, true)) {
435
+ if (
436
+ firstTokenTime === undefined &&
437
+ !options.fallbackManaged &&
438
+ isForcedToolChoiceUnsupportedError(error, true)
439
+ ) {
435
440
  markToolChoiceIncapability(model, "auto", error.message);
436
441
  stream.push({
437
442
  type: "toolChoiceIncapability",
@@ -590,6 +595,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
590
595
  }
591
596
  output.stopReason = options.signal?.aborted ? "aborted" : "error";
592
597
  output.errorStatus = extractHttpStatusFromError(error);
598
+ output.transportFailure = transportFailureFacts(error);
593
599
  output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
594
600
  output.duration = Date.now() - startTime;
595
601
  if (firstTokenTime) {
@@ -14,6 +14,7 @@ import type {
14
14
  ResolvedServiceTier,
15
15
  StopReason,
16
16
  TextContent,
17
+ ThinkingContent,
17
18
  Tool,
18
19
  ToolCall,
19
20
  ToolResultMessage,
@@ -416,6 +417,29 @@ function buildUsage(message: AssistantMessage): Record<string, unknown> {
416
417
  return usage;
417
418
  }
418
419
 
420
+ function isResponsesFamilyApi(api: AssistantMessage["api"]): boolean {
421
+ return api === "openai-responses" || api === "openai-codex-responses";
422
+ }
423
+
424
+ function safeThinkingText(content: ThinkingContent, api: AssistantMessage["api"]): string | undefined {
425
+ if (isResponsesFamilyApi(api) && content.provenance === undefined) return undefined;
426
+ if (content.provenance === "raw") return undefined;
427
+ if (content.provenance === "mixed") return content.summaryText;
428
+ if (content.provenance === "summary") return content.summaryText ?? content.thinking;
429
+ return content.thinking;
430
+ }
431
+
432
+ function hasRawOrMixedThinking(partial: AssistantMessage, contentIndex: number): boolean {
433
+ const content = partial.content[contentIndex];
434
+ return content?.type === "thinking" && (content.provenance === "raw" || content.provenance === "mixed");
435
+ }
436
+
437
+ /** Responses-family reasoning is untrusted until output_item.done assigns provenance. */
438
+ function hasUnfinalizedResponsesThinking(partial: AssistantMessage, contentIndex: number): boolean {
439
+ const content = partial.content[contentIndex];
440
+ return content?.type !== "thinking" || (isResponsesFamilyApi(partial.api) && content.provenance === undefined);
441
+ }
442
+
419
443
  function flattenAssistant(message: AssistantMessage): {
420
444
  text: string;
421
445
  reasoning: string;
@@ -429,9 +453,11 @@ function flattenAssistant(message: AssistantMessage): {
429
453
  case "text":
430
454
  text += part.text;
431
455
  break;
432
- case "thinking":
433
- reasoning += part.thinking;
456
+ case "thinking": {
457
+ const thinking = safeThinkingText(part, message.api);
458
+ if (thinking !== undefined) reasoning += thinking;
434
459
  break;
460
+ }
435
461
  case "redactedThinking":
436
462
  // Opaque blob — surface verbatim on the reasoning channel so the
437
463
  // concatenation round-trips through clients that just echo it.
@@ -521,6 +547,18 @@ export function encodeStream(
521
547
  let nextToolIndex = 0;
522
548
  let hasToolCalls = false;
523
549
  let finishReason: string = "stop";
550
+ // contentIndexes that already streamed a reasoning summary delta, so a
551
+ // final-only reasoning_summary_end does not duplicate streamed summary text.
552
+ const summaryDeltaSeen = new Set<number>();
553
+ // Responses assigns reasoning provenance only at output_item.done. Keep its
554
+ // pre-classification bytes out of this public compatibility stream.
555
+ const pendingThinkingDeltas = new Map<number, string[]>();
556
+
557
+ const writeThinkingDelta = (thinking: string) => {
558
+ // DeepSeek-style / o-series reasoning channel. Clients that don't
559
+ // understand it ignore the unknown delta key.
560
+ if (thinking.length > 0) writeSse(controller, baseChunk({ reasoning_content: thinking }, null));
561
+ };
524
562
 
525
563
  try {
526
564
  // Initial role chunk.
@@ -534,14 +572,60 @@ export function encodeStream(
534
572
  }
535
573
  break;
536
574
 
537
- case "thinking_delta":
538
- // DeepSeek-style / o-series reasoning channel. Clients that don't
539
- // understand it ignore the unknown delta key.
575
+ case "thinking_delta": {
576
+ if (hasRawOrMixedThinking(event.partial, event.contentIndex)) break;
577
+ if (hasUnfinalizedResponsesThinking(event.partial, event.contentIndex)) {
578
+ const deltas = pendingThinkingDeltas.get(event.contentIndex) ?? [];
579
+ deltas.push(event.delta);
580
+ pendingThinkingDeltas.set(event.contentIndex, deltas);
581
+ break;
582
+ }
583
+ writeThinkingDelta(event.delta);
584
+ break;
585
+ }
586
+ case "thinking_start":
587
+ if (
588
+ !hasRawOrMixedThinking(event.partial, event.contentIndex) &&
589
+ hasUnfinalizedResponsesThinking(event.partial, event.contentIndex)
590
+ ) {
591
+ pendingThinkingDeltas.set(event.contentIndex, []);
592
+ }
593
+ break;
594
+ case "thinking_end": {
595
+ const pending = pendingThinkingDeltas.get(event.contentIndex);
596
+ pendingThinkingDeltas.delete(event.contentIndex);
597
+ if (
598
+ hasRawOrMixedThinking(event.partial, event.contentIndex) ||
599
+ hasUnfinalizedResponsesThinking(event.partial, event.contentIndex)
600
+ )
601
+ break;
602
+ if (pending) for (const delta of pending) writeThinkingDelta(delta);
603
+ break;
604
+ }
605
+ case "reasoning_summary_start":
606
+ // Chat format has no explicit reasoning open frame.
607
+ break;
608
+
609
+ case "reasoning_summary_delta":
610
+ // Provider-displayable summary reasoning surfaces on the same
611
+ // reasoning_content channel as raw thinking for this legacy format.
540
612
  if (event.delta.length > 0) {
613
+ // Only a non-whitespace delta counts as a delivered summary; a bare
614
+ // separator ("\n\n") must not suppress a later final-only end content.
615
+ if (event.delta.trim().length > 0) summaryDeltaSeen.add(event.contentIndex);
541
616
  writeSse(controller, baseChunk({ reasoning_content: event.delta }, null));
542
617
  }
543
618
  break;
544
619
 
620
+ case "reasoning_summary_end":
621
+ // Final-only summary: text arrives only on the end event with no prior
622
+ // deltas, so surface it now (skip when deltas already streamed to avoid
623
+ // duplicating the summary).
624
+ if (event.content.length > 0 && !summaryDeltaSeen.has(event.contentIndex)) {
625
+ writeSse(controller, baseChunk({ reasoning_content: event.content }, null));
626
+ }
627
+ break;
628
+
545
629
  case "toolcall_start": {
546
630
  hasToolCalls = true;
547
631
  const idx = nextToolIndex++;
@@ -578,6 +662,7 @@ export function encodeStream(
578
662
  }
579
663
 
580
664
  case "done":
665
+ pendingThinkingDeltas.clear();
581
666
  finishReason =
582
667
  event.reason === "toolUse"
583
668
  ? "tool_calls"
@@ -593,6 +678,7 @@ export function encodeStream(
593
678
  return;
594
679
 
595
680
  case "error": {
681
+ pendingThinkingDeltas.clear();
596
682
  const msg = event.error.errorMessage ?? "stream error";
597
683
  writeSse(controller, { error: { message: msg, type: "upstream_error" } });
598
684
  controller.close();
@@ -607,10 +693,12 @@ export function encodeStream(
607
693
  }
608
694
 
609
695
  // Stream ended without a terminal `done` (defensive). Close gracefully.
696
+ pendingThinkingDeltas.clear();
610
697
  writeSse(controller, baseChunk({}, hasToolCalls ? "tool_calls" : "stop"));
611
698
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
612
699
  controller.close();
613
700
  } catch (err) {
701
+ pendingThinkingDeltas.clear();
614
702
  const msg = err instanceof Error ? err.message : String(err);
615
703
  writeSse(controller, { error: { message: msg, type: "upstream_error" } });
616
704
  controller.close();
@@ -14,6 +14,7 @@ export type CodexRateLimits = {
14
14
  export type CodexErrorInfo = {
15
15
  message: string;
16
16
  status: number;
17
+ code?: string;
17
18
  friendlyMessage?: string;
18
19
  rateLimits?: CodexRateLimits;
19
20
  raw?: string;
@@ -24,6 +25,7 @@ export async function parseCodexError(response: Response): Promise<CodexErrorInf
24
25
  let message = raw || response.statusText || "Request failed";
25
26
  let friendlyMessage: string | undefined;
26
27
  let rateLimits: CodexRateLimits | undefined;
28
+ let code: string | undefined;
27
29
 
28
30
  try {
29
31
  const parsed = JSON.parse(raw) as { error?: Record<string, unknown> };
@@ -45,16 +47,21 @@ export async function parseCodexError(response: Response): Promise<CodexErrorInf
45
47
  ? { primary, secondary }
46
48
  : undefined;
47
49
 
48
- const code = String((err as { code?: string; type?: string }).code ?? (err as { type?: string }).type ?? "");
50
+ code =
51
+ typeof (err as { code?: unknown }).code === "string"
52
+ ? (err as { code: string }).code
53
+ : typeof (err as { type?: unknown }).type === "string"
54
+ ? (err as { type: string }).type
55
+ : undefined;
49
56
  const resetsAt = (err as { resets_at?: number }).resets_at ?? primary.resets_at ?? secondary.resets_at;
50
57
  const mins = resetsAt ? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000)) : undefined;
51
58
 
52
- if (/usage_limit_reached|usage_not_included/i.test(code)) {
59
+ if (/usage_limit_reached|usage_not_included/i.test(code ?? "")) {
53
60
  const planType = (err as { plan_type?: string }).plan_type;
54
61
  const plan = planType ? ` (${String(planType).toLowerCase()} plan)` : "";
55
62
  const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
56
63
  friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
57
- } else if (/rate_limit_exceeded/i.test(code) || response.status === 429) {
64
+ } else if (/rate_limit_exceeded/i.test(code ?? "") || response.status === 429) {
58
65
  const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
59
66
  friendlyMessage = `ChatGPT rate limit exceeded.${when}`.trim();
60
67
  }
@@ -69,6 +76,7 @@ export async function parseCodexError(response: Response): Promise<CodexErrorInf
69
76
  message,
70
77
  status: response.status,
71
78
  friendlyMessage,
79
+ code,
72
80
  rateLimits,
73
81
  raw: raw,
74
82
  };
@@ -43,10 +43,12 @@ import {
43
43
  createOpenAIResponsesHistoryPayload,
44
44
  getOpenAIResponsesHistoryItems,
45
45
  getOpenAIResponsesHistoryPayload,
46
+ neutralizeResponsesInputControlTokens,
46
47
  normalizeSystemPrompts,
47
48
  sanitizeOpenAIResponsesHistoryItemsForReplay,
48
49
  } from "../utils";
49
50
  import { AssistantMessageEventStream } from "../utils/event-stream";
51
+ import { transportFailureFacts } from "../utils/fallback-transport";
50
52
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
51
53
  import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
52
54
  import { parseStreamingJson } from "../utils/json-parse";
@@ -111,9 +113,15 @@ const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
111
113
  "invalid_request_error",
112
114
  "invalid_schema",
113
115
  "invalid_tool_schema",
116
+ // A poisoned-history rejection (`Request blocked (code=invalid_prompt)`) is a
117
+ // deterministic content fault, not a transient upstream failure: retrying the
118
+ // same request re-sends the same offending item and re-triggers the block, so
119
+ // classify it as explicitly non-retryable instead of relying on omission
120
+ // (the request-boundary sanitizer, not a provider retry, is the recovery path).
121
+ "invalid_prompt",
114
122
  ]);
115
123
  const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
116
- /invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?/i;
124
+ /invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?|request blocked[^\n]*invalid[_ -]prompt|code=invalid[_ -]prompt/i;
117
125
  const CODEX_RETRYABLE_EVENT_MESSAGE =
118
126
  /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
119
127
  const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
@@ -131,6 +139,7 @@ const CODEX_PROGRESS_EVENT_TYPES = new Set([
131
139
  "response.reasoning_summary_part.added",
132
140
  "response.reasoning_summary_text.delta",
133
141
  "response.reasoning_summary_part.done",
142
+ "response.reasoning_text.delta",
134
143
  "response.content_part.added",
135
144
  "response.output_text.delta",
136
145
  "response.refusal.delta",
@@ -153,8 +162,8 @@ function isCodexStreamProgressEvent(event: unknown): boolean {
153
162
  }
154
163
  type CodexTransport = "sse" | "websocket";
155
164
  type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
156
- type CodexOutputBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string });
157
-
165
+ type CodexThinkingBlock = ThinkingContent & { summaryBuffer: string; rawBuffer: string; summaryStarted: boolean };
166
+ type CodexOutputBlock = CodexThinkingBlock | TextContent | (ToolCall & { partialJson: string });
158
167
  export interface OpenAICodexWebSocketDebugStats {
159
168
  fullContextRequests: number;
160
169
  deltaRequests: number;
@@ -633,7 +642,7 @@ async function buildTransformedCodexRequestBody(
633
642
  ): Promise<RequestBody> {
634
643
  const params: RequestBody = {
635
644
  model: model.id,
636
- input: [...convertMessages(model, context)],
645
+ input: neutralizeResponsesInputControlTokens(convertMessages(model, context)),
637
646
  stream: true,
638
647
  prompt_cache_key: normalizeOpenAIResponsesPromptCacheKey(options?.sessionId),
639
648
  };
@@ -993,6 +1002,11 @@ function handleCodexStreamEvent(args: {
993
1002
  return firstTokenTime;
994
1003
  }
995
1004
 
1005
+ if (eventType === "response.reasoning_text.delta") {
1006
+ handleReasoningTextDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
1007
+ return firstTokenTime;
1008
+ }
1009
+
996
1010
  if (eventType === "response.content_part.added") {
997
1011
  handleContentPartAdded(runtime.currentItem, rawEvent);
998
1012
  return firstTokenTime;
@@ -1067,7 +1081,7 @@ function handleCodexStreamEvent(args: {
1067
1081
 
1068
1082
  function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
1069
1083
  if (item.type === "reasoning") {
1070
- return { type: "thinking", thinking: "" };
1084
+ return { type: "thinking", thinking: "", summaryBuffer: "", rawBuffer: "", summaryStarted: false };
1071
1085
  }
1072
1086
  if (item.type === "message") {
1073
1087
  return { type: "text", text: "" };
@@ -1118,13 +1132,18 @@ function handleReasoningSummaryTextDelta(
1118
1132
  blockIndex: () => number,
1119
1133
  ): void {
1120
1134
  if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
1135
+ if (!currentBlock.summaryStarted) {
1136
+ currentBlock.summaryStarted = true;
1137
+ stream.push({ type: "reasoning_summary_start", contentIndex: blockIndex(), partial: output });
1138
+ }
1121
1139
  currentItem.summary = currentItem.summary || [];
1122
1140
  const lastPart = currentItem.summary[currentItem.summary.length - 1];
1123
1141
  if (!lastPart) return;
1124
1142
  const delta = (rawEvent as { delta?: string }).delta || "";
1125
1143
  currentBlock.thinking += delta;
1144
+ currentBlock.summaryBuffer += delta;
1126
1145
  lastPart.text += delta;
1127
- stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta, partial: output });
1146
+ stream.push({ type: "reasoning_summary_delta", contentIndex: blockIndex(), delta, partial: output });
1128
1147
  }
1129
1148
 
1130
1149
  function handleReasoningSummaryPartDone(
@@ -1139,8 +1158,24 @@ function handleReasoningSummaryPartDone(
1139
1158
  const lastPart = currentItem.summary[currentItem.summary.length - 1];
1140
1159
  if (!lastPart) return;
1141
1160
  currentBlock.thinking += "\n\n";
1161
+ currentBlock.summaryBuffer += "\n\n";
1142
1162
  lastPart.text += "\n\n";
1143
- stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta: "\n\n", partial: output });
1163
+ stream.push({ type: "reasoning_summary_delta", contentIndex: blockIndex(), delta: "\n\n", partial: output });
1164
+ }
1165
+
1166
+ function handleReasoningTextDelta(
1167
+ currentItem: CodexEventItem | null,
1168
+ currentBlock: CodexOutputBlock | null,
1169
+ rawEvent: Record<string, unknown>,
1170
+ stream: AssistantMessageEventStream,
1171
+ output: AssistantMessage,
1172
+ blockIndex: () => number,
1173
+ ): void {
1174
+ if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
1175
+ const delta = (rawEvent as { delta?: string }).delta || "";
1176
+ currentBlock.thinking += delta;
1177
+ currentBlock.rawBuffer += delta;
1178
+ stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta, partial: output });
1144
1179
  }
1145
1180
 
1146
1181
  function handleContentPartAdded(currentItem: CodexEventItem | null, rawEvent: Record<string, unknown>): void {
@@ -1243,14 +1278,50 @@ function handleOutputItemDone(
1243
1278
  runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
1244
1279
 
1245
1280
  if (item.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
1246
- runtime.currentBlock.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
1247
- runtime.currentBlock.thinkingSignature = JSON.stringify(item);
1248
- stream.push({
1249
- type: "thinking_end",
1250
- contentIndex: blockIndex(),
1251
- content: runtime.currentBlock.thinking,
1252
- partial: output,
1253
- });
1281
+ const block = runtime.currentBlock;
1282
+ // Prefer the streamed summary buffer only when it carries real text; a
1283
+ // part.done before/without any summary_text delta leaves only separators, so
1284
+ // fall back to the canonical item.summary from output_item.done (matches the
1285
+ // shared Responses decoder).
1286
+ const bufferSummary = block.summaryBuffer ?? "";
1287
+ const itemSummary = item.summary?.map(summary => summary.text).join("\n\n") ?? "";
1288
+ const summaryText = bufferSummary.trim() ? bufferSummary : itemSummary;
1289
+ const rawText = block.rawBuffer;
1290
+ const mutable = block as { provenance?: "summary" | "raw" | "mixed"; summaryText?: string; rawText?: string };
1291
+ if (mutable.provenance === undefined) {
1292
+ if (mutable.summaryText === undefined && summaryText) mutable.summaryText = summaryText;
1293
+ if (mutable.rawText === undefined && rawText) mutable.rawText = rawText;
1294
+ mutable.provenance = summaryText && rawText ? "mixed" : summaryText ? "summary" : rawText ? "raw" : undefined;
1295
+ }
1296
+ // Finalized display string must exclude raw CoT when a summary exists (parity
1297
+ // with openai-responses-shared). Derive from STORED write-once provenance fields
1298
+ // so a later/duplicate raw-only finalization cannot overwrite a summary/mixed
1299
+ // block's safe display with raw CoT; raw-only stays raw.
1300
+ {
1301
+ const effSummary = mutable.summaryText ?? summaryText;
1302
+ const effRaw = mutable.rawText ?? rawText;
1303
+ block.thinking = mutable.provenance === "raw" ? effRaw : effSummary || effRaw;
1304
+ }
1305
+ block.thinkingSignature = JSON.stringify(item);
1306
+ delete (block as { summaryBuffer?: string }).summaryBuffer;
1307
+ delete (block as { rawBuffer?: string }).rawBuffer;
1308
+ const wasSummaryStarted = block.summaryStarted;
1309
+ delete (block as { summaryStarted?: boolean }).summaryStarted;
1310
+ if (summaryText) {
1311
+ // Emit a summary start first when none was streamed (part.added/done or
1312
+ // canonical done-item summary with no summary_text delta), so consumers that
1313
+ // open a summary on start don't receive an orphaned reasoning_summary_end.
1314
+ if (!wasSummaryStarted) {
1315
+ stream.push({ type: "reasoning_summary_start", contentIndex: blockIndex(), partial: output });
1316
+ }
1317
+ stream.push({
1318
+ type: "reasoning_summary_end",
1319
+ contentIndex: blockIndex(),
1320
+ content: summaryText,
1321
+ partial: output,
1322
+ });
1323
+ }
1324
+ stream.push({ type: "thinking_end", contentIndex: blockIndex(), content: block.thinking, partial: output });
1254
1325
  runtime.currentBlock = null;
1255
1326
  return;
1256
1327
  }
@@ -1372,6 +1443,7 @@ async function recoverCodexStreamError(
1372
1443
  runtime: CodexStreamRuntime,
1373
1444
  error: unknown,
1374
1445
  ): Promise<boolean> {
1446
+ if (context.options?.fallbackManaged) return false;
1375
1447
  if (await tryRetryWithoutForcedToolChoice(context, runtime, error)) {
1376
1448
  return true;
1377
1449
  }
@@ -1396,6 +1468,7 @@ async function tryRetryWithoutForcedToolChoice(
1396
1468
  error: unknown,
1397
1469
  ): Promise<boolean> {
1398
1470
  if (
1471
+ context.options?.fallbackManaged ||
1399
1472
  runtime.providerRetryAttempt > 0 ||
1400
1473
  context.output.content.length > 0 ||
1401
1474
  context.firstTokenTime !== undefined ||
@@ -1468,7 +1541,12 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1468
1541
  return false;
1469
1542
  }
1470
1543
  const websocketState = context.requestContext.websocketState;
1471
- if (!websocketState || runtime.transport !== "websocket" || context.options?.signal?.aborted) {
1544
+ if (
1545
+ !websocketState ||
1546
+ runtime.transport !== "websocket" ||
1547
+ context.options?.signal?.aborted ||
1548
+ context.options?.fallbackManaged
1549
+ ) {
1472
1550
  return false;
1473
1551
  }
1474
1552
 
@@ -1519,6 +1597,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
1519
1597
  if (
1520
1598
  !isCodexPreviousResponseNotFound(error) ||
1521
1599
  !websocketState ||
1600
+ context.options?.fallbackManaged ||
1522
1601
  runtime.transport !== "websocket" ||
1523
1602
  context.output.content.length > 0 ||
1524
1603
  context.options?.signal?.aborted ||
@@ -1556,7 +1635,8 @@ async function tryReplayWebsocketFailureOverSse(
1556
1635
  isCodexWebSocketRetryableStreamError(error) &&
1557
1636
  runtime.canSafelyReplayWebsocketOverSse &&
1558
1637
  !runtime.sawTerminalEvent &&
1559
- !context.options?.signal?.aborted;
1638
+ !context.options?.signal?.aborted &&
1639
+ !context.options?.fallbackManaged;
1560
1640
  if (!canReplay) return false;
1561
1641
 
1562
1642
  const state = websocketState;
@@ -1608,7 +1688,8 @@ async function tryRetryCodexProviderError(
1608
1688
  !isRetryableCodexProviderError(error) ||
1609
1689
  context.output.content.length > 0 ||
1610
1690
  runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES) ||
1611
- context.options?.signal?.aborted
1691
+ context.options?.signal?.aborted ||
1692
+ context.options?.fallbackManaged
1612
1693
  ) {
1613
1694
  return false;
1614
1695
  }
@@ -1692,6 +1773,7 @@ async function handleCodexStreamFailure(
1692
1773
  }
1693
1774
  output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
1694
1775
  output.errorStatus = extractHttpStatusFromError(error);
1776
+ output.transportFailure = transportFailureFacts(error);
1695
1777
  output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
1696
1778
  output.duration = Date.now() - context.startTime;
1697
1779
  if (context.firstTokenTime) {
@@ -1719,6 +1801,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
1719
1801
  try {
1720
1802
  initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
1721
1803
  } catch (error) {
1804
+ if (options?.fallbackManaged) throw error;
1722
1805
  initialTransport = await retryCodexInitialTransportWithoutToolChoice(
1723
1806
  model,
1724
1807
  options,
@@ -2408,6 +2491,7 @@ async function openCodexSseEventStream(
2408
2491
  const error = new Error(info.friendlyMessage || info.message);
2409
2492
  (error as { headers?: Headers; status?: number }).headers = response.headers;
2410
2493
  (error as { headers?: Headers; status?: number }).status = response.status;
2494
+ (error as { code?: string }).code = info.code;
2411
2495
  throw error;
2412
2496
  }
2413
2497
  if (!response.body) {
@@ -2635,8 +2719,11 @@ function normalizeInputMessageContent(
2635
2719
  return convertResponsesInputContent(content, model.input.includes("image")) ?? [];
2636
2720
  }
2637
2721
 
2638
- /** @internal Exported for tests. */
2639
- export { convertMessages as convertCodexResponsesMessages };
2722
+ /** @internal Exported for tests. `classifyCodexFailureEventRetryable` is the retry classification of a Codex failure event. */
2723
+ export {
2724
+ convertMessages as convertCodexResponsesMessages,
2725
+ isRetryableCodexFailureEvent as classifyCodexFailureEventRetryable,
2726
+ };
2640
2727
 
2641
2728
  /**
2642
2729
  * Whether this OpenAI code backend-backend model should get the custom-tool grammar