@nodetool-ai/runtime 0.7.0-rc.23 → 0.8.1

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 (665) hide show
  1. package/README.md +29 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/agent-memory.d.ts +118 -0
  4. package/dist/agent-memory.d.ts.map +1 -0
  5. package/dist/agent-memory.js +186 -0
  6. package/dist/agent-memory.js.map +1 -0
  7. package/dist/async-local-storage.d.ts +20 -0
  8. package/dist/async-local-storage.d.ts.map +1 -0
  9. package/dist/async-local-storage.js +28 -0
  10. package/dist/async-local-storage.js.map +1 -0
  11. package/dist/blender-executor.d.ts +93 -0
  12. package/dist/blender-executor.d.ts.map +1 -0
  13. package/dist/blender-executor.js +294 -0
  14. package/dist/blender-executor.js.map +1 -0
  15. package/dist/comfy-executor.d.ts +23 -1
  16. package/dist/comfy-executor.d.ts.map +1 -1
  17. package/dist/comfy-executor.js +234 -55
  18. package/dist/comfy-executor.js.map +1 -1
  19. package/dist/context-packer.d.ts +1 -1
  20. package/dist/context-packer.d.ts.map +1 -1
  21. package/dist/context-packer.js +23 -25
  22. package/dist/context-packer.js.map +1 -1
  23. package/dist/context.d.ts +954 -127
  24. package/dist/context.d.ts.map +1 -1
  25. package/dist/context.js +2424 -513
  26. package/dist/context.js.map +1 -1
  27. package/dist/cost-reconciler.d.ts +32 -0
  28. package/dist/cost-reconciler.d.ts.map +1 -0
  29. package/dist/cost-reconciler.js +21 -0
  30. package/dist/cost-reconciler.js.map +1 -0
  31. package/dist/entities.d.ts +33 -0
  32. package/dist/entities.d.ts.map +1 -0
  33. package/dist/entities.js +43 -0
  34. package/dist/entities.js.map +1 -0
  35. package/dist/external-media-fetch.d.ts +29 -0
  36. package/dist/external-media-fetch.d.ts.map +1 -0
  37. package/dist/external-media-fetch.js +38 -0
  38. package/dist/external-media-fetch.js.map +1 -0
  39. package/dist/generation-receipt.d.ts +86 -0
  40. package/dist/generation-receipt.d.ts.map +1 -0
  41. package/dist/generation-receipt.js +137 -0
  42. package/dist/generation-receipt.js.map +1 -0
  43. package/dist/generation-registry.d.ts +67 -0
  44. package/dist/generation-registry.d.ts.map +1 -0
  45. package/dist/generation-registry.js +126 -0
  46. package/dist/generation-registry.js.map +1 -0
  47. package/dist/google/client.d.ts +196 -0
  48. package/dist/google/client.d.ts.map +1 -0
  49. package/dist/google/client.js +396 -0
  50. package/dist/google/client.js.map +1 -0
  51. package/dist/google/index.d.ts +3 -0
  52. package/dist/google/index.d.ts.map +1 -0
  53. package/dist/google/index.js +3 -0
  54. package/dist/google/index.js.map +1 -0
  55. package/dist/google/token.d.ts +15 -0
  56. package/dist/google/token.d.ts.map +1 -0
  57. package/dist/google/token.js +24 -0
  58. package/dist/google/token.js.map +1 -0
  59. package/dist/host-binaries.d.ts +106 -0
  60. package/dist/host-binaries.d.ts.map +1 -0
  61. package/dist/host-binaries.js +344 -0
  62. package/dist/host-binaries.js.map +1 -0
  63. package/dist/image-codec.d.ts +74 -0
  64. package/dist/image-codec.d.ts.map +1 -0
  65. package/dist/image-codec.js +273 -0
  66. package/dist/image-codec.js.map +1 -0
  67. package/dist/index.d.ts +56 -6
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +43 -4
  70. package/dist/index.js.map +1 -1
  71. package/dist/invocation-account.d.ts +43 -0
  72. package/dist/invocation-account.d.ts.map +1 -0
  73. package/dist/invocation-account.js +60 -0
  74. package/dist/invocation-account.js.map +1 -0
  75. package/dist/media-ref-bytes.d.ts +19 -0
  76. package/dist/media-ref-bytes.d.ts.map +1 -0
  77. package/dist/media-ref-bytes.js +181 -0
  78. package/dist/media-ref-bytes.js.map +1 -0
  79. package/dist/node-executor.d.ts +113 -2
  80. package/dist/node-executor.d.ts.map +1 -1
  81. package/dist/permission-gate.d.ts +152 -0
  82. package/dist/permission-gate.d.ts.map +1 -0
  83. package/dist/permission-gate.js +87 -0
  84. package/dist/permission-gate.js.map +1 -0
  85. package/dist/prefixed-storage-adapter.d.ts +33 -0
  86. package/dist/prefixed-storage-adapter.d.ts.map +1 -0
  87. package/dist/prefixed-storage-adapter.js +70 -0
  88. package/dist/prefixed-storage-adapter.js.map +1 -0
  89. package/dist/prompt-asset-refs.d.ts +222 -0
  90. package/dist/prompt-asset-refs.d.ts.map +1 -0
  91. package/dist/prompt-asset-refs.js +751 -0
  92. package/dist/prompt-asset-refs.js.map +1 -0
  93. package/dist/provider-cache.d.ts +3 -0
  94. package/dist/provider-cache.d.ts.map +1 -0
  95. package/dist/provider-cache.js +8 -0
  96. package/dist/provider-cache.js.map +1 -0
  97. package/dist/providers/aki-provider.d.ts +33 -10
  98. package/dist/providers/aki-provider.d.ts.map +1 -1
  99. package/dist/providers/aki-provider.js +84 -77
  100. package/dist/providers/aki-provider.js.map +1 -1
  101. package/dist/providers/alibaba-provider.d.ts +39 -0
  102. package/dist/providers/alibaba-provider.d.ts.map +1 -0
  103. package/dist/providers/alibaba-provider.js +86 -0
  104. package/dist/providers/alibaba-provider.js.map +1 -0
  105. package/dist/providers/anthropic-provider.d.ts +52 -3
  106. package/dist/providers/anthropic-provider.d.ts.map +1 -1
  107. package/dist/providers/anthropic-provider.js +1020 -338
  108. package/dist/providers/anthropic-provider.js.map +1 -1
  109. package/dist/providers/atlascloud-provider.d.ts +101 -0
  110. package/dist/providers/atlascloud-provider.d.ts.map +1 -0
  111. package/dist/providers/atlascloud-provider.js +1009 -0
  112. package/dist/providers/atlascloud-provider.js.map +1 -0
  113. package/dist/providers/atlascloud-transport.d.ts +100 -0
  114. package/dist/providers/atlascloud-transport.d.ts.map +1 -0
  115. package/dist/providers/atlascloud-transport.js +364 -0
  116. package/dist/providers/atlascloud-transport.js.map +1 -0
  117. package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
  118. package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
  119. package/dist/providers/atlascloud-webhook-registry.js +104 -0
  120. package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
  121. package/dist/providers/audio-mime.d.ts +11 -0
  122. package/dist/providers/audio-mime.d.ts.map +1 -0
  123. package/dist/providers/audio-mime.js +46 -0
  124. package/dist/providers/audio-mime.js.map +1 -0
  125. package/dist/providers/base-provider.d.ts +481 -24
  126. package/dist/providers/base-provider.d.ts.map +1 -1
  127. package/dist/providers/base-provider.js +1259 -42
  128. package/dist/providers/base-provider.js.map +1 -1
  129. package/dist/providers/cassette-provider.d.ts +217 -0
  130. package/dist/providers/cassette-provider.d.ts.map +1 -0
  131. package/dist/providers/cassette-provider.js +502 -0
  132. package/dist/providers/cassette-provider.js.map +1 -0
  133. package/dist/providers/cerebras-provider.d.ts +6 -12
  134. package/dist/providers/cerebras-provider.d.ts.map +1 -1
  135. package/dist/providers/cerebras-provider.js +8 -33
  136. package/dist/providers/cerebras-provider.js.map +1 -1
  137. package/dist/providers/claude-agent-provider.d.ts +131 -68
  138. package/dist/providers/claude-agent-provider.d.ts.map +1 -1
  139. package/dist/providers/claude-agent-provider.js +1095 -515
  140. package/dist/providers/claude-agent-provider.js.map +1 -1
  141. package/dist/providers/codex-provider.d.ts +82 -0
  142. package/dist/providers/codex-provider.d.ts.map +1 -0
  143. package/dist/providers/codex-provider.js +492 -0
  144. package/dist/providers/codex-provider.js.map +1 -0
  145. package/dist/providers/cohere-provider.d.ts +5 -2
  146. package/dist/providers/cohere-provider.d.ts.map +1 -1
  147. package/dist/providers/cohere-provider.js +9 -2
  148. package/dist/providers/cohere-provider.js.map +1 -1
  149. package/dist/providers/comfy-cloud-provider.d.ts +30 -0
  150. package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
  151. package/dist/providers/comfy-cloud-provider.js +42 -0
  152. package/dist/providers/comfy-cloud-provider.js.map +1 -0
  153. package/dist/providers/content-filter.d.ts +46 -0
  154. package/dist/providers/content-filter.d.ts.map +1 -0
  155. package/dist/providers/content-filter.js +92 -0
  156. package/dist/providers/content-filter.js.map +1 -0
  157. package/dist/providers/context-exceeded.d.ts +37 -0
  158. package/dist/providers/context-exceeded.d.ts.map +1 -0
  159. package/dist/providers/context-exceeded.js +114 -0
  160. package/dist/providers/context-exceeded.js.map +1 -0
  161. package/dist/providers/context-window.d.ts +44 -0
  162. package/dist/providers/context-window.d.ts.map +1 -0
  163. package/dist/providers/context-window.js +145 -0
  164. package/dist/providers/context-window.js.map +1 -0
  165. package/dist/providers/contract/index.d.ts +4 -0
  166. package/dist/providers/contract/index.d.ts.map +1 -0
  167. package/dist/providers/contract/index.js +4 -0
  168. package/dist/providers/contract/index.js.map +1 -0
  169. package/dist/providers/contract/probe-manifest.d.ts +75 -0
  170. package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
  171. package/dist/providers/contract/probe-manifest.js +379 -0
  172. package/dist/providers/contract/probe-manifest.js.map +1 -0
  173. package/dist/providers/contract/probe-runner.d.ts +57 -0
  174. package/dist/providers/contract/probe-runner.d.ts.map +1 -0
  175. package/dist/providers/contract/probe-runner.js +180 -0
  176. package/dist/providers/contract/probe-runner.js.map +1 -0
  177. package/dist/providers/contract/redact.d.ts +29 -0
  178. package/dist/providers/contract/redact.d.ts.map +1 -0
  179. package/dist/providers/contract/redact.js +113 -0
  180. package/dist/providers/contract/redact.js.map +1 -0
  181. package/dist/providers/core-tools.d.ts +74 -0
  182. package/dist/providers/core-tools.d.ts.map +1 -0
  183. package/dist/providers/core-tools.js +127 -0
  184. package/dist/providers/core-tools.js.map +1 -0
  185. package/dist/providers/cost-calculator.d.ts +69 -50
  186. package/dist/providers/cost-calculator.d.ts.map +1 -1
  187. package/dist/providers/cost-calculator.js +207 -397
  188. package/dist/providers/cost-calculator.js.map +1 -1
  189. package/dist/providers/credential-check.d.ts +45 -0
  190. package/dist/providers/credential-check.d.ts.map +1 -0
  191. package/dist/providers/credential-check.js +159 -0
  192. package/dist/providers/credential-check.js.map +1 -0
  193. package/dist/providers/custom-model-kinds.d.ts +33 -0
  194. package/dist/providers/custom-model-kinds.d.ts.map +1 -0
  195. package/dist/providers/custom-model-kinds.js +96 -0
  196. package/dist/providers/custom-model-kinds.js.map +1 -0
  197. package/dist/providers/custom-openai-provider.d.ts +81 -0
  198. package/dist/providers/custom-openai-provider.d.ts.map +1 -0
  199. package/dist/providers/custom-openai-provider.js +182 -0
  200. package/dist/providers/custom-openai-provider.js.map +1 -0
  201. package/dist/providers/custom-provider-registry.d.ts +19 -0
  202. package/dist/providers/custom-provider-registry.d.ts.map +1 -0
  203. package/dist/providers/custom-provider-registry.js +52 -0
  204. package/dist/providers/custom-provider-registry.js.map +1 -0
  205. package/dist/providers/deepseek-provider.d.ts +8 -14
  206. package/dist/providers/deepseek-provider.d.ts.map +1 -1
  207. package/dist/providers/deepseek-provider.js +6 -35
  208. package/dist/providers/deepseek-provider.js.map +1 -1
  209. package/dist/providers/defaults.d.ts +12 -0
  210. package/dist/providers/defaults.d.ts.map +1 -0
  211. package/dist/providers/defaults.js +12 -0
  212. package/dist/providers/defaults.js.map +1 -0
  213. package/dist/providers/elevenlabs-provider.d.ts +49 -0
  214. package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
  215. package/dist/providers/elevenlabs-provider.js +142 -0
  216. package/dist/providers/elevenlabs-provider.js.map +1 -0
  217. package/dist/providers/entity-references.d.ts +31 -0
  218. package/dist/providers/entity-references.d.ts.map +1 -0
  219. package/dist/providers/entity-references.js +113 -0
  220. package/dist/providers/entity-references.js.map +1 -0
  221. package/dist/providers/evolink-provider.d.ts +38 -0
  222. package/dist/providers/evolink-provider.d.ts.map +1 -0
  223. package/dist/providers/evolink-provider.js +304 -0
  224. package/dist/providers/evolink-provider.js.map +1 -0
  225. package/dist/providers/fake-provider.d.ts +72 -1
  226. package/dist/providers/fake-provider.d.ts.map +1 -1
  227. package/dist/providers/fake-provider.js +203 -1
  228. package/dist/providers/fake-provider.js.map +1 -1
  229. package/dist/providers/fal-generations.d.ts +106 -0
  230. package/dist/providers/fal-generations.d.ts.map +1 -0
  231. package/dist/providers/fal-generations.js +286 -0
  232. package/dist/providers/fal-generations.js.map +1 -0
  233. package/dist/providers/fal-provider.d.ts +171 -7
  234. package/dist/providers/fal-provider.d.ts.map +1 -1
  235. package/dist/providers/fal-provider.js +1369 -64
  236. package/dist/providers/fal-provider.js.map +1 -1
  237. package/dist/providers/fal-queue.d.ts +47 -0
  238. package/dist/providers/fal-queue.d.ts.map +1 -0
  239. package/dist/providers/fal-queue.js +231 -0
  240. package/dist/providers/fal-queue.js.map +1 -0
  241. package/dist/providers/gemini-provider.d.ts +148 -8
  242. package/dist/providers/gemini-provider.d.ts.map +1 -1
  243. package/dist/providers/gemini-provider.js +1500 -316
  244. package/dist/providers/gemini-provider.js.map +1 -1
  245. package/dist/providers/gmi-provider.d.ts +21 -0
  246. package/dist/providers/gmi-provider.d.ts.map +1 -0
  247. package/dist/providers/gmi-provider.js +34 -0
  248. package/dist/providers/gmi-provider.js.map +1 -0
  249. package/dist/providers/gpt-image-size.d.ts +3 -0
  250. package/dist/providers/gpt-image-size.d.ts.map +1 -0
  251. package/dist/providers/gpt-image-size.js +37 -0
  252. package/dist/providers/gpt-image-size.js.map +1 -0
  253. package/dist/providers/groq-provider.d.ts +9 -12
  254. package/dist/providers/groq-provider.d.ts.map +1 -1
  255. package/dist/providers/groq-provider.js +19 -33
  256. package/dist/providers/groq-provider.js.map +1 -1
  257. package/dist/providers/groq-request.d.ts +38 -0
  258. package/dist/providers/groq-request.d.ts.map +1 -0
  259. package/dist/providers/groq-request.js +138 -0
  260. package/dist/providers/groq-request.js.map +1 -0
  261. package/dist/providers/higgsfield-provider.d.ts +30 -0
  262. package/dist/providers/higgsfield-provider.d.ts.map +1 -0
  263. package/dist/providers/higgsfield-provider.js +131 -0
  264. package/dist/providers/higgsfield-provider.js.map +1 -0
  265. package/dist/providers/higgsfield-transport.d.ts +106 -0
  266. package/dist/providers/higgsfield-transport.d.ts.map +1 -0
  267. package/dist/providers/higgsfield-transport.js +182 -0
  268. package/dist/providers/higgsfield-transport.js.map +1 -0
  269. package/dist/providers/http-transport.d.ts +74 -0
  270. package/dist/providers/http-transport.d.ts.map +1 -0
  271. package/dist/providers/http-transport.js +152 -0
  272. package/dist/providers/http-transport.js.map +1 -0
  273. package/dist/providers/huggingface-provider.d.ts +71 -3
  274. package/dist/providers/huggingface-provider.d.ts.map +1 -1
  275. package/dist/providers/huggingface-provider.js +230 -34
  276. package/dist/providers/huggingface-provider.js.map +1 -1
  277. package/dist/providers/image-mime.d.ts +39 -0
  278. package/dist/providers/image-mime.d.ts.map +1 -0
  279. package/dist/providers/image-mime.js +111 -0
  280. package/dist/providers/image-mime.js.map +1 -0
  281. package/dist/providers/image-ref.d.ts +21 -0
  282. package/dist/providers/image-ref.d.ts.map +1 -0
  283. package/dist/providers/image-ref.js +44 -0
  284. package/dist/providers/image-ref.js.map +1 -0
  285. package/dist/providers/index.d.ts +84 -8
  286. package/dist/providers/index.d.ts.map +1 -1
  287. package/dist/providers/index.js +239 -56
  288. package/dist/providers/index.js.map +1 -1
  289. package/dist/providers/jina-provider.d.ts +10 -2
  290. package/dist/providers/jina-provider.d.ts.map +1 -1
  291. package/dist/providers/jina-provider.js +16 -7
  292. package/dist/providers/jina-provider.js.map +1 -1
  293. package/dist/providers/kie-provider.d.ts +170 -9
  294. package/dist/providers/kie-provider.d.ts.map +1 -1
  295. package/dist/providers/kie-provider.js +1176 -228
  296. package/dist/providers/kie-provider.js.map +1 -1
  297. package/dist/providers/kie-webhook-registry.d.ts +28 -0
  298. package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
  299. package/dist/providers/kie-webhook-registry.js +91 -0
  300. package/dist/providers/kie-webhook-registry.js.map +1 -0
  301. package/dist/providers/llama-provider.d.ts +21 -37
  302. package/dist/providers/llama-provider.d.ts.map +1 -1
  303. package/dist/providers/llama-provider.js +43 -354
  304. package/dist/providers/llama-provider.js.map +1 -1
  305. package/dist/providers/lmstudio-provider.d.ts +4 -7
  306. package/dist/providers/lmstudio-provider.d.ts.map +1 -1
  307. package/dist/providers/lmstudio-provider.js +24 -15
  308. package/dist/providers/lmstudio-provider.js.map +1 -1
  309. package/dist/providers/manifest-models.d.ts +341 -1
  310. package/dist/providers/manifest-models.d.ts.map +1 -1
  311. package/dist/providers/manifest-models.js +1106 -22
  312. package/dist/providers/manifest-models.js.map +1 -1
  313. package/dist/providers/media-mime.d.ts +14 -0
  314. package/dist/providers/media-mime.d.ts.map +1 -0
  315. package/dist/providers/media-mime.js +35 -0
  316. package/dist/providers/media-mime.js.map +1 -0
  317. package/dist/providers/meshy-provider.d.ts +14 -1
  318. package/dist/providers/meshy-provider.d.ts.map +1 -1
  319. package/dist/providers/meshy-provider.js +76 -46
  320. package/dist/providers/meshy-provider.js.map +1 -1
  321. package/dist/providers/meta-provider.d.ts +22 -0
  322. package/dist/providers/meta-provider.d.ts.map +1 -0
  323. package/dist/providers/meta-provider.js +54 -0
  324. package/dist/providers/meta-provider.js.map +1 -0
  325. package/dist/providers/minimax-provider.d.ts +25 -18
  326. package/dist/providers/minimax-provider.d.ts.map +1 -1
  327. package/dist/providers/minimax-provider.js +211 -72
  328. package/dist/providers/minimax-provider.js.map +1 -1
  329. package/dist/providers/mistral-provider.d.ts +6 -12
  330. package/dist/providers/mistral-provider.d.ts.map +1 -1
  331. package/dist/providers/mistral-provider.js +10 -34
  332. package/dist/providers/mistral-provider.js.map +1 -1
  333. package/dist/providers/moonshot-provider.d.ts +11 -15
  334. package/dist/providers/moonshot-provider.d.ts.map +1 -1
  335. package/dist/providers/moonshot-provider.js +33 -23
  336. package/dist/providers/moonshot-provider.js.map +1 -1
  337. package/dist/providers/node-llama-cpp-provider.d.ts +98 -0
  338. package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
  339. package/dist/providers/node-llama-cpp-provider.js +479 -0
  340. package/dist/providers/node-llama-cpp-provider.js.map +1 -0
  341. package/dist/providers/nodetool-provider.d.ts +49 -0
  342. package/dist/providers/nodetool-provider.d.ts.map +1 -0
  343. package/dist/providers/nodetool-provider.js +224 -0
  344. package/dist/providers/nodetool-provider.js.map +1 -0
  345. package/dist/providers/oauth/browser-launcher.d.ts +23 -0
  346. package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
  347. package/dist/providers/oauth/browser-launcher.js +45 -0
  348. package/dist/providers/oauth/browser-launcher.js.map +1 -0
  349. package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
  350. package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
  351. package/dist/providers/oauth/claude-code-credentials.js +133 -0
  352. package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
  353. package/dist/providers/oauth/claude-code-login.d.ts +124 -0
  354. package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
  355. package/dist/providers/oauth/claude-code-login.js +230 -0
  356. package/dist/providers/oauth/claude-code-login.js.map +1 -0
  357. package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
  358. package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
  359. package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
  360. package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
  361. package/dist/providers/oauth/errors.d.ts +54 -0
  362. package/dist/providers/oauth/errors.d.ts.map +1 -0
  363. package/dist/providers/oauth/errors.js +79 -0
  364. package/dist/providers/oauth/errors.js.map +1 -0
  365. package/dist/providers/oauth/index.d.ts +105 -0
  366. package/dist/providers/oauth/index.d.ts.map +1 -0
  367. package/dist/providers/oauth/index.js +167 -0
  368. package/dist/providers/oauth/index.js.map +1 -0
  369. package/dist/providers/oauth/jwt-claims.d.ts +23 -0
  370. package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
  371. package/dist/providers/oauth/jwt-claims.js +67 -0
  372. package/dist/providers/oauth/jwt-claims.js.map +1 -0
  373. package/dist/providers/oauth/local-callback-server.d.ts +60 -0
  374. package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
  375. package/dist/providers/oauth/local-callback-server.js +148 -0
  376. package/dist/providers/oauth/local-callback-server.js.map +1 -0
  377. package/dist/providers/oauth/oauth-client.d.ts +55 -0
  378. package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
  379. package/dist/providers/oauth/oauth-client.js +161 -0
  380. package/dist/providers/oauth/oauth-client.js.map +1 -0
  381. package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
  382. package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
  383. package/dist/providers/oauth/openai-oauth-provider.js +265 -0
  384. package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
  385. package/dist/providers/oauth/pkce-helper.d.ts +43 -0
  386. package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
  387. package/dist/providers/oauth/pkce-helper.js +57 -0
  388. package/dist/providers/oauth/pkce-helper.js.map +1 -0
  389. package/dist/providers/oauth/redaction.d.ts +22 -0
  390. package/dist/providers/oauth/redaction.d.ts.map +1 -0
  391. package/dist/providers/oauth/redaction.js +59 -0
  392. package/dist/providers/oauth/redaction.js.map +1 -0
  393. package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
  394. package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
  395. package/dist/providers/oauth/secure-credential-store.js +73 -0
  396. package/dist/providers/oauth/secure-credential-store.js.map +1 -0
  397. package/dist/providers/oauth/token-store.d.ts +53 -0
  398. package/dist/providers/oauth/token-store.d.ts.map +1 -0
  399. package/dist/providers/oauth/token-store.js +78 -0
  400. package/dist/providers/oauth/token-store.js.map +1 -0
  401. package/dist/providers/oauth/types.d.ts +64 -0
  402. package/dist/providers/oauth/types.d.ts.map +1 -0
  403. package/dist/providers/oauth/types.js +12 -0
  404. package/dist/providers/oauth/types.js.map +1 -0
  405. package/dist/providers/object-tracking.d.ts +47 -0
  406. package/dist/providers/object-tracking.d.ts.map +1 -0
  407. package/dist/providers/object-tracking.js +46 -0
  408. package/dist/providers/object-tracking.js.map +1 -0
  409. package/dist/providers/offline-model-index.d.ts +20 -0
  410. package/dist/providers/offline-model-index.d.ts.map +1 -0
  411. package/dist/providers/offline-model-index.js +112 -0
  412. package/dist/providers/offline-model-index.js.map +1 -0
  413. package/dist/providers/ollama-provider.d.ts +28 -1
  414. package/dist/providers/ollama-provider.d.ts.map +1 -1
  415. package/dist/providers/ollama-provider.js +193 -43
  416. package/dist/providers/ollama-provider.js.map +1 -1
  417. package/dist/providers/openai-compat/client.d.ts +57 -0
  418. package/dist/providers/openai-compat/client.d.ts.map +1 -0
  419. package/dist/providers/openai-compat/client.js +157 -0
  420. package/dist/providers/openai-compat/client.js.map +1 -0
  421. package/dist/providers/openai-compat/decode.d.ts +36 -0
  422. package/dist/providers/openai-compat/decode.d.ts.map +1 -0
  423. package/dist/providers/openai-compat/decode.js +55 -0
  424. package/dist/providers/openai-compat/decode.js.map +1 -0
  425. package/dist/providers/openai-compat/errors.d.ts +54 -0
  426. package/dist/providers/openai-compat/errors.d.ts.map +1 -0
  427. package/dist/providers/openai-compat/errors.js +144 -0
  428. package/dist/providers/openai-compat/errors.js.map +1 -0
  429. package/dist/providers/openai-compat/index.d.ts +8 -0
  430. package/dist/providers/openai-compat/index.d.ts.map +1 -0
  431. package/dist/providers/openai-compat/index.js +5 -0
  432. package/dist/providers/openai-compat/index.js.map +1 -0
  433. package/dist/providers/openai-compat/sse.d.ts +14 -0
  434. package/dist/providers/openai-compat/sse.d.ts.map +1 -0
  435. package/dist/providers/openai-compat/sse.js +69 -0
  436. package/dist/providers/openai-compat/sse.js.map +1 -0
  437. package/dist/providers/openai-compat/types.d.ts +75 -0
  438. package/dist/providers/openai-compat/types.d.ts.map +1 -0
  439. package/dist/providers/openai-compat/types.js +9 -0
  440. package/dist/providers/openai-compat/types.js.map +1 -0
  441. package/dist/providers/openai-compat-provider.d.ts +90 -0
  442. package/dist/providers/openai-compat-provider.d.ts.map +1 -0
  443. package/dist/providers/openai-compat-provider.js +282 -0
  444. package/dist/providers/openai-compat-provider.js.map +1 -0
  445. package/dist/providers/openai-provider.d.ts +89 -26
  446. package/dist/providers/openai-provider.d.ts.map +1 -1
  447. package/dist/providers/openai-provider.js +990 -105
  448. package/dist/providers/openai-provider.js.map +1 -1
  449. package/dist/providers/openrouter-provider.d.ts +6 -12
  450. package/dist/providers/openrouter-provider.d.ts.map +1 -1
  451. package/dist/providers/openrouter-provider.js +24 -53
  452. package/dist/providers/openrouter-provider.js.map +1 -1
  453. package/dist/providers/provider-call-failure.d.ts +39 -0
  454. package/dist/providers/provider-call-failure.d.ts.map +1 -0
  455. package/dist/providers/provider-call-failure.js +132 -0
  456. package/dist/providers/provider-call-failure.js.map +1 -0
  457. package/dist/providers/provider-error.d.ts +59 -0
  458. package/dist/providers/provider-error.d.ts.map +1 -0
  459. package/dist/providers/provider-error.js +165 -0
  460. package/dist/providers/provider-error.js.map +1 -0
  461. package/dist/providers/provider-generations.d.ts +99 -0
  462. package/dist/providers/provider-generations.d.ts.map +1 -0
  463. package/dist/providers/provider-generations.js +71 -0
  464. package/dist/providers/provider-generations.js.map +1 -0
  465. package/dist/providers/provider-queue.d.ts +62 -0
  466. package/dist/providers/provider-queue.d.ts.map +1 -0
  467. package/dist/providers/provider-queue.js +28 -0
  468. package/dist/providers/provider-queue.js.map +1 -0
  469. package/dist/providers/provider-registry.d.ts +33 -14
  470. package/dist/providers/provider-registry.d.ts.map +1 -1
  471. package/dist/providers/provider-registry.js +85 -68
  472. package/dist/providers/provider-registry.js.map +1 -1
  473. package/dist/providers/provider-request-log.d.ts +53 -0
  474. package/dist/providers/provider-request-log.d.ts.map +1 -0
  475. package/dist/providers/provider-request-log.js +157 -0
  476. package/dist/providers/provider-request-log.js.map +1 -0
  477. package/dist/providers/provider-session.d.ts +3 -0
  478. package/dist/providers/provider-session.d.ts.map +1 -0
  479. package/dist/providers/provider-session.js +10 -0
  480. package/dist/providers/provider-session.js.map +1 -0
  481. package/dist/providers/provider-transport.d.ts +11 -0
  482. package/dist/providers/provider-transport.d.ts.map +1 -0
  483. package/dist/providers/provider-transport.js +11 -0
  484. package/dist/providers/provider-transport.js.map +1 -0
  485. package/dist/providers/python-provider.d.ts +20 -12
  486. package/dist/providers/python-provider.d.ts.map +1 -1
  487. package/dist/providers/python-provider.js +185 -25
  488. package/dist/providers/python-provider.js.map +1 -1
  489. package/dist/providers/replicate-provider.d.ts +89 -7
  490. package/dist/providers/replicate-provider.d.ts.map +1 -1
  491. package/dist/providers/replicate-provider.js +801 -65
  492. package/dist/providers/replicate-provider.js.map +1 -1
  493. package/dist/providers/replicate-retry.d.ts +30 -0
  494. package/dist/providers/replicate-retry.d.ts.map +1 -0
  495. package/dist/providers/replicate-retry.js +97 -0
  496. package/dist/providers/replicate-retry.js.map +1 -0
  497. package/dist/providers/responses-api.d.ts +27 -0
  498. package/dist/providers/responses-api.d.ts.map +1 -0
  499. package/dist/providers/responses-api.js +297 -0
  500. package/dist/providers/responses-api.js.map +1 -0
  501. package/dist/providers/reve-provider.d.ts +45 -0
  502. package/dist/providers/reve-provider.d.ts.map +1 -0
  503. package/dist/providers/reve-provider.js +159 -0
  504. package/dist/providers/reve-provider.js.map +1 -0
  505. package/dist/providers/rodin-provider.d.ts +23 -1
  506. package/dist/providers/rodin-provider.d.ts.map +1 -1
  507. package/dist/providers/rodin-provider.js +89 -51
  508. package/dist/providers/rodin-provider.js.map +1 -1
  509. package/dist/providers/safe-url.d.ts +70 -0
  510. package/dist/providers/safe-url.d.ts.map +1 -0
  511. package/dist/providers/safe-url.js +277 -0
  512. package/dist/providers/safe-url.js.map +1 -0
  513. package/dist/providers/scripted-provider.d.ts +9 -0
  514. package/dist/providers/scripted-provider.d.ts.map +1 -1
  515. package/dist/providers/scripted-provider.js +86 -10
  516. package/dist/providers/scripted-provider.js.map +1 -1
  517. package/dist/providers/structured-output.d.ts +42 -0
  518. package/dist/providers/structured-output.d.ts.map +1 -0
  519. package/dist/providers/structured-output.js +83 -0
  520. package/dist/providers/structured-output.js.map +1 -0
  521. package/dist/providers/together-provider.d.ts +19 -16
  522. package/dist/providers/together-provider.d.ts.map +1 -1
  523. package/dist/providers/together-provider.js +103 -225
  524. package/dist/providers/together-provider.js.map +1 -1
  525. package/dist/providers/topaz-provider.d.ts +44 -0
  526. package/dist/providers/topaz-provider.d.ts.map +1 -0
  527. package/dist/providers/topaz-provider.js +239 -0
  528. package/dist/providers/topaz-provider.js.map +1 -0
  529. package/dist/providers/types.d.ts +603 -4
  530. package/dist/providers/types.d.ts.map +1 -1
  531. package/dist/providers/types.js +81 -1
  532. package/dist/providers/types.js.map +1 -1
  533. package/dist/providers/useapi-provider.d.ts +27 -0
  534. package/dist/providers/useapi-provider.d.ts.map +1 -0
  535. package/dist/providers/useapi-provider.js +266 -0
  536. package/dist/providers/useapi-provider.js.map +1 -0
  537. package/dist/providers/video-duration.d.ts +6 -0
  538. package/dist/providers/video-duration.d.ts.map +1 -0
  539. package/dist/providers/video-duration.js +63 -0
  540. package/dist/providers/video-duration.js.map +1 -0
  541. package/dist/providers/video-frame-fallback.d.ts +39 -0
  542. package/dist/providers/video-frame-fallback.d.ts.map +1 -0
  543. package/dist/providers/video-frame-fallback.js +155 -0
  544. package/dist/providers/video-frame-fallback.js.map +1 -0
  545. package/dist/providers/video-frames.d.ts +61 -0
  546. package/dist/providers/video-frames.d.ts.map +1 -0
  547. package/dist/providers/video-frames.js +208 -0
  548. package/dist/providers/video-frames.js.map +1 -0
  549. package/dist/providers/video-mime.d.ts +14 -0
  550. package/dist/providers/video-mime.d.ts.map +1 -0
  551. package/dist/providers/video-mime.js +44 -0
  552. package/dist/providers/video-mime.js.map +1 -0
  553. package/dist/providers/video-to-audio.d.ts +34 -0
  554. package/dist/providers/video-to-audio.d.ts.map +1 -0
  555. package/dist/providers/video-to-audio.js +62 -0
  556. package/dist/providers/video-to-audio.js.map +1 -0
  557. package/dist/providers/vllm-provider.d.ts +22 -8
  558. package/dist/providers/vllm-provider.d.ts.map +1 -1
  559. package/dist/providers/vllm-provider.js +48 -23
  560. package/dist/providers/vllm-provider.js.map +1 -1
  561. package/dist/providers/voyage-provider.d.ts +10 -2
  562. package/dist/providers/voyage-provider.d.ts.map +1 -1
  563. package/dist/providers/voyage-provider.js +16 -7
  564. package/dist/providers/voyage-provider.js.map +1 -1
  565. package/dist/providers/xai-provider.d.ts +54 -13
  566. package/dist/providers/xai-provider.d.ts.map +1 -1
  567. package/dist/providers/xai-provider.js +299 -22
  568. package/dist/providers/xai-provider.js.map +1 -1
  569. package/dist/python-bridge-base.d.ts +358 -0
  570. package/dist/python-bridge-base.d.ts.map +1 -0
  571. package/dist/python-bridge-base.js +1557 -0
  572. package/dist/python-bridge-base.js.map +1 -0
  573. package/dist/python-bridge-codec.d.ts +28 -0
  574. package/dist/python-bridge-codec.d.ts.map +1 -0
  575. package/dist/python-bridge-codec.js +39 -0
  576. package/dist/python-bridge-codec.js.map +1 -0
  577. package/dist/python-bridge-factory.d.ts +52 -0
  578. package/dist/python-bridge-factory.d.ts.map +1 -0
  579. package/dist/python-bridge-factory.js +69 -0
  580. package/dist/python-bridge-factory.js.map +1 -0
  581. package/dist/python-bridge-framing.d.ts +68 -0
  582. package/dist/python-bridge-framing.d.ts.map +1 -0
  583. package/dist/python-bridge-framing.js +91 -0
  584. package/dist/python-bridge-framing.js.map +1 -0
  585. package/dist/python-bridge-types.d.ts +545 -2
  586. package/dist/python-bridge-types.d.ts.map +1 -1
  587. package/dist/python-bridge-types.js +49 -1
  588. package/dist/python-bridge-types.js.map +1 -1
  589. package/dist/python-graph-resolver.d.ts +57 -0
  590. package/dist/python-graph-resolver.d.ts.map +1 -0
  591. package/dist/python-graph-resolver.js +79 -0
  592. package/dist/python-graph-resolver.js.map +1 -0
  593. package/dist/python-node-executor.d.ts +36 -4
  594. package/dist/python-node-executor.d.ts.map +1 -1
  595. package/dist/python-node-executor.js +177 -86
  596. package/dist/python-node-executor.js.map +1 -1
  597. package/dist/python-stdio-bridge.d.ts +18 -37
  598. package/dist/python-stdio-bridge.d.ts.map +1 -1
  599. package/dist/python-stdio-bridge.js +148 -475
  600. package/dist/python-stdio-bridge.js.map +1 -1
  601. package/dist/python-websocket-bridge.d.ts +140 -0
  602. package/dist/python-websocket-bridge.d.ts.map +1 -0
  603. package/dist/python-websocket-bridge.js +561 -0
  604. package/dist/python-websocket-bridge.js.map +1 -0
  605. package/dist/python-worker-stderr.d.ts +4 -0
  606. package/dist/python-worker-stderr.d.ts.map +1 -0
  607. package/dist/python-worker-stderr.js +24 -0
  608. package/dist/python-worker-stderr.js.map +1 -0
  609. package/dist/recommended-models.d.ts +2 -2
  610. package/dist/recommended-models.d.ts.map +1 -1
  611. package/dist/recommended-models.js +24 -4
  612. package/dist/recommended-models.js.map +1 -1
  613. package/dist/recoverable-node-error.d.ts +31 -0
  614. package/dist/recoverable-node-error.d.ts.map +1 -0
  615. package/dist/recoverable-node-error.js +37 -0
  616. package/dist/recoverable-node-error.js.map +1 -0
  617. package/dist/redact-params.d.ts +13 -0
  618. package/dist/redact-params.d.ts.map +1 -0
  619. package/dist/redact-params.js +57 -0
  620. package/dist/redact-params.js.map +1 -0
  621. package/dist/sandbox-module-catalog.d.ts +43 -0
  622. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  623. package/dist/sandbox-module-catalog.js +25 -0
  624. package/dist/sandbox-module-catalog.js.map +1 -0
  625. package/dist/storage-workspace.d.ts +59 -0
  626. package/dist/storage-workspace.d.ts.map +1 -0
  627. package/dist/storage-workspace.js +426 -0
  628. package/dist/storage-workspace.js.map +1 -0
  629. package/dist/swappable-python-bridge.d.ts +83 -0
  630. package/dist/swappable-python-bridge.d.ts.map +1 -0
  631. package/dist/swappable-python-bridge.js +246 -0
  632. package/dist/swappable-python-bridge.js.map +1 -0
  633. package/dist/telemetry.d.ts +7 -0
  634. package/dist/telemetry.d.ts.map +1 -1
  635. package/dist/telemetry.js +23 -0
  636. package/dist/telemetry.js.map +1 -1
  637. package/dist/testing.d.ts +81 -0
  638. package/dist/testing.d.ts.map +1 -0
  639. package/dist/testing.js +130 -0
  640. package/dist/testing.js.map +1 -0
  641. package/dist/token-counter.d.ts +30 -0
  642. package/dist/token-counter.d.ts.map +1 -0
  643. package/dist/token-counter.js +156 -0
  644. package/dist/token-counter.js.map +1 -0
  645. package/dist/trace-exporters.d.ts +10 -0
  646. package/dist/trace-exporters.d.ts.map +1 -1
  647. package/dist/trace-exporters.js +59 -20
  648. package/dist/trace-exporters.js.map +1 -1
  649. package/dist/tracing-helpers.d.ts +45 -4
  650. package/dist/tracing-helpers.d.ts.map +1 -1
  651. package/dist/tracing-helpers.js +62 -19
  652. package/dist/tracing-helpers.js.map +1 -1
  653. package/dist/turn-budget.d.ts +278 -0
  654. package/dist/turn-budget.d.ts.map +1 -0
  655. package/dist/turn-budget.js +365 -0
  656. package/dist/turn-budget.js.map +1 -0
  657. package/dist/variable-channel.d.ts +48 -0
  658. package/dist/variable-channel.d.ts.map +1 -0
  659. package/dist/variable-channel.js +110 -0
  660. package/dist/variable-channel.js.map +1 -0
  661. package/dist/workspace.d.ts +119 -0
  662. package/dist/workspace.d.ts.map +1 -0
  663. package/dist/workspace.js +112 -0
  664. package/dist/workspace.js.map +1 -0
  665. package/package.json +107 -19
