@oh-my-pi/pi-ai 16.3.5 → 16.3.7

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 (236) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/api-registry.d.ts +1 -1
  3. package/dist/types/auth-broker/client.d.ts +4 -2
  4. package/dist/types/auth-broker/discover.d.ts +1 -1
  5. package/dist/types/auth-broker/index.d.ts +7 -7
  6. package/dist/types/auth-broker/refresher.d.ts +1 -1
  7. package/dist/types/auth-broker/remote-store.d.ts +11 -6
  8. package/dist/types/auth-broker/server.d.ts +1 -1
  9. package/dist/types/auth-broker/snapshot-cache.d.ts +1 -1
  10. package/dist/types/auth-broker/types.d.ts +14 -2
  11. package/dist/types/auth-broker/wire-schemas.d.ts +46 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -3
  13. package/dist/types/auth-gateway/server.d.ts +3 -3
  14. package/dist/types/auth-gateway/types.d.ts +1 -1
  15. package/dist/types/auth-retry.d.ts +2 -2
  16. package/dist/types/auth-storage.d.ts +44 -6
  17. package/dist/types/dialect/anthropic.d.ts +1 -1
  18. package/dist/types/dialect/catalog.d.ts +1 -1
  19. package/dist/types/dialect/coercion.d.ts +1 -1
  20. package/dist/types/dialect/deepseek.d.ts +1 -1
  21. package/dist/types/dialect/demotion.d.ts +16 -14
  22. package/dist/types/dialect/examples.d.ts +1 -1
  23. package/dist/types/dialect/factory.d.ts +1 -1
  24. package/dist/types/dialect/gemini.d.ts +1 -1
  25. package/dist/types/dialect/gemma.d.ts +1 -1
  26. package/dist/types/dialect/glm.d.ts +1 -1
  27. package/dist/types/dialect/harmony.d.ts +1 -1
  28. package/dist/types/dialect/hermes.d.ts +1 -1
  29. package/dist/types/dialect/history.d.ts +2 -2
  30. package/dist/types/dialect/index.d.ts +11 -11
  31. package/dist/types/dialect/inventory.d.ts +1 -1
  32. package/dist/types/dialect/kimi.d.ts +1 -1
  33. package/dist/types/dialect/minimax.d.ts +1 -1
  34. package/dist/types/dialect/owned-stream.d.ts +2 -2
  35. package/dist/types/dialect/qwen3.d.ts +1 -1
  36. package/dist/types/dialect/rendering.d.ts +2 -2
  37. package/dist/types/dialect/thinking.d.ts +1 -1
  38. package/dist/types/dialect/types.d.ts +1 -1
  39. package/dist/types/dialect/xml.d.ts +1 -1
  40. package/dist/types/error/classes.d.ts +1 -1
  41. package/dist/types/error/finalize.d.ts +3 -3
  42. package/dist/types/error/flags.d.ts +1 -1
  43. package/dist/types/error/format.d.ts +1 -1
  44. package/dist/types/error/index.d.ts +13 -13
  45. package/dist/types/error/provider.d.ts +1 -1
  46. package/dist/types/error/rate-limit.d.ts +1 -1
  47. package/dist/types/index.d.ts +47 -47
  48. package/dist/types/provider-details.d.ts +1 -1
  49. package/dist/types/providers/amazon-bedrock.d.ts +1 -1
  50. package/dist/types/providers/anthropic-client.d.ts +3 -3
  51. package/dist/types/providers/anthropic-messages-server-schema.d.ts +1 -1
  52. package/dist/types/providers/anthropic-messages-server.d.ts +2 -2
  53. package/dist/types/providers/anthropic-wire.d.ts +1 -1
  54. package/dist/types/providers/anthropic.d.ts +3 -3
  55. package/dist/types/providers/aws-credentials.d.ts +2 -2
  56. package/dist/types/providers/azure-openai-responses.d.ts +2 -2
  57. package/dist/types/providers/cursor.d.ts +18 -3
  58. package/dist/types/providers/devin.d.ts +1 -1
  59. package/dist/types/providers/error-message.d.ts +1 -1
  60. package/dist/types/providers/github-copilot-headers.d.ts +1 -1
  61. package/dist/types/providers/gitlab-duo-workflow.d.ts +2 -2
  62. package/dist/types/providers/gitlab-duo.d.ts +2 -2
  63. package/dist/types/providers/google-auth.d.ts +1 -1
  64. package/dist/types/providers/google-gemini-cli.d.ts +6 -4
  65. package/dist/types/providers/google-interactions.d.ts +3 -3
  66. package/dist/types/providers/google-shared.d.ts +7 -5
  67. package/dist/types/providers/google-vertex.d.ts +2 -2
  68. package/dist/types/providers/google.d.ts +2 -2
  69. package/dist/types/providers/kimi.d.ts +3 -3
  70. package/dist/types/providers/mock.d.ts +2 -2
  71. package/dist/types/providers/ollama.d.ts +1 -1
  72. package/dist/types/providers/openai-anthropic-shim.d.ts +2 -2
  73. package/dist/types/providers/openai-chat-server-schema.d.ts +1 -1
  74. package/dist/types/providers/openai-chat-server.d.ts +2 -2
  75. package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
  76. package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
  77. package/dist/types/providers/openai-codex-responses.d.ts +3 -3
  78. package/dist/types/providers/openai-completions.d.ts +3 -3
  79. package/dist/types/providers/openai-reasoning-fallback.d.ts +1 -1
  80. package/dist/types/providers/openai-responses-server-schema.d.ts +1 -1
  81. package/dist/types/providers/openai-responses-server.d.ts +2 -2
  82. package/dist/types/providers/openai-responses.d.ts +5 -5
  83. package/dist/types/providers/openai-shared.d.ts +10 -9
  84. package/dist/types/providers/pi-native-client.d.ts +1 -1
  85. package/dist/types/providers/pi-native-server.d.ts +2 -2
  86. package/dist/types/providers/register-builtins.d.ts +3 -3
  87. package/dist/types/providers/synthetic.d.ts +3 -3
  88. package/dist/types/providers/transform-messages.d.ts +1 -1
  89. package/dist/types/providers/vision-guard.d.ts +1 -1
  90. package/dist/types/registry/alibaba-coding-plan.d.ts +1 -1
  91. package/dist/types/registry/anthropic.d.ts +1 -1
  92. package/dist/types/registry/api-key-login.d.ts +1 -1
  93. package/dist/types/registry/api-key-validation.d.ts +1 -1
  94. package/dist/types/registry/baseten.d.ts +2 -2
  95. package/dist/types/registry/cerebras.d.ts +2 -2
  96. package/dist/types/registry/cloudflare-ai-gateway.d.ts +1 -1
  97. package/dist/types/registry/coreweave.d.ts +2 -2
  98. package/dist/types/registry/cursor.d.ts +1 -1
  99. package/dist/types/registry/deepseek.d.ts +1 -1
  100. package/dist/types/registry/devin.d.ts +1 -1
  101. package/dist/types/registry/firepass.d.ts +2 -2
  102. package/dist/types/registry/fireworks.d.ts +2 -2
  103. package/dist/types/registry/github-copilot.d.ts +1 -1
  104. package/dist/types/registry/gitlab-duo-workflow.d.ts +1 -1
  105. package/dist/types/registry/gitlab-duo.d.ts +1 -1
  106. package/dist/types/registry/google-antigravity.d.ts +1 -1
  107. package/dist/types/registry/google-gemini-cli.d.ts +1 -1
  108. package/dist/types/registry/huggingface.d.ts +2 -2
  109. package/dist/types/registry/index.d.ts +4 -4
  110. package/dist/types/registry/kagi.d.ts +1 -1
  111. package/dist/types/registry/kilo.d.ts +1 -1
  112. package/dist/types/registry/kimi-code.d.ts +1 -1
  113. package/dist/types/registry/litellm.d.ts +1 -1
  114. package/dist/types/registry/llama-cpp.d.ts +1 -1
  115. package/dist/types/registry/lm-studio.d.ts +1 -1
  116. package/dist/types/registry/minimax-code-cn.d.ts +1 -1
  117. package/dist/types/registry/minimax-code.d.ts +1 -1
  118. package/dist/types/registry/moonshot.d.ts +2 -2
  119. package/dist/types/registry/nanogpt.d.ts +2 -2
  120. package/dist/types/registry/nvidia.d.ts +1 -1
  121. package/dist/types/registry/oauth/anthropic.d.ts +3 -3
  122. package/dist/types/registry/oauth/callback-server.d.ts +1 -1
  123. package/dist/types/registry/oauth/cursor.d.ts +1 -1
  124. package/dist/types/registry/oauth/devin.d.ts +1 -1
  125. package/dist/types/registry/oauth/github-copilot.d.ts +2 -2
  126. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +2 -2
  127. package/dist/types/registry/oauth/gitlab-duo.d.ts +1 -1
  128. package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
  129. package/dist/types/registry/oauth/google-gemini-cli.d.ts +1 -1
  130. package/dist/types/registry/oauth/google-oauth-shared.d.ts +2 -2
  131. package/dist/types/registry/oauth/index.d.ts +2 -2
  132. package/dist/types/registry/oauth/kimi.d.ts +1 -1
  133. package/dist/types/registry/oauth/minimax-code.d.ts +2 -2
  134. package/dist/types/registry/oauth/openai-codex.d.ts +1 -1
  135. package/dist/types/registry/oauth/opencode.d.ts +1 -1
  136. package/dist/types/registry/oauth/perplexity.d.ts +1 -1
  137. package/dist/types/registry/oauth/types.d.ts +2 -2
  138. package/dist/types/registry/oauth/wafer.d.ts +1 -1
  139. package/dist/types/registry/oauth/xai-oauth.d.ts +3 -3
  140. package/dist/types/registry/oauth/xiaomi.d.ts +1 -1
  141. package/dist/types/registry/ollama-cloud.d.ts +1 -1
  142. package/dist/types/registry/ollama.d.ts +1 -1
  143. package/dist/types/registry/openai-codex-device.d.ts +1 -1
  144. package/dist/types/registry/openai-codex.d.ts +1 -1
  145. package/dist/types/registry/opencode-go.d.ts +1 -1
  146. package/dist/types/registry/opencode-zen.d.ts +1 -1
  147. package/dist/types/registry/openrouter.d.ts +2 -2
  148. package/dist/types/registry/parallel.d.ts +1 -1
  149. package/dist/types/registry/perplexity.d.ts +2 -2
  150. package/dist/types/registry/qianfan.d.ts +2 -2
  151. package/dist/types/registry/qwen-portal.d.ts +1 -1
  152. package/dist/types/registry/registry.d.ts +69 -69
  153. package/dist/types/registry/sakana.d.ts +2 -2
  154. package/dist/types/registry/synthetic.d.ts +2 -2
  155. package/dist/types/registry/tavily.d.ts +1 -1
  156. package/dist/types/registry/together.d.ts +1 -1
  157. package/dist/types/registry/types.d.ts +1 -1
  158. package/dist/types/registry/umans.d.ts +2 -2
  159. package/dist/types/registry/venice.d.ts +2 -2
  160. package/dist/types/registry/vercel-ai-gateway.d.ts +1 -1
  161. package/dist/types/registry/vllm.d.ts +1 -1
  162. package/dist/types/registry/wafer-serverless.d.ts +1 -1
  163. package/dist/types/registry/xai-oauth.d.ts +1 -1
  164. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +1 -1
  165. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +1 -1
  166. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +1 -1
  167. package/dist/types/registry/xiaomi.d.ts +1 -1
  168. package/dist/types/registry/zai.d.ts +2 -2
  169. package/dist/types/registry/zenmux.d.ts +2 -2
  170. package/dist/types/registry/zhipu-coding-plan.d.ts +2 -2
  171. package/dist/types/stream.d.ts +6 -6
  172. package/dist/types/types.d.ts +40 -21
  173. package/dist/types/usage/claude.d.ts +1 -1
  174. package/dist/types/usage/gemini.d.ts +1 -1
  175. package/dist/types/usage/github-copilot.d.ts +1 -1
  176. package/dist/types/usage/google-antigravity.d.ts +1 -1
  177. package/dist/types/usage/kimi.d.ts +1 -1
  178. package/dist/types/usage/minimax-code.d.ts +1 -1
  179. package/dist/types/usage/ollama.d.ts +1 -1
  180. package/dist/types/usage/openai-codex-reset.d.ts +1 -1
  181. package/dist/types/usage/openai-codex.d.ts +1 -1
  182. package/dist/types/usage/opencode-go.d.ts +1 -1
  183. package/dist/types/usage/zai.d.ts +1 -1
  184. package/dist/types/usage.d.ts +1 -1
  185. package/dist/types/utils/block-symbols.d.ts +15 -0
  186. package/dist/types/utils/empty-completion-retry.d.ts +2 -2
  187. package/dist/types/utils/event-stream.d.ts +1 -1
  188. package/dist/types/utils/harmony-leak.d.ts +1 -1
  189. package/dist/types/utils/http-inspector.d.ts +19 -0
  190. package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
  191. package/dist/types/utils/openai-http.d.ts +3 -9
  192. package/dist/types/utils/provider-response.d.ts +1 -1
  193. package/dist/types/utils/proxy.d.ts +1 -1
  194. package/dist/types/utils/request-debug.d.ts +1 -1
  195. package/dist/types/utils/retry.d.ts +1 -1
  196. package/dist/types/utils/schema/equality.d.ts +1 -1
  197. package/dist/types/utils/schema/index.d.ts +15 -15
  198. package/dist/types/utils/schema/normalize.d.ts +7 -2
  199. package/dist/types/utils/schema/spill.d.ts +1 -1
  200. package/dist/types/utils/schema/wire.d.ts +1 -1
  201. package/dist/types/utils/sse-debug.d.ts +1 -1
  202. package/dist/types/utils/thinking-loop.d.ts +2 -2
  203. package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
  204. package/dist/types/utils/tool-choice.d.ts +1 -1
  205. package/dist/types/utils/validation.d.ts +1 -1
  206. package/dist/types/utils.d.ts +14 -2
  207. package/package.json +4 -4
  208. package/src/auth-broker/client.ts +41 -12
  209. package/src/auth-broker/remote-store.ts +189 -9
  210. package/src/auth-broker/server.ts +109 -7
  211. package/src/auth-broker/types.ts +22 -1
  212. package/src/auth-broker/wire-schemas.ts +22 -0
  213. package/src/auth-storage.ts +292 -22
  214. package/src/dialect/demotion.ts +18 -19
  215. package/src/error/rate-limit.ts +1 -1
  216. package/src/providers/azure-openai-responses.ts +3 -2
  217. package/src/providers/cursor.ts +36 -0
  218. package/src/providers/google-gemini-cli.ts +12 -3
  219. package/src/providers/google-shared.ts +3 -1
  220. package/src/providers/mock.ts +10 -3
  221. package/src/providers/ollama.ts +2 -2
  222. package/src/providers/openai-codex-responses.ts +47 -39
  223. package/src/providers/openai-completions.ts +48 -9
  224. package/src/providers/openai-responses.ts +37 -23
  225. package/src/providers/openai-shared.ts +145 -26
  226. package/src/providers/transform-messages.ts +63 -9
  227. package/src/registry/oauth/wafer.ts +1 -1
  228. package/src/stream.ts +6 -0
  229. package/src/types.ts +45 -3
  230. package/src/usage/claude.ts +28 -8
  231. package/src/utils/block-symbols.ts +18 -0
  232. package/src/utils/http-inspector.ts +32 -4
  233. package/src/utils/openai-http.ts +6 -9
  234. package/src/utils/retry-after.ts +12 -1
  235. package/src/utils/schema/normalize.ts +131 -0
  236. package/src/utils.ts +78 -0
