@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
@@ -2,33 +2,154 @@
2
2
  * FAL AI Provider — wraps the @fal-ai/client SDK to provide image generation
3
3
  * through the standard BaseProvider interface.
4
4
  *
5
- * Supports: textToImage, imageToImage, getAvailableImageModels
5
+ * Supports: textToImage, imageToImage, getAvailableImageModels, and chat
6
+ * against fal's OpenAI-compatible LLM route.
6
7
  */
7
- import { createRequire } from "node:module";
8
+ import { currentGenerationProviderRequestOptions, recordGenerationBindingAsync, recordGenerationProviderResult, recordGenerationReceiptAsync } from "../generation-receipt.js";
8
9
  import { BaseProvider } from "./base-provider.js";
10
+ import { falGetGeneration, falListGenerations } from "./fal-generations.js";
9
11
  import { createLogger } from "@nodetool-ai/config";
10
- import { loadImageModels, loadVideoModels } from "./manifest-models.js";
12
+ import { OpenAICompatProvider } from "./openai-compat-provider.js";
13
+ import { loadImageModels, loadManifest, loadMusicModels, loadAudioToAudioModels, loadTTSModels, loadVideoModels, selectPrimaryImageInput, sizeEnumToAspect } from "./manifest-models.js";
14
+ import { validateReferenceInputs, getModelReferenceInputs } from "./manifest-models.js";
15
+ import { sniffAudioMime } from "./audio-mime.js";
16
+ import { snapToGptImage2Size } from "./gpt-image-size.js";
17
+ import { detectImageMime } from "./image-mime.js";
18
+ import { sniffMediaMime } from "./media-mime.js";
19
+ import { safeFetch } from "./safe-url.js";
20
+ import { createFalQueueOperations, falSubmitAndWait } from "./fal-queue.js";
21
+ import { isNonEmptyString, isObjectLike, isRecord, isString } from "@nodetool-ai/protocol";
11
22
  const log = createLogger("nodetool.runtime.providers.fal");
23
+ function combineSignals(signal, timeoutSeconds) {
24
+ const timeout = timeoutSeconds && timeoutSeconds > 0
25
+ ? AbortSignal.timeout(timeoutSeconds * 1000)
26
+ : undefined;
27
+ if (!signal)
28
+ return timeout;
29
+ if (!timeout)
30
+ return signal;
31
+ return AbortSignal.any([signal, timeout]);
32
+ }
12
33
  const FAL_MANIFEST_PKG = "@nodetool-ai/fal-nodes";
13
34
  const FAL_MANIFEST_PATH = "fal-manifest.json";
35
+ /** fal's OpenAI-compatible chat route (`openrouter/router`). */
36
+ const FAL_LLM_BASE_URL = "https://fal.run/openrouter/router/openai/v1";
37
+ /** Public, keyless catalog of the ids that route accepts. */
38
+ const OPENROUTER_CATALOG_URL = "https://openrouter.ai/api/v1/models";
14
39
  let _falManifestByEndpoint = null;
