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

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