@@ -1111,6 +1111,17 @@ async function handleExecServerMessage(
1111
1111
  case "grepArgs": {
1112
1112
  const args = execMsg.message.value;
1113
1113
  if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
1114
+ // Cursor's model sometimes emits `grepArgs` with an empty `pattern` and a
1115
+ // non-empty `glob`, expecting grep to list files matching the glob. Reject
1116
+ // that up front with an actionable error so the model retries with a real
1117
+ // regex or switches to `ls`/`read`, instead of the local grep tool
1118
+ // surfacing a bare "Pattern must not be empty" (issue #4574) after the
1119
+ // synthesized block has already been persisted with a placeholder pattern.
1120
+ const emptyPatternError = emptyGrepPatternRejection(args.pattern, args.glob);
1121
+ if (emptyPatternError !== null) {
1122
+ sendExecClientMessage(h2Request, execMsg, "grepResult", buildGrepErrorResult(emptyPatternError));
1123
+ return;
1124
+ }
1114
1125
  // Mirror the coding-agent bridge's arg mapping so live UI (from
1115
1126
  // `tool_execution_start`) and rebuilt transcript (from this block)
1116
1127
  // display identical args.
@@ -1835,6 +1846,31 @@ function buildGrepErrorResult(error: string) {
1835
1846
  });
1836
1847
  }