@@ -1,7 +1,38 @@
1
1
  import OpenAI, { toFile } from "openai";
2
- import { createLogger } from "@nodetool-ai/config";
3
- import { BaseProvider } from "./base-provider.js";
2
+ import { createLogger, importHidden } from "@nodetool-ai/config";
3
+ import { BaseProvider, budgetStopItem, resolveTurnBudget, splitToolResultImages } from "./base-provider.js";
4
+ import { openAIContextExceeded } from "./context-exceeded.js";
5
+ import { hashSystemPrompt } from "./provider-session.js";
6
+ import { sniffAudioMime } from "./audio-mime.js";
7
+ /** Extension for each mime `sniffAudioMime` can return; anything else is mp3. */
8
+ const AUDIO_MIME_EXT = {
9
+ "audio/wav": "wav",
10
+ "audio/ogg": "ogg",
11
+ "audio/flac": "flac",
12
+ "audio/mpeg": "mp3"
13
+ };
14
+ import { extractResponsesImages, extractResponsesText, extractResponsesToolCalls, messagesToResponsesInput, responseToolChoice, responseTools, responseUsage, streamResponsesEvents } from "./responses-api.js";
15
+ import { PROVIDER_IDS, isCallable, isNumber, isRecord, isString } from "@nodetool-ai/protocol";
16
+ import { isProviderMessageEvent, isProviderSessionUpdate, IMAGE_GENERATION_TOOL_NAME, PROVIDER_STOP_ABORTED, WEB_SEARCH_TOOL_NAME } from "./types.js";
17
+ async function loadSharp() {
18
+ const mod = await importHidden("sharp");
19
+ if (!mod)
20
+ throw new Error("sharp requires Node");
21
+ return "default" in mod ? mod.default : mod;
22
+ }
4
23
  const log = createLogger("nodetool.runtime.providers.openai");