15
40
  function getFalManifestEntry(modelId) {
16
41
  if (!_falManifestByEndpoint) {
17
42
  _falManifestByEndpoint = new Map();
18
- try {
19
- const req = createRequire(import.meta.url);
20
- const data = req(`${FAL_MANIFEST_PKG}/${FAL_MANIFEST_PATH}`);
21
- for (const entry of data) {
22
- if (entry.endpointId)
23
- _falManifestByEndpoint.set(entry.endpointId, entry);
43
+ const data = loadManifest(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH);
44
+ for (const entry of data) {
45
+ if (entry.endpointId) {
46
+ _falManifestByEndpoint.set(entry.endpointId, entry);
24
47
  }
25
48
  }
26
- catch (err) {
27
- log.warn(`Could not load fal manifest for arg shaping: ${err}`);
28
- }
29
49
  }
30
50
  return _falManifestByEndpoint.get(modelId);
31
51
  }
52
+ // ---------------------------------------------------------------------------
53
+ // Topaz model variants
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * fal ships Topaz as one endpoint per media kind and selects the actual Topaz
57
+ * model — Standard V2, Redefine, Wonder 3 for images; Proteus, Artemis, Gaia,
58
+ * Nyx, Starlight for video — through a `model` enum on that endpoint. A single
59
+ * catalog entry per endpoint therefore offers no way to pick one, and every
60
+ * run silently gets fal's default (Standard V2 / Proteus), which is the wrong
61
+ * model for compressed sources, CGI, animation, or generative restoration.
62
+ *
63
+ * So each endpoint is expanded into one catalog entry per variant, id
64
+ * `<endpointId>/<variant>`, and the variant is split back off and sent as
65
+ * `model` when the request is built — the same shape TopazProvider uses for
66
+ * Topaz's own API.
67
+ */
68
+ const TOPAZ_UPSCALE_IMAGE = "fal-ai/topaz/upscale/image";
69
+ const TOPAZ_UPSCALE_VIDEO = "fal-ai/topaz/upscale/video";
70
+ const TOPAZ_VARIANT_ENDPOINTS = [TOPAZ_UPSCALE_IMAGE, TOPAZ_UPSCALE_VIDEO];
71
+ /** The `model` enum a Topaz endpoint declares, or [] when it isn't shipped. */
72
+ function topazVariants(endpointId) {
73
+ const field = getFalManifestEntry(endpointId)?.inputFields?.find((f) => (f.apiParamName ?? f.name) === "model");
74
+ return field?.enumValues ?? [];
75
+ }
76
+ /**
77
+ * Replace each Topaz entry with one entry per model variant. Entries whose
78
+ * endpoint declares no `model` enum are left as they are, so a manifest that
79
+ * drops the field degrades to today's single entry rather than to none.
80
+ */
81
+ function expandTopazVariants(models) {
82
+ const out = [];
83
+ for (const m of models) {
84
+ const variants = TOPAZ_VARIANT_ENDPOINTS.includes(m.id)
85
+ ? topazVariants(m.id)
86
+ : [];
87
+ if (variants.length === 0) {
88
+ out.push(m);
89
+ continue;
90
+ }
91
+ for (const variant of variants) {
92
+ out.push({
93
+ ...m,
94
+ id: `${m.id}/${variant}`,
95
+ name: `${m.name} — ${variant}`
96
+ });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * Split a catalog id back into the fal endpoint and the Topaz `model` value.
103
+ * Bare endpoint ids (saved before variants existed, or typed by hand) resolve
104
+ * to no variant, which leaves fal's own default in place.
105
+ */
106
+ function splitTopazVariant(modelId) {
107
+ for (const endpointId of TOPAZ_VARIANT_ENDPOINTS) {
108
+ const prefix = `${endpointId}/`;
109
+ if (modelId.startsWith(prefix)) {
110
+ return { endpointId, variant: modelId.slice(prefix.length) };
111
+ }
112
+ }
113
+ return { endpointId: modelId };
114
+ }
115
+ /**
116
+ * `UpscaleImageParams.creativity` is 0–1; the Topaz image endpoint takes a
117
+ * 1–6 level (Redefine only) and rejects anything else, so it is rescaled for
118
+ * that endpoint and passed through untouched everywhere else.
119
+ */
120
+ function topazCreativity(endpointId, creativity) {
121
+ if (endpointId !== TOPAZ_UPSCALE_IMAGE || creativity == null) {
122
+ return creativity;
123
+ }
124
+ const level = Math.round(Math.min(1, Math.max(0, creativity)) * 5) + 1;
125
+ return String(level);
126
+ }
127
+ /** The four per-side outpaint API names, in the order fal declares them. */
128
+ const EXPAND_SIDES = [
129
+ ["left", "expand_left"],
130
+ ["right", "expand_right"],
131
+ ["top", "expand_top"],
132
+ ["bottom", "expand_bottom"]
133
+ ];
134
+ /**
135
+ * Write the padding onto whatever `expand_*` fields the endpoint declares.
136
+ *
137
+ * The two families spell the same request differently: image endpoints take
138
+ * pixel counts (`expand_top: 256`), while the wan-vace video ones take a
139
+ * boolean per side plus one shared `expand_ratio`. A pixel count sent to the
140
+ * boolean field is a 422, so the declared type decides — a positive count
141
+ * becomes `true` there, and the ratio rides separately.
142
+ */
143
+ function setOutpaintPadding(b, padding) {
144
+ if (!padding)
145
+ return;
146
+ for (const [side, apiName] of EXPAND_SIDES) {
147
+ const pixels = padding[side];
148
+ if (pixels == null)
149
+ continue;
150
+ b.set(apiName, b.propType(apiName) === "bool" ? pixels > 0 : pixels);
151
+ }
152
+ }
32
153
  /**
33
154
  * Helper bound to one endpoint that adds keys only when the manifest declares
34
155
  * them, and attaches asset URLs to whatever field name/shape the endpoint
@@ -37,10 +158,12 @@ function getFalManifestEntry(modelId) {
37
158
  */
38
159
  class FalArgsBuilder {
39
160
  args = {};
161
+ modelId;
40
162
  fields;
41
163
  accepted;
42
164
  known;
43
165
  constructor(modelId) {
166
+ this.modelId = modelId;
44
167
  const entry = getFalManifestEntry(modelId);
45
168
  this.known = entry != null;
46
169
  this.fields = entry?.inputFields ?? [];
@@ -50,35 +173,201 @@ class FalArgsBuilder {
50
173
  has(apiName) {
51
174
  return this.known ? this.accepted.has(apiName) : true;
52
175
  }
176
+ /** True only when the manifest describes this endpoint and lists `apiName`. */
177
+ declares(apiName) {
178
+ return this.known && this.accepted.has(apiName);
179
+ }
53
180
  /** propType for a manifest field by API name, lowercased. */
54
181
  propType(apiName) {
55
182
  return this.accepted.get(apiName)?.propType.toLowerCase();
56
183
  }
57
- /** Set `apiName` to `value` only if the endpoint accepts it (or unknown). */
184
+ /**
185
+ * Set `apiName` to `value` only if the endpoint accepts it (or unknown),
186
+ * coercing to the manifest's declared type. fal returns 422 when a value's
187
+ * type doesn't match the field — notably numeric `duration`/`seed`/
188
+ * `num_frames` sent to fields the endpoint declares as `enum`/`str` (e.g.
189
+ * `duration` enums are `"5"`/`"10"`, not `5`). Enum values that aren't in
190
+ * the field's vocabulary are dropped rather than sent.
191
+ */
58
192
  set(apiName, value) {
59
193
  if (value == null)
60
194
  return this;
61
- if (typeof value === "string" && value === "")
195
+ if (isString(value) && value === "")
62
196
  return this;
63
- if (this.has(apiName))
64
- this.args[apiName] = value;
197
+ if (!this.has(apiName))
198
+ return this;
199
+ const t = this.propType(apiName);
200
+ if (t === "enum") {
201
+ const v = this.acceptedEnumValue(apiName, String(value));
202
+ if (v !== undefined)
203
+ this.args[apiName] = v;
204
+ return this;
205
+ }
206
+ if (t === "str" && !isString(value)) {
207
+ this.args[apiName] = String(value);
208
+ return this;
209
+ }
210
+ if (t === "int" || t === "float") {
211
+ const n = this.numericValue(apiName, value, t === "int");
212
+ if (n !== undefined)
213
+ this.args[apiName] = n;
214
+ return this;
215
+ }
216
+ this.args[apiName] = value;
65
217
  return this;
66
218
  }
219
+ /**
220
+ * Coerce a numeric field to what the endpoint declares: integers are
221
+ * rounded, and both kinds are clamped to the field's `min`/`max`. fal
222
+ * returns 422 for a value outside the declared range or a float sent to an
223
+ * `int` field — a 1.5 s storyboard shot sent as `duration` to an endpoint
224
+ * declaring `int` 5..15 fails the whole render. Clamping produces the
225
+ * nearest clip the model can make instead. Non-numeric values are dropped.
226
+ */
227
+ numericValue(apiName, value, isInt) {
228
+ const raw = isString(value) ? Number(value) : value;
229
+ if (typeof raw !== "number" || !Number.isFinite(raw))
230
+ return undefined;
231
+ const field = this.accepted.get(apiName);
232
+ let n = isInt ? Math.round(raw) : raw;
233
+ if (field) {
234
+ if (typeof field.min === "number" && n < field.min)
235
+ n = field.min;
236
+ if (typeof field.max === "number" && n > field.max)
237
+ n = field.max;
238
+ if (isInt)
239
+ n = Math.round(n);
240
+ }
241
+ return n;
242
+ }
67
243
  /** Same as set() but always writes (used for required canonical keys like prompt). */
68
244
  force(apiName, value) {
69
245
  if (value != null)
70
246
  this.args[apiName] = value;
71
247
  return this;
72
248
  }
249
+ /**
250
+ * Write `value` to the first name in `apiNames` the endpoint declares. Used
251
+ * where one concept has many spellings across endpoints (a segmentation text
252
+ * prompt is `prompt`, `text_prompt`, `text_input` or `object`), so writing a
253
+ * single name would drop the value on every endpoint using another.
254
+ */
255
+ setFirst(apiNames, value) {
256
+ for (const apiName of apiNames) {
257
+ if (this.has(apiName)) {
258
+ return this.set(apiName, value);
259
+ }
260
+ }
261
+ // The endpoint declares none of them — an unknown key would 422 the call,
262
+ // and the model simply does not take this prompt kind.
263
+ return this;
264
+ }
265
+ /**
266
+ * Write `value` to the first name in `apiNames` the endpoint declares, even
267
+ * when {@link set} would drop it. An empty string is a meaningful value on a
268
+ * segmentation prompt field: fal fills an absent field with the endpoint's
269
+ * own default, and sam-3.1 defaults `prompt` to `"wheel"`.
270
+ */
271
+ forceFirst(apiNames, value) {
272
+ for (const apiName of apiNames) {
273
+ if (this.has(apiName)) {
274
+ return this.force(apiName, value);
275
+ }
276
+ }
277
+ return this;
278
+ }
279
+ /**
280
+ * The `kind`-typed asset inputs the endpoint declares, as {@link
281
+ * ModelImageInput}s (apiName / isList / name), in manifest order.
282
+ */
283
+ assetInputs(kind) {
284
+ return this.fields
285
+ .filter((f) => {
286
+ const t = f.propType.toLowerCase();
287
+ return t === kind || t === `list[${kind}]`;
288
+ })
289
+ .map((f) => ({
290
+ apiName: f.apiParamName ?? f.name,
291
+ isList: f.propType.toLowerCase().startsWith("list["),
292
+ name: f.name
293
+ }));
294
+ }
295
+ /**
296
+ * Pick the field a source asset should attach to. For images this is the
297
+ * primary source field, skipping auxiliary slots (mask/control/reference/
298
+ * style/pose/end-frame, …) that must never receive the source image — the
299
+ * same {@link selectPrimaryImageInput} the model picker uses. For video/audio
300
+ * (endpoints with a single such field) it's the first declared field.
301
+ */
302
+ selectAssetField(kind, count) {
303
+ const inputs = this.assetInputs(kind);
304
+ if (inputs.length === 0)
305
+ return undefined;
306
+ if (kind === "image")
307
+ return selectPrimaryImageInput(inputs, count);
308
+ return inputs[0];
309
+ }
73
310
  /**
74
311
  * Attach an uploaded asset URL to whatever field (image/video/audio) the
75
312
  * endpoint declares. Handles list-typed fields (`list[image]` -> array).
76
313
  * If the endpoint isn't in the manifest, falls back to `${kind}_url`.
77
314
  */
78
315
  attachAsset(kind, url, fallbackApiName) {
316
+ const field = this.selectAssetField(kind, 1);
317
+ if (field) {
318
+ this.args[field.apiName] = field.isList ? [url] : url;
319
+ }
320
+ else {
321
+ this.args[fallbackApiName ?? `${kind}_url`] = url;
322
+ }
323
+ return this;
324
+ }
325
+ /**
326
+ * Attach one or more uploaded asset URLs to whatever field (image/video/
327
+ * audio) the endpoint declares. When the endpoint declares a list-typed field
328
+ * (e.g. `image_urls`), every URL is sent; otherwise only the first URL is
329
+ * attached to the single-valued field. Auxiliary image slots (mask, control,
330
+ * reference/style/end-frame, …) are skipped so the source never lands there.
331
+ * Falls back to `${kind}_url` (first URL) for endpoints not in the manifest.
332
+ */
333
+ attachAssets(kind, urls, fallbackApiName) {
334
+ if (urls.length === 0)
335
+ return this;
336
+ const field = this.selectAssetField(kind, urls.length);
337
+ if (field) {
338
+ this.args[field.apiName] = field.isList ? urls : urls[0];
339
+ }
340
+ else {
341
+ this.args[fallbackApiName ?? `${kind}_url`] = urls[0];
342
+ }
343
+ return this;
344
+ }
345
+ referenceAssetInputs(kind) {
346
+ return getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, this.modelId)
347
+ .filter((field) => field.kind === kind)
348
+ .map(({ kind: _kind, ...field }) => field);
349
+ }
350
+ attachReferenceAssets(kind, urls) {
351
+ if (urls.length === 0)
352
+ return this;
353
+ const fields = this.referenceAssetInputs(kind);
354
+ const field = fields[0];
355
+ if (!field)
356
+ return this;
357
+ this.args[field.apiName] = field.isList ? urls : urls[0];
358
+ return this;
359
+ }
360
+ /**
361
+ * Attach an inpaint mask URL to whatever mask field the endpoint declares.
362
+ * Endpoints name it `mask_url`, `mask_image_url`, `static_mask_url`, etc. —
363
+ * all `image`-typed fields whose name contains "mask". Falls back to
364
+ * `mask_url` for endpoints not in the manifest.
365
+ */
366
+ attachMask(url) {
79
367
  const field = this.fields.find((f) => {
80
368
  const t = f.propType.toLowerCase();
81
- return t === kind || t === `list[${kind}]`;
369
+ const apiName = (f.apiParamName ?? f.name).toLowerCase();
370
+ return (t === "image" || t === "list[image]") && apiName.includes("mask");
82
371
  });
83
372
  if (field) {
84
373
  const apiName = field.apiParamName ?? field.name;
@@ -87,7 +376,7 @@ class FalArgsBuilder {
87
376
  : url;
88
377
  }
89
378
  else {
90
- this.args[fallbackApiName ?? `${kind}_url`] = url;
379
+ this.args.mask_url = url;
91
380
  }
92
381
  return this;
93
382
  }
@@ -99,11 +388,18 @@ class FalArgsBuilder {
99
388
  setImageSize(width, height) {
100
389
  if (!width || !height)
101
390
  return this;
391
+ if (this.modelId.startsWith("openai/gpt-image-2.5/")) {
392
+ const size = snapToGptImage2Size(width, height);
393
+ if (size && this.field("image_size")?.acceptsObject) {
394
+ this.args.image_size = { width: size[0], height: size[1] };
395
+ }
396
+ return this;
397
+ }
102
398
  const t = this.propType("image_size");
103
399
  if (!this.known) {
104
400
  this.args.image_size = { width, height };
105
401
  }
106
- else if (t && t !== "enum") {
402
+ else if (t && (t !== "enum" || this.field("image_size")?.acceptsObject)) {
107
403
  this.args.image_size = { width, height };
108
404
  }
109
405
  return this;
@@ -124,12 +420,21 @@ class FalArgsBuilder {
124
420
  return undefined;
125
421
  if (field.propType.toLowerCase() !== "enum")
126
422
  return value;
127
- const enumValues = field
128
- .enumValues;
423
+ const enumValues = field.enumValues;
129
424
  if (!enumValues || enumValues.length === 0)
130
425
  return value;
131
426
  return enumValues.includes(value) ? value : undefined;
132
427
  }
428
+ field(apiName) {
429
+ return this.accepted.get(apiName);
430
+ }
431
+ /** Declared enum vocabulary for an enum-typed field, or undefined. */
432
+ enumValuesOf(apiName) {
433
+ const field = this.accepted.get(apiName);
434
+ if (!field || field.propType.toLowerCase() !== "enum")
435
+ return undefined;
436
+ return field.enumValues;
437
+ }
133
438
  /**
134
439
  * Route an aspect ratio + resolution pair to whichever of the endpoint's
135
440
  * size-shaping enum fields it actually declares: `aspect_ratio`,
@@ -146,16 +451,21 @@ class FalArgsBuilder {
146
451
  const res = this.acceptedEnumValue("resolution", resolution);
147
452
  if (res)
148
453
  this.args.resolution = res;
149
- // `video_size` / `image_size` enums share the orientation_NxM vocabulary
150
- // (square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, ...).
151
- // Derive from aspectRatio rather than asking the caller for yet another
152
- // distinct knob.
153
- const sized = aspectRatioToSizeEnum(aspectRatio);
454
+ // `video_size` / `image_size` are enums whose vocabulary varies per endpoint
455
+ // (square_hd, landscape_16_9, landscape_3_2, 1536x1024, ...). Pick the value
456
+ // the endpoint ITSELF declares whose ratio matches `aspectRatio`, using the
457
+ // same size→aspect map the model picker derives its options from — so any
458
+ // ratio the picker offers round-trips to a value fal accepts. Unknown
459
+ // endpoints (no declared enum) fall back to the canonical short list.
154
460
  for (const apiName of ["video_size", "image_size"]) {
155
461
  // Only set if not already set (e.g. setImageSize() may have written
156
462
  // image_size as a dict for endpoints that accept that shape).
157
463
  if (apiName in this.args)
158
464
  continue;
465
+ const declared = this.enumValuesOf(apiName);
466
+ const sized = declared
467
+ ? pickSizeEnumForAspect(declared, aspectRatio)
468
+ : aspectRatioToSizeEnum(aspectRatio);
159
469
  const v = this.acceptedEnumValue(apiName, sized);
160
470
  if (v)
161
471
  this.args[apiName] = v;
@@ -188,15 +498,54 @@ function aspectRatioToSizeEnum(aspectRatio) {
188
498
  return undefined;
189
499
  }
190
500
  }
501
+ /**
502
+ * Given the size-enum values an endpoint declares, pick the one whose aspect
503
+ * ratio matches `aspectRatio` (via {@link sizeEnumToAspect} — the same map the
504
+ * model picker uses to build its option list, so extraction and request-shaping
505
+ * agree). When several declared values share the ratio (e.g. `square` and
506
+ * `square_hd`), prefer the higher-resolution variant. Returns undefined when
507
+ * the endpoint declares no value for that ratio.
508
+ */
509
+ function pickSizeEnumForAspect(declared, aspectRatio) {
510
+ if (!aspectRatio)
511
+ return undefined;
512
+ const matches = declared.filter((v) => sizeEnumToAspect(v) === aspectRatio);
513
+ if (matches.length === 0)
514
+ return undefined;
515
+ return matches.sort((a, b) => sizeEnumRank(b) - sizeEnumRank(a))[0];
516
+ }
517
+ /** Rough quality ordering so "square_hd" beats "square", "…_uhd" beats both. */
518
+ function sizeEnumRank(value) {
519
+ const v = value.toLowerCase();
520
+ if (v.includes("uhd"))
521
+ return 3;
522
+ if (v.includes("hd"))
523
+ return 2;
524
+ const dims = v.match(/(\d+)x(\d+)/);
525
+ if (dims)
526
+ return 1 + (Number(dims[1]) * Number(dims[2])) / 1e9;
527
+ return 1;
528
+ }
191
529
  export class FalProvider extends BaseProvider {
192
530
  apiKey;
193
531
  _client = null;
532
+ _chat = null;
533
+ _fetch;
534
+ _webhookUrl;
535
+ _onQueueAccepted;
536
+ _onQueueBound;
537
+ /** Model ids the catalog says accept `tools`. Filled by the model listing. */
538
+ _toolCapableModels = null;
194
539
  static requiredSecrets() {
195
540
  return ["FAL_API_KEY"];
196
541
  }
197
- constructor(secrets = {}) {
542
+ constructor(secrets = {}, options = {}) {
198
543
  super("fal_ai");
199
544
  this.apiKey = secrets["FAL_API_KEY"] ?? "";
545
+ this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
546
+ this._webhookUrl = options.webhookUrl;
547
+ this._onQueueAccepted = options.onQueueAccepted;
548
+ this._onQueueBound = options.onQueueBound;
200
549
  }
201
550
  /** Build an onQueueUpdate callback that forwards progress via emitMessage. */
202
551
  makeQueueUpdateHandler() {
@@ -233,27 +582,324 @@ export class FalProvider extends BaseProvider {
233
582
  if (this._client)
234
583
  return this._client;
235
584
  const { createFalClient } = await import("@fal-ai/client");
236
- this._client = createFalClient({
585
+ const client = createFalClient({
237
586
  credentials: this.apiKey
238
587
  });
588
+ // The SDK's subscribe() hides submit/bind/wait and enables automatic POST
589
+ // retries. Use the explicit adapter when this is a queue-capable SDK. The
590
+ // fallback keeps injected legacy clients and older hosts compatible.
591
+ const queueCapable = typeof client === "object" &&
592
+ client !== null &&
593
+ "queue" in client &&
594
+ typeof client.queue === "object" &&
595
+ client.queue !== null;
596
+ this._client = {
597
+ subscribe: async (endpoint, opts) => {
598
+ if (!queueCapable) {
599
+ const result = await client.subscribe(endpoint, opts);
600
+ recordGenerationProviderResult(result.data ?? result);
601
+ return result;
602
+ }
603
+ const operations = createFalQueueOperations({
604
+ apiKey: this.apiKey,
605
+ fetchFn: this._fetch
606
+ });
607
+ const request = {
608
+ endpoint,
609
+ input: opts.input
610
+ };
611
+ const providerRequestOptions = currentGenerationProviderRequestOptions();
612
+ if (opts.abortSignal)
613
+ request.signal = opts.abortSignal;
614
+ const webhookUrl = this._webhookUrl ?? providerRequestOptions?.webhookUrl;
615
+ if (webhookUrl)
616
+ request.webhookUrl = webhookUrl;
617
+ const result = await falSubmitAndWait(operations, request, {
618
+ signal: opts.abortSignal,
619
+ onAccepted: (submission) => {
620
+ // The id is recorded immediately after the non-retried POST and
621
+ // before any status/result wait, so a later wait timeout preserves
622
+ // the provider identity for a host lifecycle adapter.
623
+ return (async () => {
624
+ await recordGenerationReceiptAsync({ provider_request_id: submission.providerRequestId }, submission);
625
+ await this._onQueueAccepted?.(submission);
626
+ })();
627
+ },
628
+ onBound: async (binding) => {
629
+ await recordGenerationBindingAsync(binding);
630
+ await this._onQueueBound?.(binding);
631
+ },
632
+ onUpdate: (observation) => {
633
+ const status = typeof observation.rawStatus === "object" &&
634
+ observation.rawStatus !== null &&
635
+ "status" in observation.rawStatus &&
636
+ typeof observation.rawStatus.status === "string"
637
+ ? observation.rawStatus.status
638
+ : observation.state === "queued"
639
+ ? "IN_QUEUE"
640
+ : observation.state === "running"
641
+ ? "IN_PROGRESS"
642
+ : "COMPLETED";
643
+ const logs = typeof observation.rawStatus === "object" &&
644
+ observation.rawStatus !== null &&
645
+ "logs" in observation.rawStatus &&
646
+ Array.isArray(observation.rawStatus.logs)
647
+ ? observation.rawStatus.logs
648
+ : undefined;
649
+ opts.onQueueUpdate?.({
650
+ status,
651
+ logs: logs?.filter((entry) => typeof entry === "object" &&
652
+ entry !== null &&
653
+ "message" in entry &&
654
+ typeof entry.message === "string")
655
+ });
656
+ }
657
+ });
658
+ recordGenerationProviderResult(result.data);
659
+ return { data: result.data, requestId: result.requestId };
660
+ },
661
+ storage: client.storage
662
+ };
239
663
  return this._client;
240
664
  }
665
+ /** How the platform-API client reaches FAL: this provider's fetch, its signal. */
666
+ generationsOptions(signal) {
667
+ const options = { fetchFn: this._fetch };
668
+ if (signal)
669
+ options.signal = signal;
670
+ return options;
671
+ }
672
+ /**
673
+ * FAL's own record of this account's requests, through its Platform APIs —
674
+ * generations this installation never ran included, at the cost FAL billed.
675
+ * See `fal-generations.ts` for which endpoint answers what.
676
+ */
677
+ async listGenerations(query = {}) {
678
+ return falListGenerations(this.apiKey, query, this.generationsOptions(query.signal));
679
+ }
680
+ /** One FAL request by its request id — the id the local row keeps. */
681
+ async getGeneration(requestId, options = {}) {
682
+ return falGetGeneration(this.apiKey, requestId, options, this.generationsOptions(options.signal));
683
+ }
241
684
  async getAvailableImageModels() {
242
- return loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
685
+ return expandTopazVariants(loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
243
686
  }
244
687
  async getAvailableVideoModels() {
245
- return loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
688
+ return expandTopazVariants(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
246
689
  }
247
- async generateMessage(_args) {
248
- throw new Error("fal_ai does not support chat generation");
690
+ async getAvailableTTSModels() {
691
+ return loadTTSModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
249
692
  }
250
- async *generateMessages(_args) {
251
- throw new Error("fal_ai does not support chat generation");
693
+ async getAvailableMusicModels() {
694
+ return loadMusicModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
695
+ }
696
+ async getAvailableAudioToAudioModels() {
697
+ return loadAudioToAudioModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
698
+ }
699
+ /**
700
+ * Rewrite a recording. The source audio is uploaded and attached to whatever
701
+ * audio field the endpoint declares; the optional direction fields are only
702
+ * written where the manifest declares them, so a voice changer that takes a
703
+ * `voice` and a separator that takes nothing both work off one call.
704
+ */
705
+ async audioToAudio(audio, params) {
706
+ const modelId = params.model.id;
707
+ const url = await this.upload(audio, "audio/mpeg");
708
+ const b = new FalArgsBuilder(modelId);
709
+ b.attachAsset("audio", url)
710
+ .set("prompt", params.prompt)
711
+ .set("voice", params.voice)
712
+ .set("strength", params.strength);
713
+ if (params.seed != null && params.seed !== -1)
714
+ b.set("seed", params.seed);
715
+ this.recordRequestPayload(b.args);
716
+ log.debug("FAL audioToAudio", { model: modelId });
717
+ const client = await this.getClient();
718
+ const result = await client.subscribe(modelId, {
719
+ input: b.args,
720
+ logs: true,
721
+ onQueueUpdate: this.makeQueueUpdateHandler()
722
+ });
723
+ const data = (result.data ?? result);
724
+ const bytes = await downloadBytes(extractAudioUrl(data));
725
+ return { data: bytes, mimeType: sniffAudioMime(bytes) };
726
+ }
727
+ /**
728
+ * Generate music as an encoded audio file. FAL music endpoints (Stable Audio,
729
+ * MiniMax Music, ACE-Step, DiffRhythm, …) return a file URL, so this mirrors
730
+ * {@link textToSpeechEncoded}: shape the request from the manifest, run it,
731
+ * and download the resulting audio bytes.
732
+ */
733
+ async textToMusic(params) {
734
+ if (!params.prompt)
735
+ throw new Error("prompt must not be empty");
736
+ const client = await this.getClient();
737
+ const modelId = params.model.id;
738
+ const input = this.buildTextToMusicArgs(modelId, params);
739
+ this.recordRequestPayload(input);
740
+ log.debug("FAL textToMusic", { model: modelId });
741
+ const result = await client.subscribe(modelId, {
742
+ input,
743
+ logs: true,
744
+ onQueueUpdate: this.makeQueueUpdateHandler()
745
+ });
746
+ const data = (result.data ?? result);
747
+ const bytes = await downloadBytes(extractAudioUrl(data));
748
+ return { data: bytes, mimeType: sniffAudioMime(bytes) };
749
+ }
750
+ /**
751
+ * Shape music inputs for whatever the endpoint declares. The text prompt
752
+ * lands in `prompt`; lyrics (for vocal models) and duration land in whichever
753
+ * of the heterogeneous duration/lyrics fields the endpoint accepts.
754
+ * {@link FalArgsBuilder.set} drops keys the endpoint doesn't declare.
755
+ */
756
+ buildTextToMusicArgs(modelId, params) {
757
+ const b = new FalArgsBuilder(modelId);
758
+ b.force("prompt", params.prompt)
759
+ .set("style_prompt", params.prompt)
760
+ .set("lyrics", params.lyrics)
761
+ .set("lyrics_prompt", params.lyrics);
762
+ if (params.durationSeconds != null) {
763
+ b.set("duration", params.durationSeconds)
764
+ .set("seconds_total", params.durationSeconds)
765
+ .set("music_duration", params.durationSeconds);
766
+ if (b.declares("music_length_ms")) {
767
+ // ElevenLabs expects an integer, despite the optional field's string
768
+ // type in the generated manifest: https://fal.ai/models/fal-ai/elevenlabs/music/api
769
+ b.force("music_length_ms", Math.round(params.durationSeconds * 1000));
770
+ }
771
+ }
772
+ if (params.seed != null && params.seed !== -1)
773
+ b.set("seed", params.seed);
774
+ return b.args;
775
+ }
776
+ /**
777
+ * Generate speech as an encoded audio file (mp3/wav, depending on the
778
+ * endpoint). FAL TTS endpoints return a file URL rather than raw PCM, so the
779
+ * unified TTS node consumes this encoded path instead of streaming samples.
780
+ */
781
+ async textToSpeechEncoded(args) {
782
+ if (!args.text)
783
+ throw new Error("text must not be empty");
784
+ const client = await this.getClient();
785
+ const input = this.buildTextToSpeechArgs(args.model, args);
786
+ log.debug("FAL textToSpeech", { model: args.model });
787
+ const result = await client.subscribe(args.model, {
788
+ input,
789
+ logs: true,
790
+ onQueueUpdate: this.makeQueueUpdateHandler()
791
+ });
792
+ const data = (result.data ?? result);
793
+ const bytes = await downloadBytes(extractAudioUrl(data));
794
+ return { data: bytes, mimeType: sniffAudioMime(bytes) };
795
+ }
796
+ /**
797
+ * Shape TTS inputs for whatever the endpoint declares. The spoken text lands
798
+ * in the first text-like field the endpoint accepts; the voice lands in
799
+ * `voice` / `speaker` when enumerated. {@link FalArgsBuilder.set} drops keys
800
+ * the endpoint doesn't declare, so this works across the heterogeneous TTS
801
+ * catalog.
802
+ */
803
+ buildTextToSpeechArgs(modelId, params) {
804
+ const b = new FalArgsBuilder(modelId);
805
+ if (b.has("text")) {
806
+ b.set("text", params.text);
807
+ }
808
+ else {
809
+ b.set("prompt", params.text)
810
+ .set("script", params.text)
811
+ .set("gen_text", params.text)
812
+ .set("input", params.text);
813
+ }
814
+ b.set("voice", params.voice).set("speaker", params.voice);
815
+ if (params.speed != null)
816
+ b.set("speed", params.speed);
817
+ b.set("language", params.language).set("instructions", params.instructions);
818
+ return b.args;
819
+ }
820
+ /**
821
+ * fal's chat surface is an OpenAI-compatible route
822
+ * ({@link FAL_LLM_BASE_URL}) backed by OpenRouter, so it speaks the same
823
+ * dialect every other gateway provider here does — with one difference: fal
824
+ * authenticates with `Key <FAL_KEY>`, not `Bearer`, and answers 401 to the
825
+ * Bearer form. The compat client spreads `defaultHeaders` after its own
826
+ * Authorization, so this overrides it.
827
+ */
828
+ chatProvider() {
829
+ if (this._chat)
830
+ return this._chat;
831
+ if (!this.apiKey) {
832
+ throw new Error("FAL_API_KEY is required for chat generation");
833
+ }
834
+ const chat = new OpenAICompatProvider({
835
+ providerId: "fal_ai",
836
+ apiKey: this.apiKey,
837
+ baseURL: FAL_LLM_BASE_URL,
838
+ defaultHeaders: { Authorization: `Key ${this.apiKey}` }
839
+ }, { fetchFn: this._fetch });
840
+ chat.setMessageEmitter((msg) => this.emitMessage(msg));
841
+ this._chat = chat;
842
+ return chat;
843
+ }
844
+ /**
845
+ * Language models reachable through fal's OpenAI-compatible route. The route
846
+ * is OpenRouter's router, so the ids it accepts are OpenRouter's ids and its
847
+ * public catalog is the model list — fal itself publishes no `/models` for
848
+ * this endpoint. Unreachable catalog means no language models rather than a
849
+ * failed provider, matching how the other enumerating providers fail.
850
+ */
851
+ async getAvailableLanguageModels() {
852
+ let rows;
853
+ try {
854
+ const response = await this._fetch(OPENROUTER_CATALOG_URL);
855
+ if (!response.ok)
856
+ return [];
857
+ rows = decodeFalLanguageCatalog(await response.json());
858
+ }
859
+ catch (err) {
860
+ log.warn(`Could not load the fal language-model catalog: ${err}`);
861
+ return [];
862
+ }
863
+ const toolCapable = new Set();
864
+ const models = [];
865
+ for (const row of rows) {
866
+ if (!isNonEmptyString(row.id))
867
+ continue;
868
+ if (row.supported_parameters?.includes("tools"))
869
+ toolCapable.add(row.id);
870
+ models.push({ id: row.id, name: row.name ?? row.id, provider: "fal_ai" });
871
+ }
872
+ this._toolCapableModels = toolCapable;
873
+ return models;
874
+ }
875
+ /**
876
+ * Tool support varies model by model across a 300-model catalog, so answer
877
+ * from what the catalog declared. Before any listing there is nothing to
878
+ * answer from — say yes, as the base class does, rather than hiding tools
879
+ * from a model that has them.
880
+ */
881
+ async hasToolSupport(model) {
882
+ return this._toolCapableModels?.has(model) ?? true;
883
+ }
884
+ async generateMessage(args) {
885
+ return this.chatProvider().generateMessage(args);
886
+ }
887
+ async *generateMessages(args) {
888
+ yield* this.chatProvider().generateMessages(args);
889
+ }
890
+ /** Chat spend accrues on the delegate; report it as this provider's own. */
891
+ get cost() {
892
+ return super.cost + (this._chat?.cost ?? 0);
893
+ }
894
+ resetCost() {
895
+ super.resetCost();
896
+ this._chat?.resetCost();
252
897
  }
253
898
  buildTextToImageArgs(modelId, params) {
254
899
  const b = new FalArgsBuilder(modelId);
255
900
  b.force("prompt", params.prompt)
256
901
  .set("output_format", "png")
902
+ .set("quality", params.quality)
257
903
  .set("negative_prompt", params.negativePrompt)
258
904
  .set("guidance_scale", params.guidanceScale)
259
905
  .set("num_inference_steps", params.numInferenceSteps)
@@ -263,14 +909,17 @@ export class FalProvider extends BaseProvider {
263
909
  b.set("seed", params.seed);
264
910
  return b.args;
265
911
  }
266
- async buildImageToImageArgs(modelId, image, params) {
267
- const client = await this.getClient();
268
- const blob = new Blob([image], { type: "image/png" });
269
- const uploadedUrl = await client.storage.upload(blob);
912
+ /**
913
+ * Configure a {@link FalArgsBuilder} with the shared image-edit inputs
914
+ * (source images + prompt + sampling knobs). Used by both image-to-image and
915
+ * inpaint; the latter additionally attaches a mask.
916
+ */
917
+ buildEditBuilder(modelId, imageUrls, params) {
270
918
  const b = new FalArgsBuilder(modelId);
271
- b.attachAsset("image", uploadedUrl)
919
+ b.attachAssets("image", imageUrls)
272
920
  .force("prompt", params.prompt)
273
921
  .set("output_format", "png")
922
+ .set("quality", params.quality)
274
923
  .set("negative_prompt", params.negativePrompt)
275
924
  .set("guidance_scale", params.guidanceScale)
276
925
  .set("num_inference_steps", params.numInferenceSteps)
@@ -279,6 +928,23 @@ export class FalProvider extends BaseProvider {
279
928
  .setSize(params.aspectRatio, params.resolution);
280
929
  if (params.seed != null && params.seed !== -1)
281
930
  b.set("seed", params.seed);
931
+ return b;
932
+ }
933
+ async buildImageToImageArgs(modelId, images, params) {
934
+ const urls = await this.uploadImages(images);
935
+ return this.buildEditBuilder(modelId, urls, params).args;
936
+ }
937
+ /**
938
+ * Build image-to-image args plus the inpaint mask. The mask is uploaded and
939
+ * attached to whatever mask field the endpoint declares (`mask_url`,
940
+ * `mask_image_url`, …) via {@link FalArgsBuilder.attachMask}.
941
+ */
942
+ async buildInpaintArgs(modelId, images, params) {
943
+ const urls = await this.uploadImages(images);
944
+ const b = this.buildEditBuilder(modelId, urls, params);
945
+ const [maskUrl] = await this.uploadImages([params.mask]);
946
+ if (maskUrl)
947
+ b.attachMask(maskUrl);
282
948
  return b.args;
283
949
  }
284
950
  buildTextToVideoArgs(modelId, params) {
@@ -293,12 +959,10 @@ export class FalProvider extends BaseProvider {
293
959
  b.set("seed", params.seed);
294
960
  return b.args;
295
961
  }
296
- async buildImageToVideoArgs(modelId, image, params) {
297
- const client = await this.getClient();
298
- const blob = new Blob([image], { type: "image/png" });
299
- const uploadedUrl = await client.storage.upload(blob);
962
+ async buildImageToVideoArgs(modelId, images, params) {
963
+ const urls = await this.uploadImages(images);
300
964
  const b = new FalArgsBuilder(modelId);
301
- b.attachAsset("image", uploadedUrl)
965
+ b.attachAssets("image", urls)
302
966
  .set("prompt", params.prompt)
303
967
  .set("negative_prompt", params.negativePrompt)
304
968
  .set("num_frames", params.numFrames)
@@ -313,20 +977,38 @@ export class FalProvider extends BaseProvider {
313
977
  const client = await this.getClient();
314
978
  const modelId = params.model.id;
315
979
  const args = this.buildTextToImageArgs(modelId, params);
980
+ this.recordRequestPayload(args);
316
981
  log.debug("FAL textToImage", { model: modelId });
317
982
  const result = await client.subscribe(modelId, {
318
983
  input: args,
319
984
  logs: true,
320
- onQueueUpdate: this.makeQueueUpdateHandler()
985
+ onQueueUpdate: this.makeQueueUpdateHandler(),
986
+ abortSignal: params.signal
321
987
  });
322
988
  const data = (result.data ?? result);
323
989
  return downloadBytes(extractImageUrl(data));
324
990
  }
325
- async imageToImage(image, params) {
991
+ async imageToImage(images, params) {
326
992
  const client = await this.getClient();
327
993
  const modelId = params.model.id;
328
- const args = await this.buildImageToImageArgs(modelId, image, params);
994
+ const args = await this.buildImageToImageArgs(modelId, images, params);
995
+ this.recordRequestPayload(args);
329
996
  log.debug("FAL imageToImage", { model: modelId });
997
+ const result = await client.subscribe(modelId, {
998
+ input: args,
999
+ logs: true,
1000
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1001
+ abortSignal: params.signal
1002
+ });
1003
+ const data = (result.data ?? result);
1004
+ return downloadBytes(extractImageUrl(data));
1005
+ }
1006
+ async inpaint(images, params) {
1007
+ const client = await this.getClient();
1008
+ const modelId = params.model.id;
1009
+ const args = await this.buildInpaintArgs(modelId, images, params);
1010
+ this.recordRequestPayload(args);
1011
+ log.debug("FAL inpaint", { model: modelId });
330
1012
  const result = await client.subscribe(modelId, {
331
1013
  input: args,
332
1014
  logs: true,
@@ -335,38 +1017,77 @@ export class FalProvider extends BaseProvider {
335
1017
  const data = (result.data ?? result);
336
1018
  return downloadBytes(extractImageUrl(data));
337
1019
  }
1020
+ async inpaintImages(images, params, numImages) {
1021
+ if (numImages <= 1) {
1022
+ return [await this.inpaint(images, params)];
1023
+ }
1024
+ const client = await this.getClient();
1025
+ const modelId = params.model.id;
1026
+ const args = await this.buildInpaintArgs(modelId, images, params);
1027
+ if (new FalArgsBuilder(modelId).has("num_images")) {
1028
+ args.num_images = numImages;
1029
+ }
1030
+ this.recordRequestPayload(args);
1031
+ log.debug("FAL inpaintImages", { model: modelId, numImages });
1032
+ const result = await client.subscribe(modelId, {
1033
+ input: args,
1034
+ logs: true,
1035
+ onQueueUpdate: this.makeQueueUpdateHandler()
1036
+ });
1037
+ const data = (result.data ?? result);
1038
+ const urls = extractImageUrls(data);
1039
+ return Promise.all(urls.map((url) => downloadBytes(url)));
1040
+ }
1041
+ async outpaintImage(images, params) {
1042
+ const modelId = params.model.id;
1043
+ const urls = await this.uploadImages(images);
1044
+ const b = new FalArgsBuilder(modelId);
1045
+ b.attachAssets("image", urls)
1046
+ .set("prompt", params.prompt)
1047
+ .set("negative_prompt", params.negativePrompt)
1048
+ .setSize(params.aspectRatio, null);
1049
+ setOutpaintPadding(b, params.padding);
1050
+ if (params.seed != null && params.seed !== -1)
1051
+ b.set("seed", params.seed);
1052
+ log.debug("FAL outpaintImage", { model: modelId });
1053
+ return this.runImageEndpoint(modelId, b.args);
1054
+ }
338
1055
  async textToVideo(params) {
339
1056
  const client = await this.getClient();
340
1057
  const modelId = params.model.id;
341
1058
  const args = this.buildTextToVideoArgs(modelId, params);
1059
+ this.recordRequestPayload(args);
342
1060
  log.debug("FAL textToVideo", { model: modelId });
343
1061
  const result = await client.subscribe(modelId, {
344
1062
  input: args,
345
1063
  logs: true,
346
- onQueueUpdate: this.makeQueueUpdateHandler()
1064
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1065
+ abortSignal: params.signal
347
1066
  });
348
1067
  const data = (result.data ?? result);
349
1068
  return downloadBytes(extractVideoUrl(data));
350
1069
  }
351
- async imageToImages(image, params, numImages) {
1070
+ async imageToImages(images, params, numImages) {
352
1071
  if (numImages <= 1) {
353
- return [await this.imageToImage(image, params)];
1072
+ return [await this.imageToImage(images, params)];
354
1073
  }
355
1074
  const client = await this.getClient();
356
1075
  const modelId = params.model.id;
357
- const args = await this.buildImageToImageArgs(modelId, image, params);
1076
+ const args = await this.buildImageToImageArgs(modelId, images, params);
358
1077
  if (new FalArgsBuilder(modelId).has("num_images")) {
359
1078
  args.num_images = numImages;
360
1079
  }
1080
+ this.recordRequestPayload(args);
361
1081
  log.debug("FAL imageToImages", { model: modelId, numImages });
362
1082
  const result = await client.subscribe(modelId, {
363
1083
  input: args,
364
1084
  logs: true,
365
- onQueueUpdate: this.makeQueueUpdateHandler()
1085
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1086
+ abortSignal: params.signal
366
1087
  });
367
1088
  const data = (result.data ?? result);
368
1089
  const urls = extractImageUrls(data);
369
- return Promise.all(urls.map(downloadBytes));
1090
+ return Promise.all(urls.map((url) => downloadBytes(url)));
370
1091
  }
371
1092
  async textToImages(params, numImages) {
372
1093
  if (numImages <= 1) {
@@ -378,31 +1099,359 @@ export class FalProvider extends BaseProvider {
378
1099
  if (new FalArgsBuilder(modelId).has("num_images")) {
379
1100
  args.num_images = numImages;
380
1101
  }
1102
+ this.recordRequestPayload(args);
381
1103
  log.debug("FAL textToImages", { model: modelId, numImages });
382
1104
  const result = await client.subscribe(modelId, {
383
1105
  input: args,
384
1106
  logs: true,
385
- onQueueUpdate: this.makeQueueUpdateHandler()
1107
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1108
+ abortSignal: params.signal
386
1109
  });
387
1110
  const data = (result.data ?? result);
388
1111
  const urls = extractImageUrls(data);
389
- return Promise.all(urls.map(downloadBytes));
1112
+ return Promise.all(urls.map((url) => downloadBytes(url)));
390
1113
  }
391
1114
  async imageToVideo(image, params) {
1115
+ if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
1116
+ .find((m) => m.id === params.model.id)
1117
+ ?.supportedTasks?.includes("image_to_video") ?? false))
1118
+ throw new Error(`FAL model ${params.model.id} does not support image_to_video`);
392
1119
  const client = await this.getClient();
393
1120
  const modelId = params.model.id;
394
- const args = await this.buildImageToVideoArgs(modelId, image, params);
1121
+ const args = await this.buildImageToVideoArgs(modelId, [image], params);
1122
+ this.recordRequestPayload(args);
395
1123
  log.debug("FAL imageToVideo", { model: modelId });
396
1124
  const result = await client.subscribe(modelId, {
397
1125
  input: args,
398
1126
  logs: true,
399
- onQueueUpdate: this.makeQueueUpdateHandler()
1127
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1128
+ abortSignal: params.signal
400
1129
  });
401
1130
  const data = (result.data ?? result);
402
1131
  return downloadBytes(extractVideoUrl(data));
403
1132
  }
1133
+ async referenceToVideo(inputs, params) {
1134
+ const images = inputs.images.filter((b) => b.length > 0);
1135
+ const videos = inputs.videos.filter((b) => b.length > 0);
1136
+ if (images.length === 0 && videos.length === 0) {
1137
+ throw new Error("reference_to_video requires at least one reference image or video");
1138
+ }
1139
+ const modelId = params.model.id;
1140
+ const builder = new FalArgsBuilder(modelId);
1141
+ if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
1142
+ .find((m) => m.id === modelId)
1143
+ ?.supportedTasks?.includes("reference_to_video") ?? false))
1144
+ throw new Error(`FAL model ${modelId} does not support reference_to_video`);
1145
+ validateReferenceInputs("FAL", modelId, inputs, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, modelId));
1146
+ const imageFields = builder.referenceAssetInputs("image");
1147
+ const videoFields = builder.referenceAssetInputs("video");
1148
+ if (images.length > 0 && imageFields.length === 0)
1149
+ throw new Error(`FAL model ${modelId} does not support reference images`);
1150
+ if (videos.length > 0 && videoFields.length === 0)
1151
+ throw new Error(`FAL model ${modelId} does not support reference videos`);
1152
+ if (params.useReferenceVideoAudio === true && videos.length === 0)
1153
+ throw new Error(`FAL model ${modelId} requires a reference video when audio is enabled`);
1154
+ if (params.useReferenceVideoAudio === true &&
1155
+ !builder.has("use_reference_video_audio"))
1156
+ throw new Error(`FAL model ${modelId} does not support reference video audio`);
1157
+ const signal = combineSignals(params.signal, params.timeoutSeconds);
1158
+ signal?.throwIfAborted();
1159
+ const imageUrls = await Promise.all(images.map((b) => this.upload(b, detectImageMime(b))));
1160
+ const videoUrls = await Promise.all(videos.map((b) => this.upload(b, sniffMediaMime(b, "video/mp4"))));
1161
+ signal?.throwIfAborted();
1162
+ builder
1163
+ .attachReferenceAssets("image", imageUrls)
1164
+ .attachReferenceAssets("video", videoUrls)
1165
+ .set("prompt", params.prompt)
1166
+ .set("negative_prompt", params.negativePrompt)
1167
+ .set("duration", params.durationSeconds)
1168
+ .setSize(params.aspectRatio, params.resolution)
1169
+ .set("use_reference_video_audio", params.useReferenceVideoAudio);
1170
+ this.recordRequestPayload(builder.args);
1171
+ const client = await this.getClient();
1172
+ const result = await client.subscribe(modelId, {
1173
+ input: builder.args,
1174
+ logs: true,
1175
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1176
+ abortSignal: signal
1177
+ });
1178
+ return downloadBytes(extractVideoUrl((result.data ?? result)), signal);
1179
+ }
1180
+ /** Upload raw bytes to FAL storage and return the hosted URL. */
1181
+ async upload(bytes, mimeType) {
1182
+ const client = await this.getClient();
1183
+ const blob = new Blob([new Uint8Array(bytes)], {
1184
+ type: mimeType
1185
+ });
1186
+ return client.storage.upload(blob);
1187
+ }
1188
+ /** Upload every non-empty image to FAL storage, returning the hosted URLs. */
1189
+ async uploadImages(images) {
1190
+ const valid = images.filter((b) => b && b.length > 0);
1191
+ return Promise.all(valid.map((b) => this.upload(b, detectImageMime(b))));
1192
+ }
1193
+ /** Run an endpoint that takes a single uploaded image and returns an image. */
1194
+ async runImageEndpoint(modelId, args) {
1195
+ const client = await this.getClient();
1196
+ this.recordRequestPayload(args);
1197
+ const result = await client.subscribe(modelId, {
1198
+ input: args,
1199
+ logs: true,
1200
+ onQueueUpdate: this.makeQueueUpdateHandler()
1201
+ });
1202
+ const data = (result.data ?? result);
1203
+ return downloadBytes(extractImageUrl(data));
1204
+ }
1205
+ /** Run an endpoint that returns a video and download the bytes. */
1206
+ async runVideoEndpoint(modelId, args, signal) {
1207
+ const client = await this.getClient();
1208
+ this.recordRequestPayload(args);
1209
+ const result = await client.subscribe(modelId, {
1210
+ input: args,
1211
+ logs: true,
1212
+ abortSignal: signal,
1213
+ onQueueUpdate: this.makeQueueUpdateHandler()
1214
+ });
1215
+ const data = (result.data ?? result);
1216
+ return downloadBytes(extractVideoUrl(data), signal);
1217
+ }
1218
+ async upscaleImage(image, params) {
1219
+ const { endpointId, variant } = splitTopazVariant(params.model.id);
1220
+ const url = await this.upload(image, detectImageMime(image));
1221
+ const b = new FalArgsBuilder(endpointId);
1222
+ b.attachAsset("image", url)
1223
+ .set("model", variant)
1224
+ .set("prompt", params.prompt)
1225
+ .set("upscale_factor", params.scale)
1226
+ .set("scale", params.scale)
1227
+ .set("creativity", topazCreativity(endpointId, params.creativity));
1228
+ if (params.seed != null && params.seed !== -1)
1229
+ b.set("seed", params.seed);
1230
+ log.debug("FAL upscaleImage", { model: endpointId, variant });
1231
+ return this.runImageEndpoint(endpointId, b.args);
1232
+ }
1233
+ /**
1234
+ * Segment an image into object masks. fal's SAM endpoints answer with mask
1235
+ * images plus loose side-channels — `scores`, `boxes` and `metadata` arrive
1236
+ * as JSON-encoded strings, and boxes are normalized `[cx, cy, w, h]` — so
1237
+ * everything past the mask list is parsed defensively and left absent when
1238
+ * it does not decode.
1239
+ */
1240
+ async segmentImage(image, params) {
1241
+ if (image.length === 0) {
1242
+ throw new Error("segmentImage requires a non-empty image");
1243
+ }
1244
+ const modelId = params.model.id;
1245
+ assertSegmentPromptIsUsable(modelId, params);
1246
+ const client = await this.getClient();
1247
+ const [imageUrl] = await this.uploadImages([image]);
1248
+ const args = buildSegmentImageArgs(modelId, imageUrl, params);
1249
+ this.recordRequestPayload(args);
1250
+ log.debug("FAL segmentImage", { model: modelId });
1251
+ const result = await client.subscribe(modelId, {
1252
+ input: args,
1253
+ logs: true,
1254
+ onQueueUpdate: this.makeQueueUpdateHandler(),
1255
+ abortSignal: params.signal
1256
+ });
1257
+ const data = (result.data ?? result);
1258
+ const { masks, decoded, bestScore } = await collectSegmentationMasks(data, params.minConfidence);
1259
+ if (masks.length === 0) {
1260
+ // Three different failures end here and look alike from the caller: the
1261
+ // model found nothing, the response carries a shape this decoder does
1262
+ // not read, or every mask it did read fell under `minConfidence`. Name
1263
+ // the fields the response carried and what was decoded so they separate.
1264
+ log.warn("FAL segmentImage returned no masks", {
1265
+ model: modelId,
1266
+ responseFields: Object.keys(data),
1267
+ decodedMasks: decoded,
1268
+ minConfidence: params.minConfidence ?? null,
1269
+ bestScore
1270
+ });
1271
+ }
1272
+ return masks;
1273
+ }
1274
+ async removeBackground(image, params) {
1275
+ const modelId = params.model.id;
1276
+ const url = await this.upload(image, detectImageMime(image));
1277
+ const b = new FalArgsBuilder(modelId);
1278
+ b.attachAsset("image", url).set("output_format", "png");
1279
+ log.debug("FAL removeBackground", { model: modelId });
1280
+ return this.runImageEndpoint(modelId, b.args);
1281
+ }
1282
+ async relightImage(image, params) {
1283
+ const modelId = params.model.id;
1284
+ const url = await this.upload(image, detectImageMime(image));
1285
+ const b = new FalArgsBuilder(modelId);
1286
+ b.attachAsset("image", url)
1287
+ .force("prompt", params.prompt)
1288
+ .set("output_format", "png")
1289
+ .set("negative_prompt", params.negativePrompt);
1290
+ if (params.seed != null && params.seed !== -1)
1291
+ b.set("seed", params.seed);
1292
+ log.debug("FAL relightImage", { model: modelId });
1293
+ return this.runImageEndpoint(modelId, b.args);
1294
+ }
1295
+ async vectorizeImage(image, params) {
1296
+ const modelId = params.model.id;
1297
+ const url = await this.upload(image, detectImageMime(image));
1298
+ const b = new FalArgsBuilder(modelId);
1299
+ b.attachAsset("image", url);
1300
+ log.debug("FAL vectorizeImage", { model: modelId });
1301
+ return this.runImageEndpoint(modelId, b.args);
1302
+ }
1303
+ async videoToVideo(video, params) {
1304
+ const { endpointId, variant } = splitTopazVariant(params.model.id);
1305
+ const images = params.referenceImages ?? [];
1306
+ if (images.length) {
1307
+ validateReferenceInputs("FAL", endpointId, { images, videos: [] }, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, endpointId));
1308
+ }
1309
+ params.signal?.throwIfAborted();
1310
+ const imageUrls = await Promise.all(images.map((image) => this.upload(image, detectImageMime(image))));
1311
+ const url = await this.upload(video, "video/mp4");
1312
+ const b = new FalArgsBuilder(endpointId);
1313
+ b.attachReferenceAssets("image", imageUrls);
1314
+ b.attachAsset("video", url)
1315
+ .set("model", variant)
1316
+ .set("prompt", params.prompt)
1317
+ .set("negative_prompt", params.negativePrompt)
1318
+ .set("strength", params.strength)
1319
+ .set("duration", params.durationSeconds)
1320
+ .setSize(null, params.resolution);
1321
+ if (params.seed != null && params.seed !== -1)
1322
+ b.set("seed", params.seed);
1323
+ log.debug("FAL videoToVideo", { model: endpointId, variant });
1324
+ return this.runVideoEndpoint(endpointId, b.args, params.signal);
1325
+ }
1326
+ async extendVideo(video, params) {
1327
+ const model = (await this.getAvailableVideoModels()).find((candidate) => candidate.id === params.model.id);
1328
+ if (!model?.supportedTasks?.includes("extend_video")) {
1329
+ throw new Error(`Model ${params.model.id} does not support extend_video`);
1330
+ }
1331
+ const directionTasks = model.supportedTasks.filter((task) => task.startsWith("extend_video_"));
1332
+ if (directionTasks.length > 0 &&
1333
+ !directionTasks.includes(`extend_video_${params.mode}`)) {
1334
+ throw new Error(`Model ${params.model.id} does not support ${params.mode} extension`);
1335
+ }
1336
+ if ((params.mode !== "start" && params.mode !== "end") ||
1337
+ !Number.isFinite(params.durationSeconds) ||
1338
+ params.durationSeconds <= 0 ||
1339
+ (model.durations?.length &&
1340
+ !model.durations.includes(params.durationSeconds))) {
1341
+ throw new Error("Choose a supported extension mode and duration.");
1342
+ }
1343
+ const entry = getFalManifestEntry(params.model.id);
1344
+ const durationField = entry?.inputFields?.find((field) => (field.apiParamName ?? field.name) === "duration");
1345
+ if ((durationField?.min !== undefined &&
1346
+ params.durationSeconds < durationField.min) ||
1347
+ (durationField?.max !== undefined &&
1348
+ params.durationSeconds > durationField.max)) {
1349
+ throw new Error("Choose an extension duration within the model's supported range.");
1350
+ }
1351
+ const duration = durationField?.propType === "str" &&
1352
+ typeof durationField.default === "string" &&
1353
+ /s$/i.test(durationField.default.trim())
1354
+ ? `${params.durationSeconds}s`
1355
+ : params.durationSeconds;
1356
+ const url = await this.upload(video, "video/mp4");
1357
+ const builder = new FalArgsBuilder(params.model.id);
1358
+ builder
1359
+ .attachAsset("video", url)
1360
+ .set("prompt", params.prompt)
1361
+ .set("mode", params.mode)
1362
+ .set("duration", duration);
1363
+ return this.runVideoEndpoint(params.model.id, builder.args);
1364
+ }
1365
+ async upscaleVideo(video, params) {
1366
+ const { endpointId, variant } = splitTopazVariant(params.model.id);
1367
+ const url = await this.upload(video, "video/mp4");
1368
+ const b = new FalArgsBuilder(endpointId);
1369
+ b.attachAsset("video", url)
1370
+ .set("model", variant)
1371
+ .set("prompt", params.prompt)
1372
+ .set("creativity", params.creativity)
1373
+ // `upscale_factor` and `scale` name the same thing across endpoints, and
1374
+ // `target_resolution` is the rung-shaped alternative; each is written
1375
+ // only where the endpoint declares it.
1376
+ .set("upscale_factor", params.scale)
1377
+ .set("scale", params.scale)
1378
+ .set("target_resolution", params.targetResolution)
1379
+ .setSize(null, params.targetResolution);
1380
+ if (params.seed != null && params.seed !== -1)
1381
+ b.set("seed", params.seed);
1382
+ log.debug("FAL upscaleVideo", { model: endpointId, variant });
1383
+ return this.runVideoEndpoint(endpointId, b.args);
1384
+ }
1385
+ async interpolateVideo(video, params) {
1386
+ const { endpointId, variant } = splitTopazVariant(params.model.id);
1387
+ const url = await this.upload(video, "video/mp4");
1388
+ const b = new FalArgsBuilder(endpointId);
1389
+ b.attachAsset("video", url)
1390
+ .set("model", variant)
1391
+ .set("output_fps", params.targetFps)
1392
+ .set("target_fps", params.targetFps)
1393
+ // AMT doubles the frame count per pass, so a factor is log2 passes;
1394
+ // Topaz takes the factor itself as a slowdown.
1395
+ .set("slowdown_factor", params.factor)
1396
+ .set("recursive_interpolation_passes", params.factor != null && params.factor > 1
1397
+ ? Math.max(1, Math.round(Math.log2(params.factor)))
1398
+ : null);
1399
+ if (params.seed != null && params.seed !== -1)
1400
+ b.set("seed", params.seed);
1401
+ log.debug("FAL interpolateVideo", { model: endpointId, variant });
1402
+ return this.runVideoEndpoint(endpointId, b.args);
1403
+ }
1404
+ async outpaintVideo(video, params) {
1405
+ const modelId = params.model.id;
1406
+ const url = await this.upload(video, "video/mp4");
1407
+ const b = new FalArgsBuilder(modelId);
1408
+ b.attachAsset("video", url)
1409
+ .set("prompt", params.prompt)
1410
+ .set("negative_prompt", params.negativePrompt)
1411
+ .set("expand_ratio", params.expandRatio)
1412
+ .setSize(params.aspectRatio, params.resolution);
1413
+ setOutpaintPadding(b, params.padding);
1414
+ if (params.seed != null && params.seed !== -1)
1415
+ b.set("seed", params.seed);
1416
+ log.debug("FAL outpaintVideo", { model: modelId });
1417
+ return this.runVideoEndpoint(modelId, b.args);
1418
+ }
1419
+ async lipSync(video, params) {
1420
+ const modelId = params.model.id;
1421
+ const [videoUrl, audioUrl] = await Promise.all([
1422
+ this.upload(video, "video/mp4"),
1423
+ this.upload(params.audio, "audio/mpeg")
1424
+ ]);
1425
+ const b = new FalArgsBuilder(modelId);
1426
+ b.attachAsset("video", videoUrl).attachAsset("audio", audioUrl);
1427
+ if (params.seed != null && params.seed !== -1)
1428
+ b.set("seed", params.seed);
1429
+ log.debug("FAL lipSync", { model: modelId });
1430
+ return this.runVideoEndpoint(modelId, b.args);
1431
+ }
1432
+ }
1433
+ /**
1434
+ * Decode the OpenRouter catalog fal's chat route is backed by. Throws when the
1435
+ * body carries no `data` array; {@link FalProvider.getAvailableLanguageModels}
1436
+ * treats that the way it treats an unreachable catalog.
1437
+ */
1438
+ export function decodeFalLanguageCatalog(payload) {
1439
+ if (!isObjectLike(payload)) {
1440
+ throw new Error("fal language catalog is not an object");
1441
+ }
1442
+ const rows = payload.data;
1443
+ if (!Array.isArray(rows)) {
1444
+ throw new Error("fal language catalog has no `data` array");
1445
+ }
1446
+ return rows;
404
1447
  }
405
- function extractImageUrls(result) {
1448
+ /**
1449
+ * The result-URL extractors below are the fal response decoders: every media
1450
+ * surface hands its endpoint's payload to one of them. The checked-in raw
1451
+ * fixtures in `packages/runtime/tests/fixtures/provider-contract/` run these
1452
+ * same functions, so an endpoint that renames its result field fails there.
1453
+ */
1454
+ export function extractImageUrls(result) {
406
1455
  const images = result.images;
407
1456
  if (images && images.length > 0) {
408
1457
  const urls = images.map((img) => img.url).filter(Boolean);
@@ -414,17 +1463,258 @@ function extractImageUrls(result) {
414
1463
  return [image.url];
415
1464
  throw new Error(`Unexpected FAL image response: ${JSON.stringify(result)}`);
416
1465
  }
417
- function extractImageUrl(result) {
1466
+ /**
1467
+ * Shape the inputs a SAM endpoint declares. The prompt kinds are optional and
1468
+ * an endpoint that does not declare one simply drops it — {@link
1469
+ * FalArgsBuilder.set} writes only fields the manifest knows.
1470
+ */
1471
+ function buildSegmentImageArgs(modelId, imageUrl, params) {
1472
+ const b = new FalArgsBuilder(modelId);
1473
+ const maxMasks = params.maxMasks != null ? Math.max(1, params.maxMasks) : null;
1474
+ b.attachAsset("image", imageUrl)
1475
+ // The mask images are the answer; a masked copy of the source is not.
1476
+ .set("apply_mask", false)
1477
+ .set("output_format", "png")
1478
+ .set("include_scores", true)
1479
+ .set("include_boxes", true);
1480
+ if (maxMasks != null) {
1481
+ b.set("max_masks", maxMasks).set("return_multiple_masks", maxMasks > 1);
1482
+ }
1483
+ // Each prompt kind is spelled differently per endpoint, and a name the
1484
+ // endpoint does not declare is dropped rather than sent — so a prompt written
1485
+ // to one name only would be silently discarded on half the SAM catalog.
1486
+ const prompt = params.prompt?.trim();
1487
+ const hasVisualPrompt = Boolean(params.points?.length || params.box);
1488
+ if (prompt) {
1489
+ b.setFirst(SEGMENT_TEXT_PROMPT_FIELDS, prompt);
1490
+ }
1491
+ else if (hasVisualPrompt) {
1492
+ // An absent field takes the endpoint's own default, and sam-3.1 defaults
1493
+ // `prompt` to "wheel" — so omitting the text prompt runs a concept search
1494
+ // for a wheel and the click is ignored. Measured against the live API: a
1495
+ // point alone returns `masks: []`, the same point with `prompt: ""`
1496
+ // returns the mask at score 0.51.
1497
+ b.forceFirst(SEGMENT_TEXT_PROMPT_FIELDS, "");
1498
+ }
1499
+ if (params.points?.length) {
1500
+ b.setFirst(SEGMENT_POINT_PROMPT_FIELDS, params.points.map((p) => ({
1501
+ x: Math.round(p.x),
1502
+ y: Math.round(p.y),
1503
+ label: p.include ? 1 : 0
1504
+ })));
1505
+ }
1506
+ if (params.box) {
1507
+ // fal takes box corners, not an origin and a size.
1508
+ b.setFirst(SEGMENT_BOX_PROMPT_FIELDS, [
1509
+ {
1510
+ x_min: Math.round(params.box.x),
1511
+ y_min: Math.round(params.box.y),
1512
+ x_max: Math.round(params.box.x + params.box.width),
1513
+ y_max: Math.round(params.box.y + params.box.height)
1514
+ }
1515
+ ]);
1516
+ }
1517
+ return b.args;
1518
+ }
1519
+ /**
1520
+ * Refuse a call whose target nothing names.
1521
+ *
1522
+ * An endpoint that declares a prompt field segments only what a prompt points
1523
+ * at, and answers an unprompted call with either a 422 (`fal-ai/sam2/image`:
1524
+ * "Please provide at least one of prompts or box_prompts") or a silent empty
1525
+ * mask list (`fal-ai/sam-3-1/image`, which fills the absent `prompt` with its
1526
+ * own default). Both cost a request and neither says what to do about it.
1527
+ * Endpoints that declare no prompt field at all — sam2/auto-segment, birefnet
1528
+ * — find objects on their own and run unprompted.
1529
+ */
1530
+ function assertSegmentPromptIsUsable(modelId, params) {
1531
+ if (params.prompt?.trim() || params.points?.length || params.box) {
1532
+ return;
1533
+ }
1534
+ const b = new FalArgsBuilder(modelId);
1535
+ const takesAPrompt = [
1536
+ ...SEGMENT_TEXT_PROMPT_FIELDS,
1537
+ ...SEGMENT_POINT_PROMPT_FIELDS,
1538
+ ...SEGMENT_BOX_PROMPT_FIELDS
1539
+ ].some((field) => b.declares(field));
1540
+ if (!takesAPrompt) {
1541
+ return;
1542
+ }
1543
+ throw new Error(`${modelId} needs a prompt: name what to find (for example "the dog"), ` +
1544
+ "click a point, or drag a box. To split a layer with no prompt, pick a " +
1545
+ "model that finds objects on its own, such as fal-ai/sam2/auto-segment.");
1546
+ }
1547
+ /** Text-prompt field names across fal's segmentation endpoints, in priority order. */
1548
+ const SEGMENT_TEXT_PROMPT_FIELDS = [
1549
+ "prompt",
1550
+ "text_prompt",
1551
+ "text_input",
1552
+ "object"
1553
+ ];
1554
+ const SEGMENT_POINT_PROMPT_FIELDS = ["point_prompts", "prompts"];
1555
+ const SEGMENT_BOX_PROMPT_FIELDS = ["box_prompts"];
1556
+ /**
1557
+ * Read one mask image.
1558
+ *
1559
+ * fal is inconsistent here: a `list[Image]` field usually holds `{url, width,
1560
+ * height}` objects, but the string-typed mask fields (`combined_mask`,
1561
+ * `mask_image`) hold a bare URL, and some endpoints nest the image one level
1562
+ * down. Accepting only the object form dropped whole responses on the floor.
1563
+ */
1564
+ function asFalMaskImage(value) {
1565
+ if (isNonEmptyString(value)) {
1566
+ return { url: value, width: null, height: null };
1567
+ }
1568
+ if (!isRecord(value)) {
1569
+ return null;
1570
+ }
1571
+ if (isNonEmptyString(value.url)) {
1572
+ return {
1573
+ url: value.url,
1574
+ width: finiteNumberOrNull(value.width),
1575
+ height: finiteNumberOrNull(value.height)
1576
+ };
1577
+ }
1578
+ return asFalMaskImage(value.image);
1579
+ }
1580
+ /**
1581
+ * The mask images in a fal segmentation response, in the order the endpoints
1582
+ * prefer: every list field first, then the single-mask fields.
1583
+ *
1584
+ * Each endpoint names this differently — sam-3/3-1 answer with `masks`,
1585
+ * sam2/auto-segment with `individual_masks` plus a `combined_mask` URL, and
1586
+ * the single-mask endpoints (evf-sam, sam2/image, birefnet, the florence
1587
+ * variants) with one `image` or `mask_image`.
1588
+ */
1589
+ function collectFalMaskImages(data) {
1590
+ for (const field of ["masks", "individual_masks", "images"]) {
1591
+ const value = data[field];
1592
+ if (Array.isArray(value)) {
1593
+ const masks = value
1594
+ .map(asFalMaskImage)
1595
+ .filter((mask) => mask !== null);
1596
+ if (masks.length > 0) {
1597
+ return masks;
1598
+ }
1599
+ }
1600
+ }
1601
+ for (const field of ["image", "mask_image", "combined_mask", "mask"]) {
1602
+ const mask = asFalMaskImage(data[field]);
1603
+ if (mask) {
1604
+ return [mask];
1605
+ }
1606
+ }
1607
+ return [];
1608
+ }
1609
+ /** A side-channel field fal sends as JSON text on some endpoints and as a value on others. */
1610
+ function parseFalJsonField(value) {
1611
+ if (!isString(value)) {
1612
+ return value;
1613
+ }
1614
+ try {
1615
+ return JSON.parse(value);
1616
+ }
1617
+ catch {
1618
+ // Not JSON — the field carries an opaque string this parser has no use for.
1619
+ return null;
1620
+ }
1621
+ }
1622
+ function finiteNumberOrNull(value) {
1623
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1624
+ }
1625
+ /**
1626
+ * Convert one entry of fal's `boxes` output into source-image pixels. fal sends
1627
+ * normalized `[cx, cy, w, h]` (center and size, 0–1), which needs the mask's
1628
+ * own size to become a rectangle.
1629
+ */
1630
+ function normalizeFalBox(value, width, height) {
1631
+ if (Array.isArray(value) && value.length >= 4) {
1632
+ const [cx, cy, w, h] = value.map(finiteNumberOrNull);
1633
+ if (cx == null || cy == null || w == null || h == null) {
1634
+ return null;
1635
+ }
1636
+ if (width == null || height == null) {
1637
+ return null;
1638
+ }
1639
+ return {
1640
+ x: Math.round((cx - w / 2) * width),
1641
+ y: Math.round((cy - h / 2) * height),
1642
+ width: Math.round(w * width),
1643
+ height: Math.round(h * height)
1644
+ };
1645
+ }
1646
+ if (isRecord(value)) {
1647
+ const x = finiteNumberOrNull(value.x);
1648
+ const y = finiteNumberOrNull(value.y);
1649
+ const w = finiteNumberOrNull(value.width);
1650
+ const h = finiteNumberOrNull(value.height);
1651
+ if (x != null && y != null && w != null && h != null) {
1652
+ return { x, y, width: w, height: h };
1653
+ }
1654
+ }
1655
+ return null;
1656
+ }
1657
+ /** The label and score fal reports per mask under `metadata`. */
1658
+ function falMaskMetadata(entries, index) {
1659
+ const list = parseFalJsonField(entries);
1660
+ const entry = Array.isArray(list) ? list[index] : index === 0 ? list : null;
1661
+ if (!isRecord(entry)) {
1662
+ return { label: null, score: null, box: null };
1663
+ }
1664
+ const label = isNonEmptyString(entry.label)
1665
+ ? entry.label
1666
+ : isNonEmptyString(entry.name)
1667
+ ? entry.name
1668
+ : null;
1669
+ return {
1670
+ label,
1671
+ score: finiteNumberOrNull(entry.score),
1672
+ box: entry.box ?? entry.bbox ?? entry.bounds ?? null
1673
+ };
1674
+ }
1675
+ async function collectSegmentationMasks(data, minConfidence) {
1676
+ const rawMasks = collectFalMaskImages(data);
1677
+ const scores = parseFalJsonField(data.scores);
1678
+ const boxes = parseFalJsonField(data.boxes);
1679
+ const threshold = minConfidence ?? null;
1680
+ const masks = [];
1681
+ let bestScore = null;
1682
+ for (let i = 0; i < rawMasks.length; i++) {
1683
+ const { url, width, height } = rawMasks[i];
1684
+ const meta = falMaskMetadata(data.metadata, i);
1685
+ const confidence = meta.score ??
1686
+ (Array.isArray(scores) ? finiteNumberOrNull(scores[i]) : null);
1687
+ if (confidence != null && (bestScore == null || confidence > bestScore)) {
1688
+ bestScore = confidence;
1689
+ }
1690
+ if (threshold != null && confidence != null && confidence < threshold) {
1691
+ continue;
1692
+ }
1693
+ const box = normalizeFalBox(meta.box, width, height) ??
1694
+ (Array.isArray(boxes) ? normalizeFalBox(boxes[i], width, height) : null);
1695
+ masks.push({
1696
+ mask: await downloadBytes(url),
1697
+ mimeType: "image/png",
1698
+ width,
1699
+ height,
1700
+ label: meta.label,
1701
+ confidence,
1702
+ box
1703
+ });
1704
+ }
1705
+ return { masks, decoded: rawMasks.length, bestScore };
1706
+ }
1707
+ export function extractImageUrl(result) {
418
1708
  return extractImageUrls(result)[0];
419
1709
  }
420
- function extractVideoUrl(result) {
1710
+ export function extractVideoUrl(result) {
421
1711
  // FAL video endpoints return { video: { url } } or { video_url } or { url }
422
1712
  const video = result.video;
423
1713
  if (video?.url)
424
1714
  return video.url;
425
- if (typeof result.video_url === "string")
1715
+ if (isString(result.video_url))
426
1716
  return result.video_url;
427
- if (typeof result.url === "string")
1717
+ if (isString(result.url))
428
1718
  return result.url;
429
1719
  // Some endpoints return an array of videos
430
1720
  const videos = result.videos;
@@ -435,8 +1725,23 @@ function extractVideoUrl(result) {
435
1725
  }
436
1726
  throw new Error(`Unexpected FAL video response: ${JSON.stringify(result)}`);
437
1727
  }
438
- async function downloadBytes(url) {
439
- const res = await fetch(url);
1728
+ export function extractAudioUrl(result) {
1729
+ // FAL TTS endpoints return { audio: { url } } or { audio: "url" } or
1730
+ // { audio_url } or a bare { url }.
1731
+ const audio = result.audio;
1732
+ if (isObjectLike(audio) && isString(audio.url)) {
1733
+ return audio.url;
1734
+ }
1735
+ if (isString(audio))
1736
+ return audio;
1737
+ if (isString(result.audio_url))
1738
+ return result.audio_url;
1739
+ if (isString(result.url))
1740
+ return result.url;
1741
+ throw new Error(`Unexpected FAL audio response: ${JSON.stringify(result)}`);
1742
+ }
1743
+ async function downloadBytes(url, signal) {
1744
+ const res = await safeFetch(url, signal ? { signal } : undefined);
440
1745
  if (!res.ok)
441
1746
  throw new Error(`Failed to download FAL result: ${res.status}`);
442
1747
  const buf = await res.arrayBuffer();