1837
1848
 
1849
+ /**
1850
+ * Reject a Cursor exec-channel `grepArgs` frame whose `pattern` is empty or
1851
+ * whitespace-only. Returns an actionable error message when the pattern is
1852
+ * unusable (with a `glob`-aware hint when the model likely meant to list
1853
+ * files), or `null` when the pattern is valid and grep should run.
1854
+ *
1855
+ * Exported for tests. Cursor's model sometimes sends `pattern=""` together
1856
+ * with a non-empty `glob`, expecting grep to enumerate matching files; the
1857
+ * downstream coding-agent `grep` tool rejects that with a bare "Pattern must
1858
+ * not be empty", which the TUI renders as `?` in the tool preview (issue
1859
+ * #4574). Handling it at the Cursor exec dispatch keeps the synthesized
1860
+ * `toolCall` block off the persisted assistant message and gives the model a
1861
+ * specific recovery hint.
1862
+ */
1863
+ export function emptyGrepPatternRejection(pattern: string | undefined, glob: string | undefined): string | null {
1864
+ if (pattern && pattern.trim().length > 0) return null;
1865
+ if (glob && glob.length > 0) {
1866
+ return (
1867
+ `grep pattern is required (received an empty pattern). To list files matching "${glob}", ` +
1868
+ `pass a non-empty regex (e.g. ".") and set path to that glob, or use the ls/read tool instead.`
1869
+ );
1870
+ }
1871
+ return "grep pattern is required (received an empty pattern).";
1872
+ }
1873
+
1838
1874
  function buildDiagnosticsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