24
+ import { snapToGptImage2Size } from "./gpt-image-size.js";
25
+ function openAIImageUsage(response, quality) {
26
+ return {
27
+ imageCount: response.data?.length ?? 1,
28
+ imageQuality: quality ?? undefined,
29
+ inputTokens: response.usage?.input_tokens,
30
+ outputTokens: response.usage?.output_tokens,
31
+ inputTextTokens: response.usage?.input_tokens_details?.text_tokens,
32
+ inputImageTokens: response.usage?.input_tokens_details?.image_tokens,
33
+ outputImageTokens: response.usage?.output_tokens_details?.image_tokens
34
+ };
35
+ }
5
36
  function asUint8Array(data) {
6
37
  if (data instanceof Uint8Array) {
7
38
  return data;
@@ -9,7 +40,7 @@ function asUint8Array(data) {
9
40
  if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
10
41
  return new Uint8Array(data);
11
42
  }
12
- if (typeof data === "string") {
43
+ if (isString(data)) {
13
44
  return Uint8Array.from(Buffer.from(data, "base64"));
14
45
  }
15
46
  if (data instanceof ArrayBuffer) {
@@ -18,8 +49,8 @@ function asUint8Array(data) {
18
49
  throw new Error("Unsupported binary payload format");
19
50
  }
20
51
  function toInt16Samples(bytes) {
21
- const aligned = bytes.length % 2 === 0 ? bytes : bytes.slice(0, bytes.length - 1);
22
- return new Int16Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 2);
52
+ const copy = Uint8Array.from(bytes);
53
+ return new Int16Array(copy.buffer);
23
54
  }
24
55
  function parseDataUri(uri) {
25
56
  const idx = uri.indexOf(",");
@@ -43,9 +74,15 @@ function makeDataUri(mime, data) {
43
74
  return `data:${mime};base64,${b64}`;
44
75
  }
45
76
  /**
46
- * Custom JSON replacer that handles objects with toJSON() methods
47
- * and other non-serializable types. Mirrors Python's _default_serializer.
77
+ * OpenAI's Chat Completions `input_audio` block accepts only `"wav"` or `"mp3"`
78
+ * as its `format` — a bare token, not a mime type. Resolve it from the declared
79
+ * mime when present, otherwise sniff the leading magic bytes. Anything that
80
+ * isn't WAV is sent as `"mp3"` (the only other format OpenAI decodes).
48
81
  */
82
+ function openAiAudioFormat(mime, bytes) {
83
+ const resolved = mime && mime !== "application/octet-stream" ? mime : sniffAudioMime(bytes);
84
+ return /wave?$/i.test(resolved) ? "wav" : "mp3";
85
+ }
49
86
  function defaultSerializer(_key, value) {
50
87
  if (value === null || value === undefined)
51
88
  return value;
@@ -59,11 +96,118 @@ function defaultSerializer(_key, value) {
59
96
  return [...value];
60
97
  if (typeof value === "object" &&
61
98
  "toJSON" in value &&
62
- typeof value.toJSON === "function") {
99
+ isCallable(value.toJSON)) {
100
+ // SAFETY: `toJSON` is the JSON.stringify protocol hook — it exists on the
101
+ // value and, by that contract, produces a JSON-representable result.
63
102
  return value.toJSON();
64
103
  }
65
104
  return value;
66
105
  }
106
+ const RESPONSE_WEB_SEARCH_TOOL = {
107
+ type: "web_search"
108
+ };
109
+ const RESPONSE_IMAGE_GENERATION_TOOL = {
110
+ type: "image_generation"
111
+ };
112
+ const RESPONSE_HOSTED_TOOL_CHOICES = {
113
+ [WEB_SEARCH_TOOL_NAME]: "web_search",
114
+ [IMAGE_GENERATION_TOOL_NAME]: "image_generation"
115
+ };
116
+ const SORA_VIDEO_DURATIONS = [4, 8, 12, 16, 20];
117
+ export const OPENAI_FALLBACK_MODELS = [
118
+ "gpt-6-astra",
119
+ "gpt-5.6",
120
+ "gpt-5.6-sol",
121
+ "gpt-5.6-terra",
122
+ "gpt-5.6-luna",
123
+ "gpt-5.5",
124
+ "gpt-5.5-pro",
125
+ "gpt-5.4",
126
+ "gpt-5.4-pro",
127
+ "gpt-5.4-mini",
128
+ "gpt-5.4-nano",
129
+ "gpt-5",
130
+ "gpt-5-mini",
131
+ "gpt-5-nano"
132
+ ].map((id) => ({
133
+ id,
134
+ name: id,
135
+ provider: PROVIDER_IDS.OPENAI
136
+ }));
137
+ /**
138
+ * OpenAI language models we support: the gpt-5 family, served through the
139
+ * Responses API. Everything before gpt-5 (gpt-4o, gpt-4.1, the o-series, …) is
140
+ * retired. OpenAI-compatible subclasses set their own provider id and never
141
+ * reach this predicate, so they keep their own model support on Chat Completions.
142
+ */
143
+ function isOpenAIResponsesModel(model) {
144
+ const id = model.toLowerCase();
145
+ // Audio / realtime / transcription variants stay on Chat Completions — the
146
+ // Responses API doesn't serve the `modalities: ["text","audio"]` path.
147
+ if (/audio|realtime|transcribe/.test(id))
148
+ return false;
149
+ return /^gpt-5(?:[.-]|$)/.test(id);
150
+ }
151
+ /**
152
+ * Decode a `GET /v1/models` body into the language models it announces.
153
+ *
154
+ * Split out of {@link OpenAIProvider.getAvailableLanguageModels} so the live
155
+ * contract probe and its raw-response fixtures parse a real production
156
+ * response the same way the provider does. Strict on the envelope: a body
157
+ * without a `data` array throws, and the caller decides what to fall back to.
158
+ */
159
+ export function decodeOpenAIModelList(payload, options) {
160
+ if (!isRecord(payload)) {
161
+ throw new Error("OpenAI model list is not an object");
162
+ }
163
+ const rows = payload.data;
164
+ if (!Array.isArray(rows)) {
165
+ throw new Error("OpenAI model list has no `data` array");
166
+ }
167
+ return rows
168
+ .filter(isRecord)
169
+ .filter((row) => isString(row.id) && row.id.length > 0)
170
+ .filter((row) => !options.onlyResponsesModels || isOpenAIResponsesModel(row.id))
171
+ .map((row) => ({
172
+ id: row.id,
173
+ name: row.id,
174
+ provider: options.provider
175
+ }));
176
+ }
177
+ function responsesSystemPrompt(messages) {
178
+ return messages
179
+ .filter((m) => m.role === "system")
180
+ .map((m) => (isString(m.content) ? m.content : ""))
181
+ .filter(Boolean)
182
+ .join("\n\n");
183
+ }
184
+ /**
185
+ * A native image_generation result surfaced by the stream parser as an image
186
+ * chunk. Returns the content block to attach to the assistant message, or
187
+ * null for any other stream item. The multi-megabyte base64 blob must be
188
+ * absorbed here, never yielded onward as a chunk.
189
+ */
190
+ function imageContentFromChunk(item) {
191
+ if (!isRecord(item) ||
192
+ item.type !== "chunk" ||
193
+ item.content_type !== "image" ||
194
+ !isString(item.content)) {
195
+ return null;
196
+ }
197
+ const metadata = isRecord(item.content_metadata) ? item.content_metadata : {};
198
+ const mimeType = isString(metadata.mimeType)
199
+ ? metadata.mimeType
200
+ : "image/png";
201
+ return { type: "image_url", image: { data: item.content, mimeType } };
202
+ }
203
+ /** Text block (when non-empty) followed by generated-image blocks. */
204
+ function assistantContentWithImages(text, images) {
205
+ const blocks = [];
206
+ if (text)
207
+ blocks.push({ type: "text", text });
208
+ blocks.push(...images);
209
+ return blocks;
210
+ }
67
211
  export class OpenAIProvider extends BaseProvider {
68
212
  static requiredSecrets() {
69
213
  return ["OPENAI_API_KEY"];
@@ -73,7 +217,7 @@ export class OpenAIProvider extends BaseProvider {
73
217
  _clientFactory;
74
218
  _fetch;
75
219
  constructor(secrets, options = {}) {
76
- super("openai");
220
+ super(options.providerId ?? "openai");
77
221
  const apiKey = secrets.OPENAI_API_KEY;
78
222
  if (!apiKey) {
79
223
  throw new Error("OPENAI_API_KEY is required");
@@ -93,46 +237,136 @@ export class OpenAIProvider extends BaseProvider {
93
237
  }
94
238
  return this._client;
95
239
  }
240
+ /**
241
+ * True when this instance should serve `model` through the Responses API.
242
+ * Gated on the real `openai` provider so OpenAI-compatible subclasses (which
243
+ * pass their own provider id) stay on Chat Completions.
244
+ */
245
+ usesResponsesApi(model) {
246
+ return (this.provider === PROVIDER_IDS.OPENAI && isOpenAIResponsesModel(model));
247
+ }
248
+ get supportsNativeWebSearch() {
249
+ return this.provider === PROVIDER_IDS.OPENAI;
250
+ }
251
+ get supportsNativeImageGeneration() {
252
+ return this.provider === PROVIDER_IDS.OPENAI;
253
+ }
96
254
  async hasToolSupport(model) {
255
+ if (this.usesResponsesApi(model))
256
+ return true;
97
257
  return !(model.startsWith("o1") || model.startsWith("o3"));
98
258
  }
259
+ /**
260
+ * Whether this instance actually serves OpenAI's own model catalog.
261
+ *
262
+ * The media and embedding lists below are hardcoded OpenAI models tagged
263
+ * `provider: "openai"`. Subclasses inherit OpenAI's *chat dialect*, not its
264
+ * lineup — a Groq or vLLM instance offering `gpt-image-2` or `whisper-1` is
265
+ * always wrong, and picking one sends the request to a host that has no such
266
+ * endpoint. Subclasses that do serve media (Together, MiniMax, GMI, xAI, …)
267
+ * override these methods with their own catalog; everyone else correctly
268
+ * reports none.
269
+ */
270
+ servesOpenAICatalog() {
271
+ return this.provider === PROVIDER_IDS.OPENAI;
272
+ }
99
273
  async getAvailableLanguageModels() {
274
+ // The `openai` provider only serves the supported (gpt-5+) models; retire
275
+ // everything older. Subclasses (their own provider id) keep their full list.
276
+ const isOpenAI = this.provider === PROVIDER_IDS.OPENAI;
100
277
  const response = await this._fetch("https://api.openai.com/v1/models", {
101
278
  headers: {
102
279
  Authorization: `Bearer ${this.apiKey}`
103
280
  }
104
281
  });
105
282
  if (!response.ok) {
106
- return [];
283
+ return isOpenAI ? OPENAI_FALLBACK_MODELS : [];
107
284
  }
108
- const payload = (await response.json());
109
- const rows = payload.data ?? [];
110
- return rows
111
- .filter((row) => typeof row.id === "string" && row.id.length > 0)
112
- .map((row) => ({
113
- id: row.id,
114
- name: row.id,
115
- provider: "openai"
116
- }));
285
+ let models;
286
+ try {
287
+ models = decodeOpenAIModelList(await response.json(), {
288
+ provider: this.provider,
289
+ onlyResponsesModels: isOpenAI
290
+ });
291
+ }
292
+ catch {
293
+ // A body OpenAI's own decoder rejects is the same situation as an
294
+ // unreachable listing: fall back rather than fail model discovery.
295
+ return isOpenAI ? OPENAI_FALLBACK_MODELS : [];
296
+ }
297
+ if (models.length === 0 && isOpenAI) {
298
+ return OPENAI_FALLBACK_MODELS;
299
+ }
300
+ return models;
117
301
  }
118
302
  async getAvailableTTSModels() {
303
+ if (!this.servesOpenAICatalog())
304
+ return [];
305
+ const legacyVoices = [
306
+ "alloy",
307
+ "ash",
308
+ "coral",
309
+ "echo",
310
+ "fable",
311
+ "onyx",
312
+ "nova",
313
+ "sage",
314
+ "shimmer"
315
+ ];
119
316
  return [
317
+ {
318
+ id: "gpt-4o-mini-tts",
319
+ name: "GPT-4o Mini TTS (Deprecated)",
320
+ provider: "openai",
321
+ voices: [
322
+ "alloy",
323
+ "ash",
324
+ "ballad",
325
+ "coral",
326
+ "echo",
327
+ "fable",
328
+ "nova",
329
+ "onyx",
330
+ "sage",
331
+ "shimmer",
332
+ "verse",
333
+ "marin",
334
+ "cedar"
335
+ ]
336
+ },
120
337
  {
121
338
  id: "tts-1",
122
339
  name: "TTS 1",
123
340
  provider: "openai",
124
- voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
341
+ voices: legacyVoices
125
342
  },
126
343
  {
127
344
  id: "tts-1-hd",
128
345
  name: "TTS 1 HD",
129
346
  provider: "openai",
130
- voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
347
+ voices: legacyVoices
131
348
  }
132
349
  ];
133
350
  }
134
351
  async getAvailableASRModels() {
352
+ if (!this.servesOpenAICatalog())
353
+ return [];
135
354
  return [
355
+ {
356
+ id: "gpt-4o-transcribe",
357
+ name: "GPT-4o Transcribe",
358
+ provider: "openai"
359
+ },
360
+ {
361
+ id: "gpt-4o-mini-transcribe",
362
+ name: "GPT-4o Mini Transcribe",
363
+ provider: "openai"
364
+ },
365
+ {
366
+ id: "gpt-4o-transcribe-diarize",
367
+ name: "GPT-4o Transcribe Diarize",
368
+ provider: "openai"
369
+ },
136
370
  {
137
371
  id: "whisper-1",
138
372
  name: "Whisper",
@@ -141,23 +375,45 @@ export class OpenAIProvider extends BaseProvider {
141
375
  ];
142
376
  }
143
377
  async getAvailableVideoModels() {
378
+ if (!this.servesOpenAICatalog())
379
+ return [];
144
380
  return [
145
381
  {
146
382
  id: "sora-2",
147
383
  name: "Sora 2",
148
384
  provider: "openai",
149
- supportedTasks: ["text_to_video"]
385
+ supportedTasks: ["text_to_video", "image_to_video"],
386
+ durations: [...SORA_VIDEO_DURATIONS],
387
+ resolutions: ["720p"],
388
+ aspectRatios: ["16:9", "9:16"]
150
389
  },
151
390
  {
152
391
  id: "sora-2-pro",
153
392
  name: "Sora 2 Pro",
154
393
  provider: "openai",
155
- supportedTasks: ["text_to_video"]
394
+ supportedTasks: ["text_to_video", "image_to_video"],
395
+ durations: [...SORA_VIDEO_DURATIONS],
396
+ resolutions: ["720p", "1024p", "1080p"],
397
+ aspectRatios: ["16:9", "9:16"]
156
398
  }
157
399
  ];
158
400
  }
159
401
  async getAvailableImageModels() {
402
+ if (!this.servesOpenAICatalog())
403
+ return [];
160
404
  return [
405
+ {
406
+ id: "gpt-image-2.5-flare",
407
+ name: "GPT Image 2.5 Flare",
408
+ provider: "openai",
409
+ supportedTasks: ["text_to_image", "image_to_image"]
410
+ },
411
+ {
412
+ id: "gpt-image-2.5-sunburst",
413
+ name: "GPT Image 2.5 Sunburst",
414
+ provider: "openai",
415
+ supportedTasks: ["text_to_image", "image_to_image"]
416
+ },
161
417
  {
162
418
  id: "gpt-image-2",
163
419
  name: "GPT Image 2",
@@ -185,6 +441,8 @@ export class OpenAIProvider extends BaseProvider {
185
441
  ];
186
442
  }
187
443
  async getAvailableEmbeddingModels() {
444
+ if (!this.servesOpenAICatalog())
445
+ return [];
188
446
  return [
189
447
  {
190
448
  id: "text-embedding-3-small",
@@ -206,10 +464,27 @@ export class OpenAIProvider extends BaseProvider {
206
464
  }
207
465
  ];
208
466
  }
209
- resolveImageSize(width, height) {
467
+ /**
468
+ * gpt-image-2 accepts arbitrary sizes within limits: both edges a multiple of
469
+ * 16, long edge <= 3840, aspect ratio <= 3:1, and total area between 0.64MP
470
+ * and 8.3MP. Requested sizes rarely land on that grid (16:9 at 1K is
471
+ * 1820x1024), so snap to the nearest valid size preserving the aspect ratio
472
+ * rather than dropping `size` and letting the API default to a square.
473
+ */
474
+ static snapToGptImage2Size(width, height) {
475
+ const size = snapToGptImage2Size(width, height);
476
+ return size ? `${size[0]}x${size[1]}` : null;
477
+ }
478
+ resolveImageSize(width, height, model) {
210
479
  if (!width || !height) {
211
480
  return null;
212
481
  }
482
+ const usesFlexibleDimensions = model === "gpt-image-2" ||
483
+ model?.startsWith("gpt-image-2-") === true ||
484
+ model?.startsWith("gpt-image-2.") === true;
485
+ if (usesFlexibleDimensions) {
486
+ return OpenAIProvider.snapToGptImage2Size(width, height);
487
+ }
213
488
  const supported = [
214
489
  [1024, 1024],
215
490
  [1024, 1536],
@@ -229,16 +504,24 @@ export class OpenAIProvider extends BaseProvider {
229
504
  : bestSoFar);
230
505
  return `${best[0]}x${best[1]}`;
231
506
  }
232
- static resolveVideoSize(aspectRatio, resolution) {
507
+ static resolveVideoSize(aspectRatio, resolution, model) {
233
508
  if (!resolution) {
234
509
  return null;
235
510
  }
236
- const supported = [
237
- [1280, 720],
238
- [1792, 1024],
239
- [720, 1280],
240
- [1024, 1792]
241
- ];
511
+ const isStandardSora2 = model?.startsWith("sora-2") === true && !model.includes("pro");
512
+ const supported = isStandardSora2
513
+ ? [
514
+ [1280, 720],
515
+ [720, 1280]
516
+ ]
517
+ : [
518
+ [1280, 720],
519
+ [1792, 1024],
520
+ [1920, 1080],
521
+ [720, 1280],
522
+ [1024, 1792],
523
+ [1080, 1920]
524
+ ];
242
525
  const aspect = (aspectRatio ?? "16:9").replaceAll(" ", "");
243
526
  const resolutionKey = resolution.toLowerCase();
244
527
  const presets = {
@@ -284,16 +567,18 @@ export class OpenAIProvider extends BaseProvider {
284
567
  return closest(Math.max(2, Math.round(targetW)), Math.max(2, Math.round(targetH)));
285
568
  }
286
569
  static secondsFromParams(params) {
570
+ const durationSeconds = params.durationSeconds;
571
+ if (durationSeconds != null &&
572
+ Number.isFinite(durationSeconds) &&
573
+ durationSeconds > 0) {
574
+ return SORA_VIDEO_DURATIONS.reduce((duration, candidate) => candidate <= durationSeconds ? candidate : duration, SORA_VIDEO_DURATIONS[0]);
575
+ }
287
576
  const numFrames = params.numFrames;
288
- if (!numFrames || numFrames <= 0) {
577
+ if (!numFrames || !Number.isFinite(numFrames) || numFrames <= 0) {
289
578
  return null;
290
579
  }
291
580
  const estimated = Math.ceil(numFrames / 24);
292
- if (estimated < 8)
293
- return 4;
294
- if (estimated < 12)
295
- return 8;
296
- return 12;
581
+ return SORA_VIDEO_DURATIONS.reduce((duration, candidate) => (candidate <= estimated ? candidate : duration), SORA_VIDEO_DURATIONS[0]);
297
582
  }
298
583
  static snapToValidVideoDimensions(width, height) {
299
584
  const supported = [
@@ -369,17 +654,38 @@ export class OpenAIProvider extends BaseProvider {
369
654
  }
370
655
  if (content.type === "audio") {
371
656
  const c = content;
372
- const base64 = c.audio.uri
373
- ? (await this.uriToBase64(c.audio.uri)).split(",", 2)[1]
374
- : Buffer.from(asUint8Array(c.audio.data ?? new Uint8Array())).toString("base64");
657
+ let bytes;
658
+ let mime = c.audio.mimeType;
659
+ if (c.audio.uri) {
660
+ const parsed = parseDataUri(await this.uriToBase64(c.audio.uri));
661
+ bytes = parsed.data;
662
+ mime = mime ?? parsed.mime;
663
+ }
664
+ else {
665
+ bytes = asUint8Array(c.audio.data ?? new Uint8Array());
666
+ }
375
667
  return {
376
668
  type: "input_audio",
377
669
  input_audio: {
378
- format: "mp3",
379
- data: base64
670
+ format: openAiAudioFormat(mime, bytes),
671
+ data: Buffer.from(bytes).toString("base64")
380
672
  }
381
673
  };
382
674
  }
675
+ if (content.type === "video") {
676
+ // Reached only with the fallback switched off: BaseProvider otherwise
677
+ // samples the clip into frames before conversion whenever
678
+ // `supportsVideoInput` is false, and a missing ffmpeg fails there with
679
+ // its own message. Named for the configured provider, not the wire
680
+ // format: every
681
+ // OpenAI-compatible re-host lands here, and reporting "openai" sent a
682
+ // diagnosis after the wrong provider.
683
+ throw new Error(`video input is not supported by ${this.provider} — its chat API is ` +
684
+ `OpenAI-compatible, which has no video content part. Frame sampling ` +
685
+ `is off (NODETOOL_VIDEO_FRAME_FALLBACK=0); unset it to read the clip ` +
686
+ `as stills, or use a provider whose chat models read video natively ` +
687
+ `(gemini).`);
688
+ }
383
689
  const c = content;
384
690
  const imageUrl = c.image.uri
385
691
  ? await this.uriToBase64(c.image.uri)
@@ -395,7 +701,7 @@ export class OpenAIProvider extends BaseProvider {
395
701
  throw new Error("Tool message requires toolCallId");
396
702
  }
397
703
  let content = "";
398
- if (typeof message.content === "string") {
704
+ if (isString(message.content)) {
399
705
  content = message.content;
400
706
  }
401
707
  else if (message.content != null) {
@@ -410,7 +716,7 @@ export class OpenAIProvider extends BaseProvider {
410
716
  if (message.role === "system") {
411
717
  return {
412
718
  role: "system",
413
- content: typeof message.content === "string" ? message.content : ""
719
+ content: isString(message.content) ? message.content : ""
414
720
  };
415
721
  }
416
722
  if (message.role === "assistant") {
@@ -422,24 +728,30 @@ export class OpenAIProvider extends BaseProvider {
422
728
  arguments: JSON.stringify(tc.args, defaultSerializer)
423
729
  }
424
730
  }));
425
- if (typeof message.content === "string" || message.content == null) {
426
- return {
731
+ if (isString(message.content) || message.content == null) {
732
+ const assistant = {
427
733
  role: "assistant",
428
- content: message.content,
429
- ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
734
+ content: message.content
430
735
  };
736
+ if (toolCalls.length > 0) {
737
+ assistant.tool_calls = toolCalls;
738
+ }
739
+ return assistant;
431
740
  }
432
741
  const parts = await Promise.all(message.content.map((part) => this.messageContentToOpenAIContentPart(part)));
433
- return {
742
+ const assistant = {
434
743
  role: "assistant",
435
- content: parts,
436
- ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
744
+ content: parts
437
745
  };
746
+ if (toolCalls.length > 0) {
747
+ assistant.tool_calls = toolCalls;
748
+ }
749
+ return assistant;
438
750
  }
439
751
  if (message.role !== "user") {
440
752
  throw new Error(`Unsupported role: ${message.role}`);
441
753
  }
442
- if (typeof message.content === "string") {
754
+ if (isString(message.content)) {
443
755
  return {
444
756
  role: "user",
445
757
  content: message.content
@@ -468,19 +780,31 @@ export class OpenAIProvider extends BaseProvider {
468
780
  });
469
781
  }
470
782
  convertSystemToUserForOModels(messages, model) {
471
- if (!model.startsWith("o")) {
783
+ // Match ONLY genuine OpenAI reasoning models (o1/o3/o4), not any id that
784
+ // merely starts with "o". Compat providers use namespaced ids like
785
+ // "openai/gpt-4o" or "openai/gpt-oss-120b" that begin with "o" but fully
786
+ // support the system role — mangling their system prompt corrupts every
787
+ // request. Strip a leading "vendor/" namespace before matching.
788
+ const bareModel = model.includes("/")
789
+ ? model.slice(model.lastIndexOf("/") + 1)
790
+ : model;
791
+ if (!/^o[134](-|$)/.test(bareModel)) {
472
792
  return messages;
473
793
  }
474
794
  return messages.map((msg) => msg.role === "system"
475
795
  ? {
476
796
  ...msg,
477
797
  role: "user",
478
- content: `Instructions: ${typeof msg.content === "string" ? msg.content : ""}`
798
+ content: `Instructions: ${isString(msg.content) ? msg.content : ""}`
479
799
  }
480
800
  : msg);
481
801
  }
482
802
  async *generateMessages(args) {
483
- const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty, audio } = args;
803
+ if (this.usesResponsesApi(args.model)) {
804
+ yield* this.generateMessagesResponses(args);
805
+ return;
806
+ }
807
+ const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty, audio, toolChoice } = args;
484
808
  const messages = this.convertSystemToUserForOModels(args.messages, model);
485
809
  const openaiMessages = await Promise.all(messages.map((m) => this.convertMessage(m)));
486
810
  const request = {
@@ -505,9 +829,19 @@ export class OpenAIProvider extends BaseProvider {
505
829
  }
506
830
  if (hasTools) {
507
831
  request.tools = this.formatTools(tools);
832
+ if (toolChoice) {
833
+ request.tool_choice =
834
+ toolChoice === "any"
835
+ ? "required"
836
+ : { type: "function", function: { name: toolChoice } };
837
+ }
508
838
  }
509
839
  log.debug("OpenAI request", { model });
510
- const stream = (await this.getClient().chat.completions.create(request));
840
+ this.recordRequestPayload(request);
841
+ // SAFETY: the request is assembled field by field as a dictionary, which
842
+ // the SDK's closed param interface does not overlap; `stream: true` on it
843
+ // selects the streaming overload.
844
+ const stream = (await this.getClient().chat.completions.create(request, { signal: args.signal }));
511
845
  const deltaToolCalls = new Map();
512
846
  try {
513
847
  for await (const chunk of stream) {
@@ -555,22 +889,34 @@ export class OpenAIProvider extends BaseProvider {
555
889
  };
556
890
  yield item;
557
891
  }
558
- if (choice.finish_reason === "tool_calls") {
892
+ if (choice.finish_reason && deltaToolCalls.size > 0) {
559
893
  for (const call of deltaToolCalls.values()) {
560
894
  const toolCall = this.buildToolCall(call.id, call.name, call.arguments);
561
895
  yield toolCall;
562
896
  }
563
897
  deltaToolCalls.clear();
564
898
  }
899
+ // Always emit a terminal `done: true` chunk when the completion
900
+ // finishes, regardless of reason. "stop" already emits one above; for
901
+ // every other terminal reason (tool_calls, length, content_filter)
902
+ // emit one here so consumers get a consistent end-of-stream marker
903
+ // (matching the Anthropic and Gemini providers).
904
+ if (choice.finish_reason && choice.finish_reason !== "stop") {
905
+ const doneChunk = { type: "chunk", content: "", done: true };
906
+ yield doneChunk;
907
+ }
565
908
  }
566
909
  }
567
910
  finally {
568
- if (typeof stream.close === "function") {
911
+ if (isCallable(stream.close)) {
569
912
  await stream.close();
570
913
  }
571
914
  }
572
915
  }
573
916
  async generateMessage(args) {
917
+ if (this.usesResponsesApi(args.model)) {
918
+ return this.generateMessageResponses(args);
919
+ }
574
920
  const { model, tools = [], toolChoice, maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
575
921
  const messages = this.convertSystemToUserForOModels(args.messages, model);
576
922
  const openaiMessages = await Promise.all(messages.map((m) => this.convertMessage(m)));
@@ -599,7 +945,10 @@ export class OpenAIProvider extends BaseProvider {
599
945
  }
600
946
  }
601
947
  log.debug("OpenAI request", { model });
602
- const completion = await this.getClient().chat.completions.create(request);
948
+ this.recordRequestPayload(request);
949
+ // SAFETY: as above — a dictionary request against the SDK's closed
950
+ // non-streaming param interface.
951
+ const completion = await this.getClient().chat.completions.create(request, { signal: args.signal });
603
952
  const choice = completion.choices?.[0];
604
953
  if (!choice) {
605
954
  throw new Error("OpenAI returned no choices");
@@ -624,6 +973,401 @@ export class OpenAIProvider extends BaseProvider {
624
973
  toolCalls
625
974
  };
626
975
  }
976
+ // ---------------------------------------------------------------------------
977
+ // Responses API path (used for OpenAI models matched by isOpenAIResponsesModel)
978
+ // ---------------------------------------------------------------------------
979
+ /** Tool shapes for the Responses API: native web_search / image_generation. */
980
+ formatResponsesTools(tools) {
981
+ const formatted = [];
982
+ for (const tool of tools) {
983
+ if (tool.name === WEB_SEARCH_TOOL_NAME) {
984
+ formatted.push(RESPONSE_WEB_SEARCH_TOOL);
985
+ continue;
986
+ }
987
+ if (tool.name === IMAGE_GENERATION_TOOL_NAME) {
988
+ formatted.push(RESPONSE_IMAGE_GENERATION_TOOL);
989
+ continue;
990
+ }
991
+ const [functionTool] = responseTools([tool]);
992
+ if (functionTool)
993
+ formatted.push(functionTool);
994
+ }
995
+ return formatted;
996
+ }
997
+ async generateMessageResponses(args) {
998
+ const request = await this.buildResponsesRequest(args, {
999
+ input: await messagesToResponsesInput(args.messages, (uri) => this.resolveUri(uri)),
1000
+ stream: false,
1001
+ store: false
1002
+ });
1003
+ const client = this.getClient();
1004
+ this.recordRequestPayload(request);
1005
+ // SAFETY: the SDK's `create` is overloaded over a closed param interface,
1006
+ // while this request is assembled field by field; `stream: false` is set
1007
+ // on it, so the SDK resolves a response object, not an async iterable.
1008
+ const response = (await client.responses.create.call(client.responses, request, {
1009
+ signal: args.signal
1010
+ }));
1011
+ this.trackUsage(args.model, responseUsage(response));
1012
+ const outputText = isString(response.output_text)
1013
+ ? response.output_text
1014
+ : extractResponsesText(response.output);
1015
+ const toolCalls = extractResponsesToolCalls(response.output, this.buildToolCall.bind(this));
1016
+ const images = extractResponsesImages(response.output);
1017
+ const content = images.length > 0
1018
+ ? assistantContentWithImages(outputText, images)
1019
+ : outputText || null;
1020
+ return {
1021
+ role: "assistant",
1022
+ content,
1023
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1024
+ };
1025
+ }
1026
+ async *generateMessagesResponses(args) {
1027
+ const input = await messagesToResponsesInput(args.messages, (uri) => this.resolveUri(uri));
1028
+ const request = await this.buildResponsesRequest(args, {
1029
+ input,
1030
+ stream: true,
1031
+ store: false
1032
+ });
1033
+ // Absorb native image results here too — this non-loop path must uphold
1034
+ // the same invariant as collectModelTurn: the base64 blob never leaks
1035
+ // outward as a chunk. The image rides a trailing assistant message event.
1036
+ const images = [];
1037
+ let assistantText = "";
1038
+ for await (const item of this.streamResponsesRequest(args, request)) {
1039
+ const image = imageContentFromChunk(item);
1040
+ if (image) {
1041
+ images.push(image);
1042
+ continue;
1043
+ }
1044
+ if (isRecord(item) &&
1045
+ item.type === "chunk" &&
1046
+ isString(item.content) &&
1047
+ !item.thinking) {
1048
+ assistantText += item.content;
1049
+ }
1050
+ yield item;
1051
+ }
1052
+ if (images.length > 0) {
1053
+ yield {
1054
+ type: "message",
1055
+ message: {
1056
+ role: "assistant",
1057
+ content: assistantContentWithImages(assistantText, images)
1058
+ }
1059
+ };
1060
+ }
1061
+ }
1062
+ async *generateLoop(args) {
1063
+ if (!this.usesResponsesApi(args.model)) {
1064
+ yield* super.generateLoop(args);
1065
+ return;
1066
+ }
1067
+ const maxIterations = args.maxIterations ?? 25;
1068
+ const { executeTool, maxIterations: _omitMaxIterations, sequentialTools, turnBudget: budgetArg, resolveMedia, ...turnArgs } = args;
1069
+ const { runBudget, turnBudget } = resolveTurnBudget(budgetArg);
1070
+ const systemHash = hashSystemPrompt(responsesSystemPrompt(args.messages));
1071
+ const prior = args.providerSession ?? null;
1072
+ const canResume = prior != null &&
1073
+ prior.providerId === this.provider &&
1074
+ prior.model === args.model &&
1075
+ (prior.systemHash == null || prior.systemHash === systemHash) &&
1076
+ args.messages.length > prior.checkpoint;
1077
+ if (canResume && prior) {
1078
+ const firstMessages = args.messages.slice(prior.checkpoint);
1079
+ const firstTurnState = this.createResponsesTurnState(prior.token);
1080
+ try {
1081
+ yield* this.runResponsesLoop({
1082
+ args: turnArgs,
1083
+ resolveMedia,
1084
+ executeTool,
1085
+ sequentialTools: sequentialTools === true,
1086
+ maxIterations,
1087
+ firstMessages,
1088
+ firstPreviousResponseId: prior.token,
1089
+ systemHash,
1090
+ firstTurnState,
1091
+ turnBudget,
1092
+ runBudget
1093
+ });
1094
+ return;
1095
+ }
1096
+ catch (err) {
1097
+ if (firstTurnState.emittedContent) {
1098
+ throw err;
1099
+ }
1100
+ log.warn("OpenAI Responses resume failed; starting fresh", {
1101
+ error: err instanceof Error ? err.message : String(err)
1102
+ });
1103
+ }
1104
+ }
1105
+ const freshMessages = args.loadFullHistory
1106
+ ? await args.loadFullHistory()
1107
+ : args.messages;
1108
+ yield* this.runResponsesLoop({
1109
+ args: turnArgs,
1110
+ resolveMedia,
1111
+ executeTool,
1112
+ sequentialTools: sequentialTools === true,
1113
+ maxIterations,
1114
+ firstMessages: freshMessages,
1115
+ firstPreviousResponseId: null,
1116
+ systemHash,
1117
+ firstTurnState: this.createResponsesTurnState(null),
1118
+ turnBudget,
1119
+ runBudget
1120
+ });
1121
+ }
1122
+ async buildResponsesRequest(args, config) {
1123
+ const request = {
1124
+ model: args.model,
1125
+ input: config.input,
1126
+ stream: config.stream,
1127
+ store: config.store
1128
+ };
1129
+ if (config.previousResponseId) {
1130
+ request.previous_response_id = config.previousResponseId;
1131
+ }
1132
+ if (args.maxTokens != null)
1133
+ request.max_output_tokens = args.maxTokens;
1134
+ if (args.temperature != null)
1135
+ request.temperature = args.temperature;
1136
+ if (args.topP != null)
1137
+ request.top_p = args.topP;
1138
+ const tools = this.formatResponsesTools(args.tools ?? []);
1139
+ if (tools.length > 0) {
1140
+ request.tools = tools;
1141
+ const hostedToolChoice = args.toolChoice
1142
+ ? RESPONSE_HOSTED_TOOL_CHOICES[args.toolChoice]
1143
+ : undefined;
1144
+ request.tool_choice = hostedToolChoice
1145
+ ? { type: hostedToolChoice }
1146
+ : (responseToolChoice(args.toolChoice) ?? "auto");
1147
+ }
1148
+ return request;
1149
+ }
1150
+ async *streamResponsesRequest(args, request, onResponseId) {
1151
+ const client = this.getClient();
1152
+ this.recordRequestPayload(request);
1153
+ // SAFETY: as in `generateMessageResponses` — an overloaded SDK method over
1154
+ // a closed param interface, given a request assembled field by field. The
1155
+ // caller set `stream: true`, so the SDK resolves an async iterable.
1156
+ const stream = (await client.responses.create.call(client.responses, request, {
1157
+ signal: args.signal
1158
+ }));
1159
+ yield* streamResponsesEvents(stream, {
1160
+ model: args.model,
1161
+ buildToolCall: this.buildToolCall.bind(this),
1162
+ onUsage: (model, usage) => this.trackUsage(model, usage),
1163
+ onResponseId
1164
+ });
1165
+ }
1166
+ createResponsesTurnState(responseId) {
1167
+ return {
1168
+ assistantText: "",
1169
+ images: [],
1170
+ pending: [],
1171
+ responseId,
1172
+ emittedContent: false
1173
+ };
1174
+ }
1175
+ async *runResponsesLoop(config) {
1176
+ const toolMap = new Map((config.args.tools ?? []).map((tool) => [tool.name, tool]));
1177
+ let previousResponseId = config.firstPreviousResponseId;
1178
+ const toInput = async (messages) => messagesToResponsesInput(config.resolveMedia ? await config.resolveMedia(messages) : messages, (uri) => this.resolveUri(uri));
1179
+ let input = await toInput(config.firstMessages);
1180
+ let state = config.firstTurnState;
1181
+ // Responses carries prior turns server-side behind `previousResponseId`,
1182
+ // so `input` holds only the delta. Reserving against the delta would price
1183
+ // a turn as if the conversation restarted; the transcript is tracked here
1184
+ // so the reservation grows with the turn it is admitting.
1185
+ const transcript = [...config.firstMessages];
1186
+ const runBudget = config.runBudget ?? null;
1187
+ for (let iteration = 0; iteration < config.maxIterations; iteration++) {
1188
+ if (config.args.signal?.aborted) {
1189
+ yield PROVIDER_STOP_ABORTED;
1190
+ return;
1191
+ }
1192
+ if (runBudget?.deadline.expired()) {
1193
+ yield budgetStopItem(runBudget);
1194
+ return;
1195
+ }
1196
+ let reservation = null;
1197
+ if (config.turnBudget) {
1198
+ reservation = this._admitTurn(config.turnBudget, config.args.model, transcript);
1199
+ if (reservation === null) {
1200
+ yield budgetStopItem(runBudget);
1201
+ return;
1202
+ }
1203
+ }
1204
+ const costBeforeTurn = config.turnBudget ? this.getTotalCost() : 0;
1205
+ // The reservation covers exactly one turn, so it has to be reconciled
1206
+ // even when that turn throws. Left outstanding, a single provider error
1207
+ // would shrink the run's headroom for good — the budget is run-level and
1208
+ // outlives the decision that hit the error.
1209
+ try {
1210
+ const request = await this.buildResponsesRequest(config.args, {
1211
+ input,
1212
+ stream: true,
1213
+ store: true,
1214
+ previousResponseId
1215
+ });
1216
+ yield* this.collectResponsesTurn(config.args, request, config.systemHash, state);
1217
+ }
1218
+ finally {
1219
+ if (config.turnBudget && reservation) {
1220
+ config.turnBudget.commit(reservation, this.getTotalCost() - costBeforeTurn);
1221
+ }
1222
+ }
1223
+ previousResponseId = state.responseId;
1224
+ // Native image_generation results ride the assistant message as image
1225
+ // content and don't affect pending-tool semantics.
1226
+ const assistantContent = state.images.length > 0
1227
+ ? assistantContentWithImages(state.assistantText, state.images)
1228
+ : state.assistantText || null;
1229
+ const assistantMsg = {
1230
+ role: "assistant",
1231
+ content: assistantContent,
1232
+ toolCalls: state.pending.length > 0 ? state.pending : null
1233
+ };
1234
+ yield { type: "message", message: assistantMsg };
1235
+ transcript.push(assistantMsg);
1236
+ if (state.pending.length === 0) {
1237
+ return;
1238
+ }
1239
+ if (!previousResponseId) {
1240
+ throw new Error("OpenAI Responses returned tool calls without a response id");
1241
+ }
1242
+ const toolMessages = [];
1243
+ const runTool = async (tc) => {
1244
+ // Per-tool error isolation: a thrown tool must still yield a
1245
+ // tool_result so a parallel Promise.all rejection can't discard sibling
1246
+ // results and leave a dangling tool_use (rejected by the API next turn).
1247
+ try {
1248
+ const tool = toolMap.get(tc.name);
1249
+ if (tool?.execute)
1250
+ return await tool.execute(tc.args ?? {}, tc.id);
1251
+ if (config.executeTool)
1252
+ return await config.executeTool(tc);
1253
+ return `Tool "${tc.name}" is not available`;
1254
+ }
1255
+ catch (err) {
1256
+ return `Error executing tool "${tc.name}": ${err instanceof Error ? err.message : String(err)}`;
1257
+ }
1258
+ };
1259
+ const emitToolResult = function* (tc, content) {
1260
+ const { toolContent, imageMessage } = splitToolResultImages(content);
1261
+ const toolMsg = {
1262
+ role: "tool",
1263
+ toolCallId: tc.id,
1264
+ content: toolContent
1265
+ };
1266
+ toolMessages.push(toolMsg);
1267
+ transcript.push(toolMsg);
1268
+ yield { type: "message", message: toolMsg };
1269
+ if (imageMessage) {
1270
+ toolMessages.push(imageMessage);
1271
+ // It rides the next turn's input, so it is part of what that turn
1272
+ // is billed for — and therefore part of what its reservation covers.
1273
+ transcript.push(imageMessage);
1274
+ }
1275
+ };
1276
+ let terminated = false;
1277
+ if (config.sequentialTools) {
1278
+ for (const tc of state.pending) {
1279
+ // Honor a mid-turn abort (mirrors base-provider generateLoop): a tool
1280
+ // like finish_step aborts the signal to stop the model running any
1281
+ // further tools THIS turn. Without this check the Responses loop would
1282
+ // keep executing pending tools after completion — duplicate
1283
+ // step_result/StepCompleted events, overwritten memory, and stray
1284
+ // side-effecting tools after the step is already done.
1285
+ if (config.args.signal?.aborted)
1286
+ break;
1287
+ const content = await runTool(tc);
1288
+ yield* emitToolResult(tc, content);
1289
+ const tool = toolMap.get(tc.name);
1290
+ if (tool?.terminal) {
1291
+ terminated = true;
1292
+ break;
1293
+ }
1294
+ }
1295
+ }
1296
+ else {
1297
+ const results = await Promise.all(state.pending.map(async (tc) => ({
1298
+ tc,
1299
+ content: await runTool(tc)
1300
+ })));
1301
+ for (const { tc, content } of results) {
1302
+ yield* emitToolResult(tc, content);
1303
+ const tool = toolMap.get(tc.name);
1304
+ if (tool?.terminal)
1305
+ terminated = true;
1306
+ }
1307
+ }
1308
+ input = await toInput(toolMessages);
1309
+ if (terminated)
1310
+ return;
1311
+ state = this.createResponsesTurnState(previousResponseId);
1312
+ }
1313
+ yield { type: "chunk", content: "", done: true };
1314
+ yield {
1315
+ type: "stop",
1316
+ reason: "iterations",
1317
+ detail: `stopped after ${config.maxIterations} tool-calling round(s)`
1318
+ };
1319
+ }
1320
+ async *collectResponsesTurn(args, request, systemHash, state) {
1321
+ const sessionUpdates = [];
1322
+ const enqueueSession = (responseId) => {
1323
+ if (state.responseId === responseId)
1324
+ return;
1325
+ state.responseId = responseId;
1326
+ const session = {
1327
+ providerId: this.provider,
1328
+ model: args.model,
1329
+ token: responseId,
1330
+ checkpoint: args.messages.length,
1331
+ systemHash
1332
+ };
1333
+ sessionUpdates.push({ type: "session", session });
1334
+ };
1335
+ const flushSessions = function* () {
1336
+ while (sessionUpdates.length > 0) {
1337
+ const update = sessionUpdates.shift();
1338
+ if (update)
1339
+ yield update;
1340
+ }
1341
+ };
1342
+ for await (const item of this.streamResponsesRequest(args, request, enqueueSession)) {
1343
+ yield* flushSessions();
1344
+ if (isProviderSessionUpdate(item) || isProviderMessageEvent(item)) {
1345
+ yield item;
1346
+ continue;
1347
+ }
1348
+ if ("id" in item && "name" in item && "args" in item) {
1349
+ state.pending.push(item);
1350
+ state.emittedContent = true;
1351
+ yield item;
1352
+ continue;
1353
+ }
1354
+ if (isRecord(item) && item.type === "chunk") {
1355
+ const image = imageContentFromChunk(item);
1356
+ if (image) {
1357
+ state.images.push(image);
1358
+ state.emittedContent = true;
1359
+ continue;
1360
+ }
1361
+ if (isString(item.content) && !item.thinking) {
1362
+ state.assistantText += item.content;
1363
+ if (item.content)
1364
+ state.emittedContent = true;
1365
+ }
1366
+ yield item;
1367
+ }
1368
+ }
1369
+ yield* flushSessions();
1370
+ }
627
1371
  async textToImage(params) {
628
1372
  if (!params.prompt) {
629
1373
  throw new Error("The input prompt cannot be empty.");
@@ -635,12 +1379,14 @@ export class OpenAIProvider extends BaseProvider {
635
1379
  model: params.model.id,
636
1380
  prompt
637
1381
  };
638
- const size = this.resolveImageSize(params.width ?? undefined, params.height ?? undefined);
1382
+ const size = this.resolveImageSize(params.width ?? undefined, params.height ?? undefined, params.model.id);
639
1383
  if (size)
640
1384
  request.size = size;
641
1385
  if (params.quality)
642
1386
  request.quality = params.quality;
643
- const response = (await this.getClient().images.generate(request));
1387
+ // SAFETY: a dictionary request against the SDK's closed image params.
1388
+ const response = (await this.getClient().images.generate(request, { signal: params.signal }));
1389
+ this.trackUsage(params.model.id, openAIImageUsage(response, params.quality));
644
1390
  const item = response.data?.[0];
645
1391
  if (!item) {
646
1392
  throw new Error("OpenAI image generation returned no image data.");
@@ -657,8 +1403,40 @@ export class OpenAIProvider extends BaseProvider {
657
1403
  }
658
1404
  throw new Error("OpenAI image generation returned no image data.");
659
1405
  }
660
- async imageToImage(image, params) {
661
- if (!image || image.length === 0) {
1406
+ /**
1407
+ * Convert a white-region mask (R=G=B=255, alpha = selection) into the mask
1408
+ * OpenAI's edit endpoint expects, where fully transparent pixels mark the
1409
+ * region to regenerate. Inverts the alpha channel and keeps RGBA shape.
1410
+ */
1411
+ async toOpenAiEditMask(mask) {
1412
+ const sharp = await loadSharp();
1413
+ const { data, info } = await sharp(Buffer.from(mask))
1414
+ .ensureAlpha()
1415
+ .raw()
1416
+ .toBuffer({ resolveWithObject: true });
1417
+ for (let i = 3; i < data.length; i += 4) {
1418
+ data[i] = 255 - data[i];
1419
+ }
1420
+ const png = await sharp(data, {
1421
+ raw: { width: info.width, height: info.height, channels: 4 }
1422
+ })
1423
+ .png()
1424
+ .toBuffer();
1425
+ return new Uint8Array(png);
1426
+ }
1427
+ async imageToImage(images, params) {
1428
+ return this.runImageEdit(images, params);
1429
+ }
1430
+ async inpaint(images, params) {
1431
+ return this.runImageEdit(images, params, params.mask);
1432
+ }
1433
+ /**
1434
+ * Shared OpenAI image-edit call for both image-to-image and inpaint. When
1435
+ * `mask` is supplied the masked region is regenerated.
1436
+ */
1437
+ async runImageEdit(images, params, mask) {
1438
+ const sources = images.filter((b) => b && b.length > 0);
1439
+ if (sources.length === 0) {
662
1440
  throw new Error("image must not be empty.");
663
1441
  }
664
1442
  if (!params.prompt) {
@@ -667,19 +1445,32 @@ export class OpenAIProvider extends BaseProvider {
667
1445
  const prompt = params.negativePrompt
668
1446
  ? `${params.prompt.trim()}\n\nDo not include: ${params.negativePrompt.trim()}`
669
1447
  : params.prompt;
1448
+ // gpt-image-* accepts multiple reference images as an array; fall back to a
1449
+ // single file for one input so older edit models keep working.
1450
+ const imageFiles = await Promise.all(sources.map((b, i) => toFile(Buffer.from(b), `image_${i}.png`, { type: "image/png" })));
670
1451
  const request = {
671
1452
  model: params.model.id,
672
- image: await toFile(Buffer.from(image), "image.png", {
673
- type: "image/png"
674
- }),
1453
+ image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
675
1454
  prompt
676
1455
  };
677
- const size = this.resolveImageSize(params.targetWidth ?? undefined, params.targetHeight ?? undefined);
1456
+ // Inpainting: the edit endpoint marks the region to regenerate with the
1457
+ // mask's *transparent* pixels. Our masks use the opposite convention
1458
+ // (opaque/white alpha = edit region, matching FAL), so invert the alpha
1459
+ // channel before sending.
1460
+ if (mask && mask.length > 0) {
1461
+ const openAiMask = await this.toOpenAiEditMask(mask);
1462
+ request.mask = await toFile(Buffer.from(openAiMask), "mask.png", {
1463
+ type: "image/png"
1464
+ });
1465
+ }
1466
+ const size = this.resolveImageSize(params.targetWidth ?? undefined, params.targetHeight ?? undefined, params.model.id);
678
1467
  if (size)
679
1468
  request.size = size;
680
1469
  if (params.quality)
681
1470
  request.quality = params.quality;
682
- const response = (await this.getClient().images.edit(request));
1471
+ // SAFETY: a dictionary request against the SDK's closed edit params.
1472
+ const response = (await this.getClient().images.edit(request, { signal: params.signal }));
1473
+ this.trackUsage(params.model.id, openAIImageUsage(response, params.quality));
683
1474
  const item = response.data?.[0];
684
1475
  if (!item) {
685
1476
  throw new Error("OpenAI image editing returned no image data.");
@@ -704,29 +1495,58 @@ export class OpenAIProvider extends BaseProvider {
704
1495
  const speed = Math.max(0.25, Math.min(4.0, args.speed ?? 1.0));
705
1496
  const speechApi = this.getClient().audio.speech;
706
1497
  if (speechApi.with_streaming_response?.create) {
707
- const response = await speechApi.with_streaming_response.create({
1498
+ const request = {
708
1499
  model: args.model,
709
1500
  input: args.text,
710
1501
  voice,
711
1502
  speed,
712
1503
  response_format: "pcm"
713
- });
1504
+ };
1505
+ if (args.model === "gpt-4o-mini-tts" && (args.language || args.instructions)) {
1506
+ request.instructions = [
1507
+ args.language ? `Speak in ${args.language}.` : undefined,
1508
+ args.instructions
1509
+ ]
1510
+ .filter((value) => value !== undefined)
1511
+ .join(" ");
1512
+ }
1513
+ const response = await speechApi.with_streaming_response.create(request);
1514
+ // TTS is billed per input character, not per token.
1515
+ this.trackUsage(args.model, { inputCharacters: args.text.length });
1516
+ let carry;
714
1517
  for await (const chunk of response.iterBytes(4096)) {
715
- const bytes = asUint8Array(chunk);
716
- yield { samples: toInt16Samples(bytes) };
1518
+ const incoming = asUint8Array(chunk);
1519
+ const bytes = new Uint8Array(incoming.length + (carry == null ? 0 : 1));
1520
+ if (carry != null)
1521
+ bytes[0] = carry;
1522
+ bytes.set(incoming, carry == null ? 0 : 1);
1523
+ const completeLength = bytes.length - (bytes.length % 2);
1524
+ carry =
1525
+ completeLength < bytes.length ? bytes[bytes.length - 1] : undefined;
1526
+ if (completeLength > 0) {
1527
+ yield { samples: toInt16Samples(bytes.subarray(0, completeLength)) };
1528
+ }
717
1529
  }
718
1530
  return;
719
1531
  }
720
- const response = await speechApi.create({
1532
+ const request = {
721
1533
  model: args.model,
722
1534
  input: args.text,
723
1535
  voice,
724
1536
  speed,
725
1537
  response_format: "pcm"
726
- });
727
- const bytes = asUint8Array(typeof response.arrayBuffer === "function"
728
- ? await response.arrayBuffer()
729
- : response);
1538
+ };
1539
+ if (args.model === "gpt-4o-mini-tts" && (args.language || args.instructions)) {
1540
+ request.instructions = [
1541
+ args.language ? `Speak in ${args.language}.` : undefined,
1542
+ args.instructions
1543
+ ]
1544
+ .filter((value) => value !== undefined)
1545
+ .join(" ");
1546
+ }
1547
+ const response = await speechApi.create(request);
1548
+ this.trackUsage(args.model, { inputCharacters: args.text.length });
1549
+ const bytes = asUint8Array(isCallable(response.arrayBuffer) ? await response.arrayBuffer() : response);
730
1550
  yield { samples: toInt16Samples(bytes) };
731
1551
  }
732
1552
  /**
@@ -754,16 +1574,24 @@ export class OpenAIProvider extends BaseProvider {
754
1574
  const voice = args.voice ?? "alloy";
755
1575
  const speed = Math.max(0.25, Math.min(4.0, args.speed ?? 1.0));
756
1576
  const speechApi = this.getClient().audio.speech;
757
- const response = await speechApi.create({
1577
+ const request = {
758
1578
  model: args.model,
759
1579
  input: args.text,
760
1580
  voice,
761
1581
  speed,
762
1582
  response_format: fmt
763
- });
764
- const bytes = asUint8Array(typeof response.arrayBuffer === "function"
765
- ? await response.arrayBuffer()
766
- : response);
1583
+ };
1584
+ if (args.model === "gpt-4o-mini-tts" && (args.language || args.instructions)) {
1585
+ request.instructions = [
1586
+ args.language ? `Speak in ${args.language}.` : undefined,
1587
+ args.instructions
1588
+ ]
1589
+ .filter((value) => value !== undefined)
1590
+ .join(" ");
1591
+ }
1592
+ const response = await speechApi.create(request);
1593
+ this.trackUsage(args.model, { inputCharacters: args.text.length });
1594
+ const bytes = asUint8Array(isCallable(response.arrayBuffer) ? await response.arrayBuffer() : response);
767
1595
  return { data: bytes, mimeType: formatToMime[fmt] };
768
1596
  }
769
1597
  async automaticSpeechRecognition(args) {
@@ -772,25 +1600,53 @@ export class OpenAIProvider extends BaseProvider {
772
1600
  }
773
1601
  const temperature = Math.max(0, Math.min(1, args.temperature ?? 0));
774
1602
  const audioPart = new Uint8Array(args.audio);
1603
+ // Label the upload with what the bytes actually are. Hardcoding audio/mpeg
1604
+ // made OpenAI reject anything that was not MP3 with "400 Audio file might
1605
+ // be corrupted or unsupported" — which is every clip coming through
1606
+ // `nodetool.video.ExtractAudio`, since that writes PCM WAV.
1607
+ const audioMime = sniffAudioMime(audioPart);
1608
+ const audioName = `audio.${AUDIO_MIME_EXT[audioMime] ?? "mp3"}`;
775
1609
  const fileLike = typeof File !== "undefined"
776
- ? new File([audioPart], "audio.mp3", { type: "audio/mpeg" })
777
- : Object.assign(new Blob([audioPart], { type: "audio/mpeg" }), {
778
- name: "audio.mp3"
1610
+ ? new File([audioPart], audioName, { type: audioMime })
1611
+ : Object.assign(new Blob([audioPart], { type: audioMime }), {
1612
+ name: audioName
779
1613
  });
1614
+ const isDiarizationModel = args.model.startsWith("gpt-4o-transcribe-diarize");
1615
+ if (args.word_timestamps && args.model !== "whisper-1") {
1616
+ throw new Error("Word timestamps are only supported by whisper-1");
1617
+ }
780
1618
  const requestParams = {
781
1619
  file: fileLike,
782
1620
  model: args.model,
783
1621
  language: args.language,
784
- prompt: args.prompt,
785
1622
  temperature
786
1623
  };
1624
+ if (!isDiarizationModel && args.prompt) {
1625
+ requestParams.prompt = args.prompt;
1626
+ }
1627
+ if (isDiarizationModel) {
1628
+ requestParams.response_format = "diarized_json";
1629
+ requestParams.chunking_strategy = "auto";
1630
+ }
787
1631
  if (args.word_timestamps) {
788
1632
  requestParams.response_format = "verbose_json";
789
1633
  requestParams.timestamp_granularities = ["word", "segment"];
790
1634
  }
791
1635
  const response = await this.getClient().audio.transcriptions.create(requestParams);
1636
+ // Whisper/transcribe is billed per second of audio. The duration is only
1637
+ // reported on the verbose/diarized formats (and as `usage.seconds` on the
1638
+ // newer transcribe models); skip accounting when it is absent rather than
1639
+ // guessing.
1640
+ const durationSeconds = isNumber(response.duration)
1641
+ ? response.duration
1642
+ : isNumber(response.usage?.seconds)
1643
+ ? response.usage.seconds
1644
+ : undefined;
1645
+ if (durationSeconds !== undefined) {
1646
+ this.trackUsage(args.model, { durationSeconds });
1647
+ }
792
1648
  const text = String(response.text ?? "");
793
- if (!args.word_timestamps) {
1649
+ if (!args.word_timestamps && !isDiarizationModel) {
794
1650
  return { text };
795
1651
  }
796
1652
  const chunks = [];
@@ -820,19 +1676,24 @@ export class OpenAIProvider extends BaseProvider {
820
1676
  if (!params.prompt) {
821
1677
  throw new Error("The input prompt cannot be empty.");
822
1678
  }
823
- const size = OpenAIProvider.resolveVideoSize(params.aspectRatio, params.resolution);
1679
+ const size = OpenAIProvider.resolveVideoSize(params.aspectRatio, params.resolution, params.model.id);
824
1680
  const seconds = OpenAIProvider.secondsFromParams(params) ?? 4;
825
1681
  const request = {
826
1682
  model: params.model.id,
827
1683
  prompt: params.prompt,
828
- size: size ?? "1024x1024",
1684
+ size: size ?? "1280x720",
829
1685
  seconds: String(seconds)
830
1686
  };
831
- const video = await this.getClient().videos.create(request);
1687
+ // SAFETY: a dictionary request against the SDK's closed video params.
1688
+ const video = await this.getClient().videos.create(request, { signal: params.signal });
832
1689
  if (!video?.id) {
833
1690
  throw new Error("OpenAI video create response did not contain a video id");
834
1691
  }
835
- const timeoutMs = 10 * 60 * 1000;
1692
+ const timeoutMs = params.timeoutSeconds != null &&
1693
+ Number.isFinite(params.timeoutSeconds) &&
1694
+ params.timeoutSeconds > 0
1695
+ ? params.timeoutSeconds * 1000
1696
+ : 10 * 60 * 1000;
836
1697
  const intervalMs = 2000;
837
1698
  const start = Date.now();
838
1699
  let latest = video;
@@ -840,8 +1701,17 @@ export class OpenAIProvider extends BaseProvider {
840
1701
  if (Date.now() - start > timeoutMs) {
841
1702
  throw new Error("Video generation timed out");
842
1703
  }
843
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
844
- latest = await this.getClient().videos.retrieve(video.id);
1704
+ const remainingMs = timeoutMs - (Date.now() - start);
1705
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remainingMs)));
1706
+ if (Date.now() - start >= timeoutMs) {
1707
+ throw new Error("Video generation timed out");
1708
+ }
1709
+ if (params.signal?.aborted) {
1710
+ throw new Error("Video generation cancelled");
1711
+ }
1712
+ latest = await this.getClient().videos.retrieve(video.id, {
1713
+ signal: params.signal
1714
+ });
845
1715
  }
846
1716
  if (latest.status !== "completed") {
847
1717
  throw new Error(String(latest.error ??
@@ -855,14 +1725,13 @@ export class OpenAIProvider extends BaseProvider {
855
1725
  throw new Error("The input image cannot be empty.");
856
1726
  }
857
1727
  const [width, height] = OpenAIProvider.extractImageDimensions(image);
858
- const requestedSize = OpenAIProvider.resolveVideoSize(params.aspectRatio, params.resolution);
1728
+ const requestedSize = OpenAIProvider.resolveVideoSize(params.aspectRatio, params.resolution, params.model.id);
859
1729
  const size = requestedSize ?? OpenAIProvider.snapToValidVideoDimensions(width, height);
860
1730
  const seconds = OpenAIProvider.secondsFromParams(params) ?? 4;
861
1731
  const [targetW, targetH] = size.split("x").map(Number);
862
- const sharp = (await import("sharp")).default;
863
1732
  const resized = width === targetW && height === targetH
864
1733
  ? image
865
- : new Uint8Array(await sharp(image)
1734
+ : new Uint8Array(await (await loadSharp())(image)
866
1735
  .resize(targetW, targetH, { fit: "cover", position: "centre" })
867
1736
  .png()
868
1737
  .toBuffer());
@@ -874,11 +1743,15 @@ export class OpenAIProvider extends BaseProvider {
874
1743
  }),
875
1744
  size: size,
876
1745
  seconds: String(seconds)
877
- });
1746
+ }, { signal: params.signal });
878
1747
  if (!video?.id) {
879
1748
  throw new Error("OpenAI image-to-video create response did not contain a video id");
880
1749
  }
881
- const timeoutMs = 10 * 60 * 1000;
1750
+ const timeoutMs = params.timeoutSeconds != null &&
1751
+ Number.isFinite(params.timeoutSeconds) &&
1752
+ params.timeoutSeconds > 0
1753
+ ? params.timeoutSeconds * 1000
1754
+ : 10 * 60 * 1000;
882
1755
  const intervalMs = 2000;
883
1756
  const start = Date.now();
884
1757
  let latest = video;
@@ -886,8 +1759,17 @@ export class OpenAIProvider extends BaseProvider {
886
1759
  if (Date.now() - start > timeoutMs) {
887
1760
  throw new Error("Image-to-video generation timed out");
888
1761
  }
889
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
890
- latest = await this.getClient().videos.retrieve(video.id);
1762
+ const remainingMs = timeoutMs - (Date.now() - start);
1763
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remainingMs)));
1764
+ if (Date.now() - start >= timeoutMs) {
1765
+ throw new Error("Image-to-video generation timed out");
1766
+ }
1767
+ if (params.signal?.aborted) {
1768
+ throw new Error("Image-to-video generation cancelled");
1769
+ }
1770
+ latest = await this.getClient().videos.retrieve(video.id, {
1771
+ signal: params.signal
1772
+ });
891
1773
  }
892
1774
  if (latest.status !== "completed") {
893
1775
  throw new Error(String(latest.error ??
@@ -901,13 +1783,16 @@ export class OpenAIProvider extends BaseProvider {
901
1783
  if (input.length === 0 || input.every((v) => !v)) {
902
1784
  throw new Error("text must not be empty");
903
1785
  }
904
- const response = await this.getClient().embeddings.create({
905
- model: args.model,
906
- input,
907
- ...(args.dimensions ? { dimensions: args.dimensions } : {})
908
- });
1786
+ const request = { model: args.model, input };
1787
+ if (args.dimensions) {
1788
+ request.dimensions = args.dimensions;
1789
+ }
1790
+ const response = await this.getClient().embeddings.create(request);
909
1791
  return response.data.map((row) => row.embedding);
910
1792
  }
1793
+ isContextExceededError(error) {
1794
+ return openAIContextExceeded(error) !== null;
1795
+ }
911
1796
  isContextLengthError(error) {
912
1797
  const msg = String(error).toLowerCase();
913
1798
  return msg.includes("context length") || msg.includes("maximum context");