1839
1875
  const text = toolResultToText(toolResult);
1840
1876
  if (toolResult.isError) {
@@ -263,6 +263,8 @@ export interface GoogleGeminiCliOptions extends StreamOptions {
263
263
  */
264
264
  suppress?: { level: GoogleThinkingLevel } | { budget: number };
265
265
  };
266
+ /** Request that Cloud Code Assist omit human-readable thought summaries while still allowing internal reasoning. */
267
+ hideThinkingSummary?: boolean;
266
268
  /**
267
269
  * Upstream wire model id override for collapsed effort-tier variants.
268
270
  * Serialized as `requestModelId ?? model.requestModelId ?? model.id`.
@@ -1002,8 +1004,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1002
1004
  }
1003
1005
 
1004
1006
  const streamed = await streamResponse(currentResponse);
1005
- if (streamed) {
1006
- receivedContent = true;
1007
+ if (output.stopReason !== "stop" || streamed) {
1008
+ receivedContent = streamed;
1007
1009
  break;
1008
1010
  }
1009
1011
 
@@ -1012,6 +1014,13 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
1012
1014
  }
1013
1015
  }
1014
1016
 
1017
+ if (output.stopReason === "aborted" || output.stopReason === "error") {
1018
+ throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
1019
+ provider: model.provider,
1020
+ kind: "output",
1021
+ });
1022
+ }
1023
+
1015
1024
  if (!receivedContent) {
1016
1025
  throw new AIError.ProviderResponseError("Cloud Code Assist API returned an empty response", {
1017
1026
  provider: model.provider,
@@ -1242,7 +1251,7 @@ export function buildRequest(
1242
1251
  // Thinking config
1243
1252
  if (options.thinking?.enabled && model.reasoning) {
1244
1253
  generationConfig.thinkingConfig = {
1245
- includeThoughts: true,
1254
+ includeThoughts: !options.hideThinkingSummary,
1246
1255
  };
1247
1256
  // Gemini 3 models use thinkingLevel, older models use thinkingBudget
1248
1257
  if (options.thinking.level !== undefined) {
@@ -75,6 +75,8 @@ export interface GoogleSharedStreamOptions extends StreamOptions {
75
75
  budgetTokens?: number;
76
76
  level?: GoogleThinkingLevel;
77
77
  };
78
+ /** Request that Google omit human-readable thought summaries while still allowing internal reasoning. */
79
+ hideThinkingSummary?: boolean;
78
80
  /** Gemini/Vertex serving tier (`flex`/`priority`); other values are omitted. */
79
81
  serviceTier?: ServiceTier;
80
82
  /**
@@ -859,7 +861,7 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
859
861
  }
860
862
 
861
863
  if (options.thinking?.enabled && model.reasoning) {
862
- const cfg: ThinkingConfig = { includeThoughts: true };
864
+ const cfg: ThinkingConfig = { includeThoughts: !options.hideThinkingSummary };
863
865
  if (options.thinking.level !== undefined) {
864
866
  // GoogleThinkingLevel mirrors the SDK's `ThinkingLevel` string enum values 1:1.
865
867
  cfg.thinkingLevel = options.thinking.level as ThinkingLevel;
@@ -446,10 +446,17 @@ function mergeUsage(partial?: Partial<Omit<Usage, "cost">> & { cost?: Partial<Us
446
446
  if (costProvided) {
447
447
  merged.cost = { ...base.cost, ...partial.cost } as Usage["cost"];
448
448
  }
449
- // Recompute totalTokens when not explicitly provided (canonical formula matches types.ts:
450
- // input + output + cacheRead + cacheWrite).
449
+ // Recompute totalTokens when not explicitly provided (canonical formula matches types.ts).
451
450
  if (partial.totalTokens === undefined) {
452
- merged.totalTokens = merged.input + merged.output + merged.cacheRead + merged.cacheWrite;
451
+ const orchestration = merged.orchestration;
452
+ merged.totalTokens =
453
+ merged.input +
454
+ merged.output +
455
+ merged.cacheRead +
456
+ merged.cacheWrite +
457
+ (orchestration?.input ?? 0) +
458
+ (orchestration?.output ?? 0) +
459
+ (orchestration?.cacheRead ?? 0);
453
460
  }
454
461
  // Recompute cost.total when cost components were supplied without an explicit total.
455
462
  if (costProvided && partial.cost?.total === undefined) {
@@ -23,7 +23,7 @@ import {
23
23
  getOpenAIStreamFirstEventTimeoutMs,
24
24
  getOpenAIStreamIdleTimeoutMs,
25
25
  } from "../utils/idle-iterator";
26
- import { toolWireSchema } from "../utils/schema/wire";
26
+ import { sanitizeSchemaForOllama, toolWireSchema } from "../utils/schema";
27
27
  import {
28
28
  getStreamMarkupHealingPattern,
29
29
  type HealedToolCall,
@@ -280,7 +280,7 @@ function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefin
280
280
  function: {
281
281
  name: tool.name,
282
282
  description: tool.description,
283
- parameters: toolWireSchema(tool),
283
+ parameters: sanitizeSchemaForOllama(toolWireSchema(tool)),
284
284
  },
285
285
  }));
286
286
  }
@@ -43,6 +43,8 @@ import {
43
43
  getOpenAIResponsesHistoryItems,
44
44
  getOpenAIResponsesHistoryPayload,
45
45
  normalizeSystemPrompts,
46
+ sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
47
+ sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
46
48
  } from "../utils";
47
49
  import { clearStreamingPartialJson, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols";
48
50
  import { AssistantMessageEventStream } from "../utils/event-stream";
@@ -286,7 +288,7 @@ interface CodexProviderSessionState extends ProviderSessionState {
286
288
 
287
289
  interface CodexRequestContext {
288
290
  apiKey: string;
289
- accountId: string;
291
+ accountId?: string;
290
292
  baseUrl: string;
291
293
  url: string;
292
294
  requestHeaders: Record<string, string>;
@@ -834,7 +836,7 @@ async function buildCodexRequestContext(
834
836
  throw new AIError.MissingApiKeyError(model.provider);
835
837
  }
836
838
 
837
- const accountId = getAccountId(apiKey);
839
+ const accountId = getCodexAccountId(apiKey);
838
840
  const baseUrl = model.baseUrl || CODEX_BASE_URL;
839
841
  const url = resolveCodexResponsesUrl(baseUrl);
840
842
  const promptCacheKey = normalizeOpenAIResponsesPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
@@ -853,7 +855,7 @@ async function buildCodexRequestContext(
853
855
 
854
856
  const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
855
857
  const responsesLite = shouldUseCodexResponsesLite(transformedBody, options?.responsesLite);
856
- const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, baseUrl, responsesLite);
858
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
857
859
  const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
858
860
  if (sessionKey && publicSessionKey) {
859
861
  providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
@@ -902,7 +904,7 @@ export async function buildTransformedCodexRequestBody(
902
904
  // `{"detail":"Unsupported parameter: temperature"}` etc., so we drop
903
905
  // everything from `StreamOptions` rather than forwarding any of them.
904
906
  // (#3117 — codex-rs sends none of these either.)
905
- applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
907
+ applyOpenAIServiceTier(params, options?.serviceTier, model);
906
908
  if (context.tools && context.tools.length > 0) {
907
909
  params.tools = convertOpenAICodexResponsesTools(context.tools, model);
908
910
  if (options?.toolChoice) {
@@ -1531,8 +1533,15 @@ class CodexStreamProcessor {
1531
1533
  input_tokens?: number;
1532
1534
  output_tokens?: number;
1533
1535
  total_tokens?: number;
1534
- input_tokens_details?: { cached_tokens?: number };
1535
- output_tokens_details?: { reasoning_tokens?: number };
1536
+ input_tokens_details?: {
1537
+ cached_tokens?: number;
1538
+ orchestration_input_tokens?: number;
1539
+ orchestration_input_cached_tokens?: number;
1540
+ };
1541
+ output_tokens_details?: {
1542
+ reasoning_tokens?: number;
1543
+ orchestration_output_tokens?: number;
1544
+ };
1536
1545
  };
1537
1546
  status?: string;
1538
1547
  service_tier?: ServiceTier | "default";
@@ -2055,14 +2064,14 @@ export async function prewarmOpenAICodexResponses(
2055
2064
  ): Promise<void> {
2056
2065
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
2057
2066
  if (!apiKey) return;
2058
- const accountId = getAccountId(apiKey);
2067
+ const accountId = getCodexAccountId(apiKey);
2059
2068
  const baseUrl = model.baseUrl || CODEX_BASE_URL;
2060
2069
  const url = resolveCodexResponsesUrl(baseUrl);
2061
2070
  const transportSessionId = normalizeOpenAIResponsesPromptCacheKey(options?.sessionId);
2062
2071
  const promptCacheKey = transportSessionId;
2063
2072
  const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
2064
2073
  const responsesLite = options?.responsesLite === true;
2065
- const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, baseUrl, responsesLite);
2074
+ const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
2066
2075
  const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : undefined;
2067
2076
  if (publicSessionKey && sessionKey) {
2068
2077
  providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
@@ -2095,15 +2104,17 @@ export async function prewarmOpenAICodexResponses(
2095
2104
  function getCodexWebSocketSessionKey(
2096
2105
  normalizedSessionId: string | undefined,
2097
2106
  model: Model<"openai-codex-responses">,
2098
- accountId: string,
2107
+ accountId: string | undefined,
2108
+ apiKey: string,
2099
2109
  baseUrl: string,
2100
2110
  responsesLite: boolean,
2101
2111
  ): string | undefined {
2102
2112
  if (!normalizedSessionId) return undefined;
2113
+ const credentialKey = accountId ? `account:${accountId}` : `token:${Bun.hash(apiKey).toString(36)}`;
2103
2114
  // Responses Lite is connection-scoped on the WebSocket upgrade, so lite and
2104
2115
  // non-lite turns must never share a pooled socket or append state.
2105
2116
  const liteSuffix = responsesLite ? ":lite" : "";
2106
- return `${accountId}:${baseUrl}:${model.id}:${normalizedSessionId}${liteSuffix}`;
2117
+ return `${credentialKey}:${baseUrl}:${model.id}:${normalizedSessionId}${liteSuffix}`;
2107
2118
  }
2108
2119
 
2109
2120
  function getCodexWebSocketSessionState(
@@ -2979,7 +2990,7 @@ async function getOrCreateCodexWebSocketConnection(
2979
2990
  async function openCodexSseEventStream(
2980
2991
  url: string,
2981
2992
  requestHeaders: Record<string, string> | undefined,
2982
- accountId: string,
2993
+ accountId: string | undefined,
2983
2994
  apiKey: string,
2984
2995
  sessionId: string | undefined,
2985
2996
  body: RequestBody,
@@ -3044,7 +3055,7 @@ async function openCodexSseEventStream(
3044
3055
 
3045
3056
  function createCodexHeaders(
3046
3057
  initHeaders: Record<string, string> | undefined,
3047
- accountId: string,
3058
+ accountId: string | undefined,
3048
3059
  accessToken: string,
3049
3060
  sessionId?: string,
3050
3061
  transport: CodexTransport = "sse",
@@ -3054,7 +3065,7 @@ function createCodexHeaders(
3054
3065
  const headers = new Headers(initHeaders ?? {});
3055
3066
  headers.delete("x-api-key");
3056
3067
  headers.set("Authorization", `Bearer ${accessToken}`);
3057
- headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
3068
+ if (accountId) headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
3058
3069
  const betaHeader =
3059
3070
  transport === "websocket"
3060
3071
  ? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
@@ -3129,17 +3140,6 @@ function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
3129
3140
  return `${normalized}/codex/responses`;
3130
3141
  }
3131
3142
 
3132
- function getAccountId(accessToken: string): string {
3133
- const accountId = getCodexAccountId(accessToken);
3134
- if (!accountId) {
3135
- throw new AIError.OAuthError("Failed to extract accountId from token", {
3136
- kind: "validation",
3137
- provider: "openai",
3138
- });
3139
- }
3140
- return accountId;
3141
- }
3142
-
3143
3143
  function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
3144
3144
  const messages: ResponseInput = [];
3145
3145
 
@@ -3200,32 +3200,40 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
3200
3200
  assistantMsg.api === model.api && assistantMsg.model === model.id
3201
3201
  ? getOpenAIResponsesHistoryPayload(assistantMsg.providerPayload, model.provider, assistantMsg.provider)
3202
3202
  : undefined;
3203
- const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
3203
+ const historyItems = providerPayload?.items as Array<Record<string, unknown>> | undefined;
3204
+ let suppressHiddenEmptyFallback = false;
3204
3205
  if (historyItems) {
3205
- for (const item of historyItems) {
3206
- const maybe = item as { type?: string; call_id?: string };
3207
- if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
3208
- customCallIds.add(maybe.call_id);
3206
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
3207
+ if (sanitizedHistoryItems) {
3208
+ for (const item of sanitizedHistoryItems) {
3209
+ const maybe = item as { type?: string; call_id?: string };
3210
+ if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
3211
+ customCallIds.add(maybe.call_id);
3212
+ }
3209
3213
  }
3214
+ if (providerPayload?.dt) {
3215
+ messages.push(...sanitizedHistoryItems);
3216
+ } else {
3217
+ messages.splice(0, messages.length, ...sanitizedHistoryItems);
3218
+ // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
3219
+ }
3220
+ msgIndex += 1;
3221
+ continue;
3210
3222
  }
3211
- if (providerPayload?.dt) {
3212
- messages.push(...historyItems);
3213
- } else {
3214
- messages.splice(0, messages.length, ...historyItems);
3215
- // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
3216
- }
3217
- msgIndex += 1;
3218
- continue;
3223
+ suppressHiddenEmptyFallback = true;
3219
3224
  }
3220
3225
 
3221
- const outputItems = convertResponsesAssistantMessage(
3226
+ const convertedOutputItems = convertResponsesAssistantMessage(
3222
3227
  msg as AssistantMessage,
3223
3228
  model,
3224
3229
  msgIndex,
3225
3230
  knownCallIds,
3226
- true,
3231
+ !suppressHiddenEmptyFallback,
3227
3232
  customCallIds,
3228
3233
  );
3234
+ const outputItems = suppressHiddenEmptyFallback
3235
+ ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
3236
+ : convertedOutputItems;
3229
3237
  if (outputItems.length > 0) {
3230
3238
  messages.push(...outputItems);
3231
3239
  }
@@ -29,7 +29,7 @@ import type {
29
29
  } from "../types";
30
30
  import { normalizeSystemPrompts } from "../utils";
31
31
  import { createAbortSourceTracker } from "../utils/abort";
32
- import { kStreamingLastParseLen } from "../utils/block-symbols";
32
+ import { isDemotedThinking, kStreamingLastParseLen } from "../utils/block-symbols";
33
33
  import { hasVisibleAssistantContent, withEmptyCompletionRetry } from "../utils/empty-completion-retry";
34
34
  import { AssistantMessageEventStream } from "../utils/event-stream";
35
35
  import type { RawHttpRequestDump } from "../utils/http-inspector";
@@ -684,9 +684,10 @@ const streamOpenAICompletionsOnce = (
684
684
  body: params,
685
685
  signal: requestSignal,
686
686
  fetch: options?.fetch,
687
- // With a first-event watchdog armed, transport retries must
688
- // not silently extend the deadline (old SDK `maxRetries: 0`).
689
- maxAttempts: requestTimeoutMs === undefined ? undefined : 1,
687
+ // Transient 408/429/5xx get Retry-After-aware transport retries.
688
+ // The first-event watchdog above aborts `requestSignal`, which
689
+ // bounds every attempt and backoff sleep — retries cannot
690
+ // extend the deadline.
690
691
  onSseEvent: rawSseObserver,
691
692
  });
692
693
  await notifyProviderResponse(options, response, model, requestId);
@@ -759,6 +760,16 @@ const streamOpenAICompletionsOnce = (
759
760
  type OpenAIStreamBlock = TextContent | ThinkingContent | ToolCallStreamBlock;
760
761
  const pendingToolCallBlocks: ToolCallStreamBlock[] = [];
761
762
  const toolCallBlockByIndex = new Map<number, ToolCallStreamBlock>();
763
+ // Blocks born from an unkeyed multi-entry `tool_calls` array (no `id`,
764
+ // no `index`), tracked by array offset so continuation chunks that omit
765
+ // the entry name still route back to the sibling created earlier
766
+ // instead of collapsing onto `currentBlock`.
767
+ const unkeyedBatchBlocks: (ToolCallStreamBlock | undefined)[] = [];
768
+ const clearUnkeyedBatchSlot = (block: ToolCallStreamBlock): void => {
769
+ for (let index = 0; index < unkeyedBatchBlocks.length; index++) {
770
+ if (unkeyedBatchBlocks[index] === block) unkeyedBatchBlocks[index] = undefined;
771
+ }
772
+ };
762
773
  let currentBlock: OpenAIStreamBlock | undefined;
763
774
  const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
764
775
  if (!block) return Math.max(0, output.content.length - 1);
@@ -791,6 +802,7 @@ const streamOpenAICompletionsOnce = (
791
802
  }
792
803
  const pendingIndex = pendingToolCallBlocks.indexOf(block);
793
804
  if (pendingIndex >= 0) pendingToolCallBlocks.splice(pendingIndex, 1);
805
+ clearUnkeyedBatchSlot(block);
794
806
  stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
795
807
  };
796
808
  const finishPendingToolCallBlocks = (): void => {
@@ -1092,14 +1104,27 @@ const streamOpenAICompletionsOnce = (
1092
1104
  }
1093
1105
 
1094
1106
  if (choice?.delta?.tool_calls && choice.delta.tool_calls.length > 0) {
1095
- for (const toolCall of choice.delta.tool_calls) {
1107
+ const toolCalls = choice.delta.tool_calls;
1108
+ for (let toolCallOffset = 0; toolCallOffset < toolCalls.length; toolCallOffset++) {
1109
+ const toolCall = toolCalls[toolCallOffset]!;
1096
1110
  const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
1111
+ const incomingName = toolCall.function?.name || "";
1112
+ // Multi-entry `tool_calls` arrays without `id`/`index` — either the
1113
+ // opening chunk that carries per-entry names, or a continuation whose
1114
+ // entries are argument-only. Either way, route by array offset so
1115
+ // sibling calls stay isolated.
1116
+ const unkeyedBatchedArrayEntry = toolCalls.length > 1 && streamIndex === undefined && !toolCall.id;
1097
1117
  let block = streamIndex !== undefined ? toolCallBlockByIndex.get(streamIndex) : undefined;
1098
1118
  if (!block && toolCall.id) {
1099
1119
  block = pendingToolCallBlocks.find(candidate => candidate.id === toolCall.id);
1100
1120
  }
1121
+ if (!block && unkeyedBatchedArrayEntry) {
1122
+ const offsetBlock = unkeyedBatchBlocks[toolCallOffset];
1123
+ if (offsetBlock && offsetBlock.partialArgs !== undefined) block = offsetBlock;
1124
+ }
1101
1125
  if (
1102
1126
  !block &&
1127
+ !unkeyedBatchedArrayEntry &&
1103
1128
  currentBlock?.type === "toolCall" &&
1104
1129
  (!toolCall.id || currentBlock.id === toolCall.id)
1105
1130
  ) {
@@ -1113,7 +1138,7 @@ const streamOpenAICompletionsOnce = (
1113
1138
  block = {
1114
1139
  type: "toolCall",
1115
1140
  id: toolCall.id || "",
1116
- name: toolCall.function?.name || "",
1141
+ name: incomingName,
1117
1142
  arguments: {},
1118
1143
  partialArgs: "",
1119
1144
  streamIndex,
@@ -1127,6 +1152,7 @@ const streamOpenAICompletionsOnce = (
1127
1152
  contentIndex: blockIndex(block),
1128
1153
  partial: output,
1129
1154
  });
1155
+ if (unkeyedBatchedArrayEntry) unkeyedBatchBlocks[toolCallOffset] = block;
1130
1156
  } else {
1131
1157
  // Resuming a pending call after interleaved text/thinking:
1132
1158
  // close the text/thinking block we drifted into.
@@ -1141,7 +1167,7 @@ const streamOpenAICompletionsOnce = (
1141
1167
  }
1142
1168
 
1143
1169
  if (toolCall.id) block.id = toolCall.id;
1144
- if (toolCall.function?.name) block.name = toolCall.function.name;
1170
+ if (incomingName) block.name = incomingName;
1145
1171
  let delta = "";
1146
1172
  // The OpenAI SDK types `function.arguments` as a JSON string, but MiniMax-compatible
1147
1173
  // hosts stream a fully-formed object instead. Model both shapes so the branches below
@@ -1433,7 +1459,7 @@ function buildParams(
1433
1459
  if (options?.frequencyPenalty !== undefined) {
1434
1460
  params.frequency_penalty = options.frequencyPenalty;
1435
1461
  }
1436
- applyOpenAIServiceTier(params, options?.serviceTier, model.provider);
1462
+ applyOpenAIServiceTier(params, options?.serviceTier, model);
1437
1463
 
1438
1464
  if (context.tools?.length) {
1439
1465
  const builtTools = convertTools(context.tools, initialCompat, toolStrictModeOverride);
@@ -1778,7 +1804,20 @@ export function convertMessages(
1778
1804
  // Always send assistant content as a plain string. Some OpenAI-compatible
1779
1805
  // backends mirror array-of-text-block payloads back to the model literally,
1780
1806
  // causing recursive nested content in subsequent turns.
1781
- assistantMsg.content = nonEmptyTextBlocks.map(b => b.text.toWellFormed()).join("");
1807
+ // Join ordinary adjacent text blocks with no separator so bridge
1808
+ // stitching, imported transcripts, and streaming chunks keep their
1809
+ // original byte sequence. Demoted-thinking blocks (kDemotedThinking,
1810
+ // synthesized by transformMessages) are the one exception: bare
1811
+ // Anthropic-dialect reasoning would otherwise glue onto the first word
1812
+ // of the visible answer. Insert a paragraph break after them — only
1813
+ // when another block actually follows, so a trailing demoted block
1814
+ // never ships trailing whitespace.
1815
+ assistantMsg.content = nonEmptyTextBlocks
1816
+ .map((b, i) => {
1817
+ const text = b.text.toWellFormed();
1818
+ return isDemotedThinking(b) && i < nonEmptyTextBlocks.length - 1 ? `${text}\n` : text;
1819
+ })
1820
+ .join("");
1782
1821
  }
1783
1822
 
1784
1823
  // Handle thinking blocks
@@ -20,7 +20,7 @@ import {
20
20
  createOpenAIResponsesHistoryPayload,
21
21
  normalizeSystemPrompts,
22
22
  resolveCacheRetention,
23
- sanitizeOpenAIResponsesHistoryItemsForReplay,
23
+ sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
24
24
  } from "../utils";
25
25
  import { createAbortSourceTracker } from "../utils/abort";
26
26
  import { withEmptyCompletionRetry } from "../utils/empty-completion-retry";
@@ -487,9 +487,9 @@ const streamOpenAIResponsesOnce = (
487
487
  body: requestParams,
488
488
  signal: requestSignal,
489
489
  fetch: options?.fetch,
490
- // With a first-event watchdog armed, transport retries must
491
- // not silently extend the caller's deadline.
492
- maxAttempts: requestTimeoutMs !== undefined ? 1 : undefined,
490
+ // Transient 408/429/5xx get Retry-After-aware transport
491
+ // retries; the first-event watchdog aborts `requestSignal`,
492
+ // so retries cannot extend the caller's deadline.
493
493
  onSseEvent: rawSseObserver,
494
494
  });
495
495
  // Disarm the first-event watchdog as soon as headers arrive — a slow
@@ -687,22 +687,33 @@ const streamOpenAIResponsesOnce = (
687
687
  }
688
688
 
689
689
  output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
690
- if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
691
- if (chainState) {
692
- chainState.lastParams = structuredCloneJSON(activeParams);
693
- if (output.responseId) {
694
- chainState.lastResponseId = output.responseId;
695
- chainState.lastResponseItems = sanitizeOpenAIResponsesHistoryItemsForReplay(
696
- structuredCloneJSON(nativeOutputItems),
697
- );
698
- chainState.canAppend = true;
699
- // Only a successful CHAINED completion clears the stale counter — a
700
- // full-context success must not mask categorical rejection.
701
- if (sentPreviousResponseId) chainState.staleFailures = 0;
702
- } else {
703
- // Without a response id the append baseline cannot be trusted.
704
- chainState.canAppend = false;
690
+ const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
691
+ structuredCloneJSON(nativeOutputItems),
692
+ );
693
+ if (replayableResponseItems) {
694
+ if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
695
+ if (chainState) {
696
+ chainState.lastParams = structuredCloneJSON(activeParams);
697
+ if (output.responseId) {
698
+ chainState.lastResponseId = output.responseId;
699
+ chainState.lastResponseItems = replayableResponseItems;
700
+ chainState.canAppend = true;
701
+ // Only a successful CHAINED completion clears the stale counter — a
702
+ // full-context success must not mask categorical rejection.
703
+ if (sentPreviousResponseId) chainState.staleFailures = 0;
704
+ } else {
705
+ // Without a response id the append baseline cannot be trusted.
706
+ chainState.canAppend = false;
707
+ }
705
708
  }
709
+ } else if (chainState) {
710
+ // Hidden-empty / fully sanitized successes cannot be used as an append
711
+ // baseline, but `lastParams` still records the successful wire controls
712
+ // without re-enabling `previous_response_id` chaining.
713
+ chainState.canAppend = false;
714
+ chainState.lastParams = structuredCloneJSON(activeParams);
715
+ chainState.lastResponseId = undefined;
716
+ chainState.lastResponseItems = undefined;
706
717
  }
707
718
 
708
719
  output.duration = performance.now() - startTime;
@@ -854,7 +865,7 @@ export function buildParams(
854
865
  if (context.tools) {
855
866
  const disableStrictTools =
856
867
  disableStrictToolsOverride || isStrictToolsDisabledForScope(providerSessionState, strictToolsScope);
857
- const strictMode = !disableStrictTools && model.compat.supportsStrictMode;
868
+ const strictMode = !disableStrictTools && model.compat.supportsStrictMode !== false;
858
869
  params.tools = convertTools(context.tools, strictMode, model);
859
870
  strictToolsApplied = params.tools.some(t => (t as { strict?: boolean }).strict === true);
860
871
  if (options?.toolChoice) {
@@ -989,9 +1000,12 @@ export function convertTools(
989
1000
  parameters,
990
1001
  // `strict: false` and an omitted `strict` are NOT equivalent for every
991
1002
  // OpenAI-compat backend — some over-fill optional args when the flag is
992
- // absent (#4336). Preserve the author's explicit `false` only while the
993
- // Responses strict field is enabled; compatibility disables and
994
- // strict-schema fallback retries rely on uniformly absent flags.
1003
+ // absent (#4336). Preserve the author's explicit `false` unless the
1004
+ // provider is explicitly known not to understand the field
1005
+ // (`supportsStrictMode: false`) or the strict-schema fallback is
1006
+ // active — both paths rely on a uniformly absent wire flag. Mirrors the
1007
+ // `supportsStrictMode !== false` gate used by openai-completions
1008
+ // (#4527).
995
1009
  ...(effectiveStrict
996
1010
  ? { strict: true }
997
1011
  : !NO_STRICT && strictMode && tool.strict === false