@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
@@ -1,41 +1,384 @@
1
1
  import { createLogger } from "@nodetool-ai/config";
2
2
  import { BaseProvider } from "./base-provider.js";
3
+ import { geminiContextExceeded } from "./context-exceeded.js";
4
+ import { sniffAudioMime } from "./audio-mime.js";
5
+ import { sniffVideoMime } from "./video-mime.js";
6
+ import { detectImageMime } from "./image-mime.js";
7
+ import { safeFetch } from "./safe-url.js";
8
+ import { ContentFilterRefusal, isContentFilterRefusal } from "./content-filter.js";
9
+ import { isBoolean, isFiniteNumber, isNonEmptyString, isString } from "@nodetool-ai/protocol";
3
10
  const log = createLogger("nodetool.runtime.providers.gemini");
11
+ import { WEB_SEARCH_TOOL_NAME } from "./types.js";
4
12
  const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
13
+ const GEMINI_UPLOAD_BASE = `${GEMINI_API_BASE.replace("/v1beta", "")}/upload/v1beta/files`;
14
+ /**
15
+ * Largest payload sent as `inlineData`. Gemini caps a generateContent request
16
+ * at 20 MB total; anything above this goes through the Files API instead, which
17
+ * leaves room for the rest of the request.
18
+ */
19
+ export const GEMINI_INLINE_VIDEO_MAX_BYTES = 19 * 1024 * 1024;
20
+ /** Polls of `files/{name}` before an uploaded file is declared stuck. */
21
+ const GEMINI_FILE_MAX_POLLS = 60;
22
+ const GEMINI_FILE_POLL_INTERVAL_MS = 2_000;
23
+ /** Veo accepts at most three `asset` reference images per request. */
24
+ const VEO_MAX_REFERENCE_IMAGES = 3;
25
+ /** Veo requires 8-second clips for reference images and first/last frames. */
26
+ const VEO_CONSTRAINED_DURATION_SECONDS = 8;
27
+ /** Veo appends a fixed 7-second continuation to the input video. */
28
+ const VEO_EXTENSION_SECONDS = 7;
29
+ /** Omni Flash accepts at most three reference clips. */
30
+ const OMNI_MAX_REFERENCE_VIDEOS = 3;
31
+ /**
32
+ * Video models and the tasks each accepts. Veo 3.1 Lite takes no reference
33
+ * images, last frame, or extension. Omni Flash runs through the Interactions
34
+ * API and edits existing video; its duration follows the prompt.
35
+ *
36
+ * Veo lists no `durations`: callers apply that list to extension length too,
37
+ * and Veo generates 4, 6, or 8 seconds but extends by 7 only.
38
+ */
39
+ const GEMINI_VIDEO_MODELS = [
40
+ {
41
+ id: "veo-3.1-generate-preview",
42
+ name: "Veo 3.1 Preview",
43
+ provider: "gemini",
44
+ supportedTasks: [
45
+ "text_to_video",
46
+ "image_to_video",
47
+ "reference_to_video",
48
+ "extend_video",
49
+ "extend_video_end"
50
+ ],
51
+ resolutions: ["720p", "1080p", "4k"],
52
+ aspectRatios: ["16:9", "9:16"]
53
+ },
54
+ {
55
+ id: "veo-3.1-fast-generate-preview",
56
+ name: "Veo 3.1 Fast Preview",
57
+ provider: "gemini",
58
+ supportedTasks: [
59
+ "text_to_video",
60
+ "image_to_video",
61
+ "reference_to_video",
62
+ "extend_video",
63
+ "extend_video_end"
64
+ ],
65
+ resolutions: ["720p", "1080p", "4k"],
66
+ aspectRatios: ["16:9", "9:16"]
67
+ },
68
+ {
69
+ id: "veo-3.1-lite-generate-preview",
70
+ name: "Veo 3.1 Lite Preview",
71
+ provider: "gemini",
72
+ supportedTasks: ["text_to_video", "image_to_video"],
73
+ resolutions: ["720p", "1080p"],
74
+ aspectRatios: ["16:9", "9:16"]
75
+ },
76
+ {
77
+ id: "gemini-omni-1.1-flash",
78
+ name: "Gemini Omni Flash",
79
+ provider: "gemini",
80
+ supportedTasks: [
81
+ "text_to_video",
82
+ "image_to_video",
83
+ "reference_to_video",
84
+ "video_to_video"
85
+ ],
86
+ resolutions: ["360p", "720p", "1080p", "4k"],
87
+ aspectRatios: ["16:9", "9:16"]
88
+ }
89
+ ];
90
+ function isOmniVideoModel(modelId) {
91
+ return modelId.startsWith("gemini-omni-");
92
+ }
93
+ function requireVideoTask(modelId, task) {
94
+ const model = GEMINI_VIDEO_MODELS.find((entry) => entry.id === modelId);
95
+ if (!model?.supportedTasks?.includes(task)) {
96
+ throw new Error(`Gemini model ${modelId} does not support ${task}`);
97
+ }
98
+ }
99
+ function combineSignals(signal, timeoutSeconds) {
100
+ const timeout = timeoutSeconds && timeoutSeconds > 0
101
+ ? AbortSignal.timeout(timeoutSeconds * 1000)
102
+ : undefined;
103
+ if (!signal)
104
+ return timeout;
105
+ if (!timeout)
106
+ return signal;
107
+ return AbortSignal.any([signal, timeout]);
108
+ }
109
+ /** The image object Veo reads for `image`, `lastFrame`, and references. */
110
+ function veoImage(bytes) {
111
+ return {
112
+ bytesBase64Encoded: Buffer.from(bytes).toString("base64"),
113
+ mimeType: detectImageMime(bytes)
114
+ };
115
+ }
116
+ /** Drop `; charset=…`/`; codecs=…` parameters from a Content-Type header. */
117
+ function stripMimeParams(value) {
118
+ const mime = value?.split(";")[0].trim();
119
+ return mime || undefined;
120
+ }
121
+ /**
122
+ * Normalize an audio mime to one Gemini's `inlineData` accepts. Gemini lists
123
+ * audio/wav, audio/mp3, audio/aiff, audio/aac, audio/ogg and audio/flac; the
124
+ * common `audio/mpeg` label is remapped to `audio/mp3`. Falls back to
125
+ * `audio/mp3` when the type is unknown.
126
+ */
127
+ function geminiAudioMime(mime) {
128
+ if (!mime)
129
+ return "audio/mp3";
130
+ if (mime === "audio/mpeg" || mime === "audio/mpga")
131
+ return "audio/mp3";
132
+ return mime;
133
+ }
134
+ /**
135
+ * Gemini's native/custom tool combination contract is versioned by model
136
+ * family. Keep this check deliberately based on the model id supplied for the
137
+ * request: model discovery is optional and callers may use a newly released
138
+ * Gemini 3 id before it appears in the catalog.
139
+ */
140
+ function isGemini3Model(model) {
141
+ const modelId = model.split("/").pop()?.toLowerCase() ?? model.toLowerCase();
142
+ return /^gemini-3(?:[.-]|$)/.test(modelId);
143
+ }
5
144
  // Gemini's function-declaration schema is a strict subset of OpenAPI 3.0.
6
- // It rejects JSON-Schema-only fields like `additionalProperties`, `$schema`,
7
- // `$id`, `$ref`, `definitions`, `patternProperties`, etc. Any one of these
145
+ // It rejects JSON-Schema-only fields like `const`, `additionalProperties`,
146
+ // `$schema`, `$ref`, `definitions`, `patternProperties`, etc. Any one of these
8
147
  // anywhere in the tree causes a 400 that aborts the entire tool batch, so we
9
- // recursively strip them before sending.
148
+ // recursively strip them before sending. Zod 4's `z.toJSONSchema` (draft
149
+ // 2020-12) emits several of them — `const` for every `z.literal()`, `$ref` +
150
+ // `$defs` for every reused schema — so tools defined in Zod hit this.
10
151
  const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([
11
152
  "additionalProperties",
12
153
  "$schema",
13
154
  "$id",
14
155
  "$ref",
156
+ "$defs",
157
+ "$comment",
15
158
  "definitions",
16
159
  "patternProperties",
160
+ "propertyNames",
17
161
  "unevaluatedProperties",
162
+ "unevaluatedItems",
18
163
  "dependentSchemas",
19
- "dependentRequired"
164
+ "dependentRequired",
165
+ "exclusiveMinimum",
166
+ "exclusiveMaximum",
167
+ "const",
168
+ "allOf",
169
+ "oneOf",
170
+ "not",
171
+ "if",
172
+ "then",
173
+ "else",
174
+ "prefixItems",
175
+ "additionalItems",
176
+ "contains",
177
+ "minContains",
178
+ "maxContains",
179
+ "uniqueItems",
180
+ "multipleOf",
181
+ "examples",
182
+ "readOnly",
183
+ "writeOnly",
184
+ "deprecated",
185
+ "contentEncoding",
186
+ "contentMediaType"
20
187
  ]);
21
- function sanitizeGeminiSchema(value) {
22
- if (Array.isArray(value)) {
23
- return value.map(sanitizeGeminiSchema);
188
+ /** Keywords whose value is data, not a schema — never recurse into them. */
189
+ const GEMINI_DATA_KEYS = new Set([
190
+ "enum",
191
+ "const",
192
+ "default",
193
+ "example",
194
+ "examples"
195
+ ]);
196
+ function isArraySchemaType(type) {
197
+ if (isString(type))
198
+ return type.toLowerCase() === "array";
199
+ if (Array.isArray(type)) {
200
+ return type.some((t) => isString(t) && t.toLowerCase() === "array");
201
+ }
202
+ return false;
203
+ }
204
+ function isPlainObject(value) {
205
+ return !!value && typeof value === "object" && !Array.isArray(value);
206
+ }
207
+ /** The JSON Schema type name for a primitive literal, if it has one. */
208
+ function primitiveSchemaType(value) {
209
+ if (isString(value))
210
+ return "string";
211
+ if (isBoolean(value))
212
+ return "boolean";
213
+ if (isFiniteNumber(value)) {
214
+ return Number.isInteger(value) ? "integer" : "number";
24
215
  }
25
- if (value && typeof value === "object") {
26
- const out = {};
27
- for (const [k, v] of Object.entries(value)) {
28
- if (GEMINI_UNSUPPORTED_SCHEMA_KEYS.has(k))
216
+ return undefined;
217
+ }
218
+ function resolveJsonPointer(root, pointer) {
219
+ if (pointer === "#" || pointer === "")
220
+ return { found: true, value: root };
221
+ if (!pointer.startsWith("#/"))
222
+ return { found: false, value: undefined };
223
+ let cursor = root;
224
+ for (const rawSegment of pointer.slice(2).split("/")) {
225
+ const segment = decodeURIComponent(rawSegment)
226
+ .replace(/~1/g, "/")
227
+ .replace(/~0/g, "~");
228
+ if (Array.isArray(cursor)) {
229
+ const index = Number(segment);
230
+ if (!Number.isInteger(index) || index < 0 || index >= cursor.length) {
231
+ return { found: false, value: undefined };
232
+ }
233
+ cursor = cursor[index];
234
+ continue;
235
+ }
236
+ if (!isPlainObject(cursor) || !(segment in cursor)) {
237
+ return { found: false, value: undefined };
238
+ }
239
+ cursor = cursor[segment];
240
+ }
241
+ return { found: true, value: cursor };
242
+ }
243
+ function inlineGeminiRefs(node, root, seen) {
244
+ if (Array.isArray(node)) {
245
+ return node.map((item) => inlineGeminiRefs(item, root, seen));
246
+ }
247
+ // SAFETY: a schema node that is neither a list nor an object is a JSON
248
+ // scalar — this walker is only ever handed a decoded JSON Schema.
249
+ if (!isPlainObject(node))
250
+ return node;
251
+ if (isString(node.$ref)) {
252
+ const { $ref, ...rest } = node;
253
+ if (seen.has($ref))
254
+ return { type: "object", ...rest };
255
+ const { found, value } = resolveJsonPointer(root, $ref);
256
+ if (!found || !isPlainObject(value))
257
+ return { type: "object", ...rest };
258
+ // SAFETY: both inputs are objects, so both results are objects too.
259
+ const resolved = inlineGeminiRefs(value, root, new Set([...seen, $ref]));
260
+ // SAFETY: as above.
261
+ const overrides = inlineGeminiRefs(rest, root, seen);
262
+ return { ...resolved, ...overrides };
263
+ }
264
+ const out = {};
265
+ for (const [key, value] of Object.entries(node)) {
266
+ out[key] = GEMINI_DATA_KEYS.has(key)
267
+ ? // SAFETY: a data key's value is carried through verbatim; it is part of
268
+ // the same decoded JSON Schema.
269
+ value
270
+ : inlineGeminiRefs(value, root, seen);
271
+ }
272
+ return out;
273
+ }
274
+ /** Fold `allOf` members into the parent schema; parent keys win. */
275
+ function mergeAllOf(out, members) {
276
+ for (const member of members) {
277
+ const sanitized = sanitizeSchemaNode(member);
278
+ if (!isPlainObject(sanitized))
279
+ continue;
280
+ for (const [key, value] of Object.entries(sanitized)) {
281
+ if (key === "properties" && isPlainObject(value)) {
282
+ // SAFETY: both sides are `properties` maps of the same schema.
283
+ out.properties = {
284
+ ...value,
285
+ ...(out.properties ?? {})
286
+ };
287
+ continue;
288
+ }
289
+ if (key === "required" && Array.isArray(value)) {
290
+ const existing = Array.isArray(out.required) ? out.required : [];
291
+ out.required = [...new Set([...existing, ...value])];
29
292
  continue;
30
- out[k] = sanitizeGeminiSchema(v);
293
+ }
294
+ if (out[key] === undefined)
295
+ out[key] = value;
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+ function sanitizeSchemaNode(value) {
301
+ if (Array.isArray(value))
302
+ return value.map(sanitizeSchemaNode);
303
+ // SAFETY: as in `inlineGeminiRefs` — a non-list, non-object node is a scalar.
304
+ if (!isPlainObject(value))
305
+ return value;
306
+ const out = {};
307
+ for (const [key, nested] of Object.entries(value)) {
308
+ if (GEMINI_UNSUPPORTED_SCHEMA_KEYS.has(key))
309
+ continue;
310
+ if (key === "properties" && isPlainObject(nested)) {
311
+ const properties = {};
312
+ // Property *names* are data — a property called "const" must survive.
313
+ for (const [name, sub] of Object.entries(nested)) {
314
+ properties[name] = sanitizeSchemaNode(sub);
315
+ }
316
+ out.properties = properties;
317
+ continue;
318
+ }
319
+ if (key === "items") {
320
+ out.items = sanitizeSchemaNode(Array.isArray(nested) ? (nested[0] ?? { type: "string" }) : nested);
321
+ continue;
322
+ }
323
+ if (key === "anyOf" && Array.isArray(nested)) {
324
+ out.anyOf = nested.map(sanitizeSchemaNode);
325
+ continue;
31
326
  }
32
- return out;
327
+ out[key] = GEMINI_DATA_KEYS.has(key)
328
+ ? // SAFETY: a data key's value is carried through verbatim; it is part of
329
+ // the same decoded JSON Schema.
330
+ nested
331
+ : sanitizeSchemaNode(nested);
332
+ }
333
+ // `oneOf` means the same thing to a model as `anyOf`, which Gemini accepts.
334
+ if (out.anyOf === undefined && Array.isArray(value.oneOf)) {
335
+ out.anyOf = value.oneOf.map(sanitizeSchemaNode);
336
+ }
337
+ if (Array.isArray(value.allOf))
338
+ mergeAllOf(out, value.allOf);
339
+ // `const` is what Zod emits for a literal. A single-value `enum` says the
340
+ // same thing in Gemini's dialect, but only for strings — its `enum` is a
341
+ // list of strings — so other literals keep the constraint in the description.
342
+ if (value.const !== undefined && out.enum === undefined) {
343
+ const literalType = primitiveSchemaType(value.const);
344
+ if (literalType === "string") {
345
+ // SAFETY: `primitiveSchemaType` proved the literal is a string.
346
+ out.enum = [value.const];
347
+ out.type ??= "string";
348
+ }
349
+ else if (literalType) {
350
+ out.type ??= literalType;
351
+ const hint = `Must be ${JSON.stringify(value.const)}.`;
352
+ out.description =
353
+ isNonEmptyString(out.description)
354
+ ? `${out.description} ${hint}`
355
+ : hint;
356
+ }
357
+ }
358
+ // Gemini's `type` is one string; JSON Schema allows a union. `["x","null"]`
359
+ // is Zod's optional/nullable shape and maps onto `nullable`.
360
+ if (Array.isArray(out.type)) {
361
+ const named = out.type.filter((t) => typeof t === "string" && t.toLowerCase() !== "null");
362
+ if (named.length < out.type.length)
363
+ out.nullable = true;
364
+ if (named.length > 0)
365
+ out.type = named[0];
366
+ else
367
+ delete out.type;
368
+ }
369
+ // Gemini rejects an array schema that omits `items` ("items: missing
370
+ // field"). JSON Schema allows it, so backfill a permissive default.
371
+ if (isArraySchemaType(out.type) && out.items === undefined) {
372
+ out.items = { type: "string" };
33
373
  }
34
- return value;
374
+ return out;
375
+ }
376
+ function sanitizeGeminiSchema(value) {
377
+ return sanitizeSchemaNode(inlineGeminiRefs(value, value, new Set()));
35
378
  }
36
379
  function sanitizeToolName(name) {
37
380
  let sanitized = (name ?? "").trim();
38
- sanitized = sanitized.replace(/[^a-zA-Z0-9_.:-]/g, "_");
381
+ sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, "_");
39
382
  sanitized = sanitized.replace(/_+/g, "_");
40
383
  if (!sanitized)
41
384
  sanitized = "_tool";
@@ -47,12 +390,189 @@ function sanitizeToolName(name) {
47
390
  sanitized = "_tool";
48
391
  return sanitized;
49
392
  }
393
+ function appendGeminiContent(contents, content) {
394
+ const previous = contents[contents.length - 1];
395
+ if (previous?.role === content.role) {
396
+ previous.parts.push(...content.parts);
397
+ }
398
+ else {
399
+ contents.push(content);
400
+ }
401
+ }
402
+ /**
403
+ * Sentinel Gemini accepts in place of a real thought signature. Gemini 3
404
+ * rejects a request whose history replays a function call with no signature —
405
+ * which is every turn recorded before signatures were persisted, and every
406
+ * call a caller injected by hand. The documented escape hatch is this literal:
407
+ * https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures
408
+ * Reasoning continuity is lost for that call, so it is only ever a fallback
409
+ * for a signature we do not have.
410
+ */
411
+ const GEMINI_UNSIGNED_CALL_SENTINEL = "skip_thought_signature_validator";
412
+ /**
413
+ * Gemini validates that the *first* functionCall part of each model turn
414
+ * carries a signature. Stamp the sentinel on any that reaches us without one
415
+ * so an unsigned history fails soft (degraded reasoning) instead of 400.
416
+ */
417
+ function signUnsignedFunctionCalls(contents) {
418
+ for (const content of contents) {
419
+ if (content.role !== "model")
420
+ continue;
421
+ const first = content.parts.find((p) => p.functionCall !== undefined);
422
+ if (first && !first.thoughtSignature) {
423
+ first.thoughtSignature = GEMINI_UNSIGNED_CALL_SENTINEL;
424
+ }
425
+ }
426
+ }
427
+ function geminiResponseError(data) {
428
+ if (data.error?.message)
429
+ return new Error(`Gemini API error: ${data.error.message}`);
430
+ if (data.promptFeedback?.blockReason) {
431
+ const detail = data.promptFeedback.blockReasonMessage
432
+ ? `: ${data.promptFeedback.blockReasonMessage}`
433
+ : "";
434
+ return new Error(`Gemini prompt blocked (${data.promptFeedback.blockReason})${detail}`);
435
+ }
436
+ return null;
437
+ }
438
+ function parseGeminiResponse(value) {
439
+ if (!isPlainObject(value)) {
440
+ throw new Error("Gemini returned an invalid response envelope");
441
+ }
442
+ const response = value;
443
+ if (response.candidates !== undefined &&
444
+ !Array.isArray(response.candidates)) {
445
+ throw new Error("Gemini returned invalid candidates");
446
+ }
447
+ for (const candidate of response.candidates ?? []) {
448
+ if (candidate.content?.parts !== undefined &&
449
+ !Array.isArray(candidate.content.parts)) {
450
+ throw new Error("Gemini returned invalid candidate parts");
451
+ }
452
+ }
453
+ return response;
454
+ }
455
+ /**
456
+ * Decode a `:generateContent` body: validate the envelope, raise the API's own
457
+ * error or block reason, and return the first candidate's parts.
458
+ *
459
+ * The live contract probe and the checked-in raw-response fixtures
460
+ * (`packages/runtime/tests/fixtures/provider-contract/`) run this same
461
+ * function, so a wire change surfaces here rather than only on a real call.
462
+ */
463
+ export function decodeGeminiGenerateContent(value) {
464
+ const data = parseGeminiResponse(value);
465
+ const dataError = geminiResponseError(data);
466
+ if (dataError)
467
+ throw dataError;
468
+ const candidate = data.candidates?.[0];
469
+ if (!candidate?.content?.parts) {
470
+ throw new Error("Gemini returned no candidates");
471
+ }
472
+ return {
473
+ parts: candidate.content.parts,
474
+ finishReason: candidate.finishReason ?? null,
475
+ usage: data.usageMetadata
476
+ };
477
+ }
478
+ /**
479
+ * Decode one page of `GET /models`. Throws when the page carries no `models`
480
+ * array; {@link GeminiProvider.getAvailableLanguageModels} treats that the same
481
+ * way it treats an unreachable listing.
482
+ */
483
+ export function decodeGeminiModelsPage(value) {
484
+ if (!isPlainObject(value)) {
485
+ throw new Error("Gemini model page is not an object");
486
+ }
487
+ const page = value;
488
+ if (!Array.isArray(page.models)) {
489
+ throw new Error("Gemini model page has no `models` array");
490
+ }
491
+ return page;
492
+ }
493
+ function normalizeEmbedding(values) {
494
+ const norm = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
495
+ return norm > 0 ? values.map((value) => value / norm) : values;
496
+ }
497
+ function abortError(signal) {
498
+ return signal?.reason instanceof Error
499
+ ? signal.reason
500
+ : new DOMException("Aborted", "AbortError");
501
+ }
502
+ async function* decodeGeminiSse(body, signal) {
503
+ const reader = body.getReader();
504
+ const decoder = new TextDecoder();
505
+ let buffer = "";
506
+ try {
507
+ while (true) {
508
+ if (signal?.aborted)
509
+ throw abortError(signal);
510
+ const { done, value } = await reader.read();
511
+ buffer += decoder.decode(value, { stream: !done });
512
+ const events = buffer.split(/\r?\n\r?\n/);
513
+ buffer = events.pop() ?? "";
514
+ if (done && buffer.trim()) {
515
+ events.push(buffer);
516
+ buffer = "";
517
+ }
518
+ for (const eventText of events) {
519
+ const data = eventText
520
+ .split(/\r?\n/)
521
+ .filter((line) => line.startsWith("data:"))
522
+ .map((line) => line.slice(5).trimStart())
523
+ .join("\n")
524
+ .trim();
525
+ if (!data || data === "[DONE]")
526
+ continue;
527
+ let parsed;
528
+ try {
529
+ parsed = JSON.parse(data);
530
+ }
531
+ catch (error) {
532
+ throw new Error("Gemini returned malformed SSE JSON", {
533
+ cause: error
534
+ });
535
+ }
536
+ if (!isPlainObject(parsed)) {
537
+ throw new Error("Gemini returned an invalid SSE event");
538
+ }
539
+ yield parseGeminiResponse(parsed);
540
+ }
541
+ if (done)
542
+ break;
543
+ }
544
+ }
545
+ finally {
546
+ // Stop the underlying connection whenever the consumer bails early (abort
547
+ // or `break`); releasing the lock alone leaves the HTTP body undrained.
548
+ await reader
549
+ .cancel(signal?.aborted ? signal.reason : undefined)
550
+ .catch(() => undefined);
551
+ reader.releaseLock();
552
+ }
553
+ }
50
554
  export class GeminiProvider extends BaseProvider {
555
+ declaredCapabilities() {
556
+ return [
557
+ "text_to_image",
558
+ "image_to_image",
559
+ "text_to_video",
560
+ "image_to_video",
561
+ "reference_to_video",
562
+ "video_to_video",
563
+ "extend_video",
564
+ "text_to_speech",
565
+ "text_to_music",
566
+ "automatic_speech_recognition",
567
+ "generate_embedding"
568
+ ];
569
+ }
51
570
  static requiredSecrets() {
52
571
  return ["GEMINI_API_KEY"];
53
572
  }
54
573
  apiKey;
55
574
  _fetch;
575
+ _sleep;
56
576
  constructor(secrets, options = {}) {
57
577
  super("gemini");
58
578
  const apiKey = secrets.GEMINI_API_KEY;
@@ -61,6 +581,9 @@ export class GeminiProvider extends BaseProvider {
61
581
  }
62
582
  this.apiKey = apiKey;
63
583
  this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
584
+ this._sleep =
585
+ options.sleepFn ??
586
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
64
587
  }
65
588
  getContainerEnv() {
66
589
  return { GEMINI_API_KEY: this.apiKey };
@@ -68,33 +591,55 @@ export class GeminiProvider extends BaseProvider {
68
591
  async hasToolSupport(_model) {
69
592
  return true;
70
593
  }
594
+ get supportsNativeWebSearch() {
595
+ return true;
596
+ }
597
+ /** Gemini reads a whole clip: see {@link videoContentToGeminiPart}. */
598
+ get supportsVideoInput() {
599
+ return true;
600
+ }
71
601
  // ---------------------------------------------------------------------------
72
602
  // Model listing
73
603
  // ---------------------------------------------------------------------------
74
604
  async getAvailableLanguageModels() {
75
- const url = `${GEMINI_API_BASE}/models?key=${this.apiKey}`;
76
- let response;
605
+ const items = [];
606
+ let pageToken;
77
607
  try {
78
- response = await this._fetch(url);
608
+ do {
609
+ const query = new URLSearchParams({
610
+ key: this.apiKey,
611
+ pageSize: "1000"
612
+ });
613
+ if (pageToken)
614
+ query.set("pageToken", pageToken);
615
+ const response = await this._fetch(`${GEMINI_API_BASE}/models?${query}`);
616
+ if (!response.ok)
617
+ return [];
618
+ const payload = decodeGeminiModelsPage(await response.json());
619
+ items.push(...(payload.models ?? []));
620
+ pageToken = payload.nextPageToken;
621
+ } while (pageToken);
79
622
  }
80
623
  catch {
81
624
  return [];
82
625
  }
83
- if (!response.ok)
84
- return [];
85
- const payload = (await response.json());
86
- const items = payload.models ?? [];
626
+ const seen = new Set();
87
627
  return items
88
628
  .filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent"))
89
629
  .filter((m) => !!m.name)
630
+ .filter((m) => !/(embedding|aqa|imagen|veo|image|tts)/i.test(m.name ?? ""))
90
631
  .map((m) => {
91
632
  const id = m.name.split("/").pop();
633
+ if (seen.has(id))
634
+ return null;
635
+ seen.add(id);
92
636
  return {
93
637
  id,
94
638
  name: m.displayName ?? id,
95
639
  provider: "gemini"
96
640
  };
97
- });
641
+ })
642
+ .filter((model) => model !== null);
98
643
  }
99
644
  // ---------------------------------------------------------------------------
100
645
  // Message conversion helpers
@@ -107,26 +652,43 @@ export class GeminiProvider extends BaseProvider {
107
652
  const img = content.image;
108
653
  let base64Data;
109
654
  let mimeType = img.mimeType ?? "image/jpeg";
110
- if (img.data) {
111
- if (typeof img.data === "string") {
112
- base64Data = img.data;
655
+ const parseImageDataUri = (uri) => {
656
+ const idx = uri.indexOf(",");
657
+ if (idx < 0)
658
+ throw new Error("Invalid image data URI");
659
+ const header = uri.slice(5, idx);
660
+ mimeType = header.split(";")[0] || mimeType;
661
+ return uri.slice(idx + 1);
662
+ };
663
+ if (isNonEmptyString(img.data) ||
664
+ (img.data instanceof Uint8Array && img.data.length > 0)) {
665
+ if (isString(img.data)) {
666
+ // Inline data may itself be a data: URI — strip the prefix and take
667
+ // the real mime type from it rather than shipping the header as
668
+ // base64 payload.
669
+ base64Data = img.data.startsWith("data:")
670
+ ? parseImageDataUri(img.data)
671
+ : img.data;
113
672
  }
114
673
  else {
115
674
  base64Data = Buffer.from(img.data).toString("base64");
116
675
  }
117
676
  }
118
677
  else if (img.uri) {
119
- if (img.uri.startsWith("data:")) {
120
- const idx = img.uri.indexOf(",");
121
- const header = img.uri.slice(5, idx);
122
- mimeType = header.split(";")[0] || mimeType;
123
- base64Data = img.uri.slice(idx + 1);
678
+ // resolveUri turns asset file:// URIs (what the chat pipeline produces)
679
+ // into data: URIs; http(s) URIs pass through to safeFetch.
680
+ const resolvedUri = img.uri.startsWith("data:")
681
+ ? img.uri
682
+ : await this.resolveUri(img.uri);
683
+ if (resolvedUri.startsWith("data:")) {
684
+ base64Data = parseImageDataUri(resolvedUri);
124
685
  }
125
686
  else {
126
- const resp = await this._fetch(img.uri);
687
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
127
688
  if (!resp.ok)
128
689
  throw new Error(`Failed to fetch image: ${resp.status}`);
129
- mimeType = resp.headers.get("content-type") ?? mimeType;
690
+ mimeType =
691
+ stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
130
692
  base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
131
693
  }
132
694
  }
@@ -138,87 +700,271 @@ export class GeminiProvider extends BaseProvider {
138
700
  if (content.type === "audio") {
139
701
  const aud = content.audio;
140
702
  let base64Data;
141
- let mimeType = aud.mimeType ?? "audio/mp3";
142
- if (aud.data) {
143
- if (typeof aud.data === "string") {
144
- base64Data = aud.data;
703
+ let mimeType = aud.mimeType;
704
+ const parseAudioDataUri = (uri) => {
705
+ const idx = uri.indexOf(",");
706
+ if (idx < 0)
707
+ throw new Error("Invalid audio data URI");
708
+ const header = uri.slice(5, idx);
709
+ mimeType = mimeType ?? header.split(";")[0];
710
+ return uri.slice(idx + 1);
711
+ };
712
+ if (isNonEmptyString(aud.data) ||
713
+ (aud.data instanceof Uint8Array && aud.data.length > 0)) {
714
+ if (isString(aud.data)) {
715
+ base64Data = aud.data.startsWith("data:")
716
+ ? parseAudioDataUri(aud.data)
717
+ : aud.data;
718
+ mimeType =
719
+ mimeType ?? sniffAudioMime(Buffer.from(base64Data, "base64"));
145
720
  }
146
721
  else {
147
- base64Data = Buffer.from(aud.data).toString("base64");
722
+ const bytes = Buffer.from(aud.data);
723
+ base64Data = bytes.toString("base64");
724
+ mimeType = mimeType ?? sniffAudioMime(bytes);
148
725
  }
149
726
  }
150
727
  else if (aud.uri) {
151
- if (aud.uri.startsWith("data:")) {
152
- const idx = aud.uri.indexOf(",");
153
- const header = aud.uri.slice(5, idx);
154
- mimeType = header.split(";")[0] || mimeType;
155
- base64Data = aud.uri.slice(idx + 1);
728
+ const resolvedUri = aud.uri.startsWith("data:")
729
+ ? aud.uri
730
+ : await this.resolveUri(aud.uri);
731
+ if (resolvedUri.startsWith("data:")) {
732
+ base64Data = parseAudioDataUri(resolvedUri);
156
733
  }
157
734
  else {
158
- const resp = await this._fetch(aud.uri);
735
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
159
736
  if (!resp.ok)
160
737
  throw new Error(`Failed to fetch audio: ${resp.status}`);
161
- mimeType = resp.headers.get("content-type") ?? mimeType;
162
- base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
738
+ const bytes = Buffer.from(await resp.arrayBuffer());
739
+ mimeType =
740
+ stripMimeParams(resp.headers.get("content-type")) ??
741
+ mimeType ??
742
+ sniffAudioMime(bytes);
743
+ base64Data = bytes.toString("base64");
163
744
  }
164
745
  }
165
746
  else {
166
747
  base64Data = "";
167
748
  }
168
- return { inlineData: { mimeType, data: base64Data } };
749
+ return {
750
+ inlineData: { mimeType: geminiAudioMime(mimeType), data: base64Data }
751
+ };
752
+ }
753
+ if (content.type === "video") {
754
+ return await this.videoContentToGeminiPart(content.video);
169
755
  }
170
756
  return { text: "[unsupported content type]" };
171
757
  }
758
+ /**
759
+ * Source a video's bytes the way the image and audio branches do, then send
760
+ * them inline when small enough and through the Files API when not.
761
+ */
762
+ async videoContentToGeminiPart(video) {
763
+ let bytes;
764
+ let mimeType = video.mimeType;
765
+ const parseVideoDataUri = (uri) => {
766
+ const idx = uri.indexOf(",");
767
+ if (idx < 0)
768
+ throw new Error("Invalid video data URI");
769
+ const header = uri.slice(5, idx);
770
+ mimeType = mimeType ?? stripMimeParams(header.split(";base64")[0]);
771
+ return Buffer.from(uri.slice(idx + 1), "base64");
772
+ };
773
+ if (isNonEmptyString(video.data) ||
774
+ (video.data instanceof Uint8Array && video.data.length > 0)) {
775
+ if (isString(video.data)) {
776
+ bytes = video.data.startsWith("data:")
777
+ ? parseVideoDataUri(video.data)
778
+ : Buffer.from(video.data, "base64");
779
+ }
780
+ else {
781
+ bytes = Buffer.from(video.data);
782
+ }
783
+ }
784
+ else if (video.uri) {
785
+ const resolvedUri = video.uri.startsWith("data:")
786
+ ? video.uri
787
+ : await this.resolveUri(video.uri);
788
+ if (resolvedUri.startsWith("data:")) {
789
+ bytes = parseVideoDataUri(resolvedUri);
790
+ }
791
+ else {
792
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
793
+ if (!resp.ok)
794
+ throw new Error(`Failed to fetch video: ${resp.status}`);
795
+ mimeType =
796
+ stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
797
+ bytes = Buffer.from(await resp.arrayBuffer());
798
+ }
799
+ }
800
+ else {
801
+ bytes = Buffer.alloc(0);
802
+ }
803
+ const resolvedMime = mimeType ?? sniffVideoMime(bytes);
804
+ if (bytes.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
805
+ const fileUri = await this.uploadFileToGemini(bytes, resolvedMime);
806
+ return { fileData: { mimeType: resolvedMime, fileUri } };
807
+ }
808
+ return {
809
+ inlineData: { mimeType: resolvedMime, data: bytes.toString("base64") }
810
+ };
811
+ }
812
+ /**
813
+ * Upload bytes with the Files API resumable protocol and wait until the file
814
+ * is ACTIVE — Gemini rejects a `fileData` part that references a file still
815
+ * being processed. Returns the file's uri.
816
+ */
817
+ async uploadFileToGemini(bytes, mimeType) {
818
+ const startResp = await this._fetch(`${GEMINI_UPLOAD_BASE}?key=${this.apiKey}`, {
819
+ method: "POST",
820
+ headers: {
821
+ "X-Goog-Upload-Protocol": "resumable",
822
+ "X-Goog-Upload-Command": "start",
823
+ "X-Goog-Upload-Header-Content-Length": String(bytes.length),
824
+ "X-Goog-Upload-Header-Content-Type": mimeType,
825
+ "Content-Type": "application/json"
826
+ },
827
+ body: JSON.stringify({
828
+ file: { display_name: `nodetool-upload.${mimeType.split("/")[1]}` }
829
+ })
830
+ });
831
+ if (!startResp.ok) {
832
+ const errText = await startResp.text();
833
+ throw new Error(`Gemini file upload failed to start ${startResp.status}: ${errText}`);
834
+ }
835
+ const uploadUrl = startResp.headers.get("x-goog-upload-url");
836
+ if (!uploadUrl) {
837
+ throw new Error("Gemini file upload returned no X-Goog-Upload-URL");
838
+ }
839
+ const uploadResp = await this._fetch(uploadUrl, {
840
+ method: "POST",
841
+ headers: {
842
+ "Content-Length": String(bytes.length),
843
+ "X-Goog-Upload-Offset": "0",
844
+ "X-Goog-Upload-Command": "upload, finalize"
845
+ },
846
+ body: new Blob([new Uint8Array(bytes)], {
847
+ type: mimeType
848
+ })
849
+ });
850
+ if (!uploadResp.ok) {
851
+ const errText = await uploadResp.text();
852
+ throw new Error(`Gemini file upload failed ${uploadResp.status}: ${errText}`);
853
+ }
854
+ const uploaded = (await uploadResp.json());
855
+ let file = uploaded.file;
856
+ if (!file?.name || !file.uri) {
857
+ throw new Error("Gemini file upload returned no file record");
858
+ }
859
+ const fileUri = file.uri;
860
+ const fileName = file.name;
861
+ for (let poll = 0; poll < GEMINI_FILE_MAX_POLLS; poll++) {
862
+ if (file?.state === "ACTIVE")
863
+ return fileUri;
864
+ if (file?.state === "FAILED") {
865
+ throw new Error(`Gemini file processing failed: ${file.error?.message ?? "unknown error"}`);
866
+ }
867
+ await this._sleep(GEMINI_FILE_POLL_INTERVAL_MS);
868
+ const pollResp = await this._fetch(`${GEMINI_API_BASE}/${fileName}`, {
869
+ headers: { "x-goog-api-key": this.apiKey }
870
+ });
871
+ if (!pollResp.ok) {
872
+ const errText = await pollResp.text();
873
+ throw new Error(`Gemini file poll failed ${pollResp.status}: ${errText}`);
874
+ }
875
+ file = (await pollResp.json());
876
+ }
877
+ if (file?.state === "ACTIVE")
878
+ return fileUri;
879
+ throw new Error(`Gemini file ${fileName} did not become ACTIVE in time`);
880
+ }
172
881
  /**
173
882
  * Convert our Message array into Gemini contents + optional system instruction.
174
883
  */
175
- async convertMessages(messages) {
884
+ async convertMessages(messages, nameMap = new Map()) {
176
885
  let systemInstruction;
177
886
  const contents = [];
887
+ // Gemini correlates a tool result to its call by the function *name*, not
888
+ // by id (and our tool-call ids are synthesized — never valid Gemini
889
+ // function names). Map each tool-call id back to its function name so the
890
+ // `functionResponse.name` below matches the earlier `functionCall.name`.
891
+ const toolCallNames = new Map();
892
+ for (const m of messages) {
893
+ if (m.role === "assistant" && m.toolCalls) {
894
+ for (const tc of m.toolCalls) {
895
+ if (tc.id)
896
+ toolCallNames.set(tc.id, nameMap.get(tc.name) ?? tc.name);
897
+ }
898
+ }
899
+ }
178
900
  for (const msg of messages) {
179
901
  if (msg.role === "system") {
180
- systemInstruction =
181
- typeof msg.content === "string"
182
- ? msg.content
183
- : (msg.content ?? [])
184
- .filter((c) => c.type === "text")
185
- .map((c) => c.text)
186
- .join(" ");
902
+ const instruction = isString(msg.content)
903
+ ? msg.content
904
+ : (msg.content ?? [])
905
+ .filter((c) => c.type === "text")
906
+ .map((c) => c.text)
907
+ .join(" ");
908
+ systemInstruction = systemInstruction
909
+ ? `${systemInstruction}\n${instruction}`
910
+ : instruction;
187
911
  continue;
188
912
  }
189
913
  if (msg.role === "tool") {
190
- // Tool result → model role with functionResponse part
191
- const responseText = typeof msg.content === "string"
914
+ // Tool result → user role with functionResponse part. The name must
915
+ // match the originating functionCall's name, resolved from the call id.
916
+ const responseText = isString(msg.content)
192
917
  ? msg.content
193
918
  : JSON.stringify(msg.content);
194
- contents.push({
195
- role: "user",
196
- parts: [
197
- {
198
- functionResponse: {
199
- name: msg.toolCallId ?? "unknown",
200
- response: { result: responseText }
201
- }
202
- }
203
- ]
204
- });
919
+ const functionName = (msg.toolCallId ? toolCallNames.get(msg.toolCallId) : undefined) ??
920
+ msg.toolCallId ??
921
+ "unknown";
922
+ const responsePart = {
923
+ functionResponse: {
924
+ name: functionName,
925
+ id: msg.toolCallId ?? undefined,
926
+ response: { result: responseText }
927
+ }
928
+ };
929
+ // Merge parallel tool results into a single user turn so the request
930
+ // keeps alternating user/model roles.
931
+ const prev = contents[contents.length - 1];
932
+ if (prev &&
933
+ prev.role === "user" &&
934
+ prev.parts.length > 0 &&
935
+ prev.parts.every((p) => p.functionResponse !== undefined)) {
936
+ prev.parts.push(responsePart);
937
+ }
938
+ else {
939
+ appendGeminiContent(contents, {
940
+ role: "user",
941
+ parts: [responsePart]
942
+ });
943
+ }
205
944
  continue;
206
945
  }
207
946
  if (msg.role === "assistant") {
208
947
  // If we have raw Gemini parts (with thought content), replay them exactly
209
948
  if (msg._rawGeminiParts && Array.isArray(msg._rawGeminiParts)) {
210
- contents.push({
949
+ // Copy: the contents we build get merged and stamped below, and the
950
+ // message's own parts must survive that untouched for the next turn.
951
+ appendGeminiContent(contents, {
211
952
  role: "model",
212
- parts: msg._rawGeminiParts
953
+ parts: msg._rawGeminiParts.map((part) => ({
954
+ ...part
955
+ }))
213
956
  });
214
957
  continue;
215
958
  }
216
959
  const parts = [];
217
- // Tool calls
218
960
  if (msg.toolCalls && msg.toolCalls.length > 0) {
219
961
  for (const tc of msg.toolCalls) {
220
962
  const part = {
221
- functionCall: { name: tc.name, args: tc.args }
963
+ functionCall: {
964
+ id: tc.id,
965
+ name: nameMap.get(tc.name) ?? tc.name,
966
+ args: tc.args
967
+ }
222
968
  };
223
969
  if (tc.thought_signature) {
224
970
  part.thoughtSignature = tc.thought_signature;
@@ -226,8 +972,7 @@ export class GeminiProvider extends BaseProvider {
226
972
  parts.push(part);
227
973
  }
228
974
  }
229
- // Text / content
230
- if (typeof msg.content === "string" && msg.content) {
975
+ if (isNonEmptyString(msg.content)) {
231
976
  parts.push({ text: msg.content });
232
977
  }
233
978
  else if (Array.isArray(msg.content)) {
@@ -236,13 +981,12 @@ export class GeminiProvider extends BaseProvider {
236
981
  }
237
982
  }
238
983
  if (parts.length > 0) {
239
- contents.push({ role: "model", parts });
984
+ appendGeminiContent(contents, { role: "model", parts });
240
985
  }
241
986
  continue;
242
987
  }
243
- // user
244
988
  const parts = [];
245
- if (typeof msg.content === "string") {
989
+ if (isString(msg.content)) {
246
990
  parts.push({ text: msg.content });
247
991
  }
248
992
  else if (Array.isArray(msg.content)) {
@@ -251,17 +995,26 @@ export class GeminiProvider extends BaseProvider {
251
995
  }
252
996
  }
253
997
  if (parts.length > 0) {
254
- contents.push({ role: "user", parts });
998
+ appendGeminiContent(contents, { role: "user", parts });
255
999
  }
256
1000
  }
1001
+ signUnsignedFunctionCalls(contents);
257
1002
  return { contents, systemInstruction };
258
1003
  }
259
1004
  formatTools(tools) {
1005
+ return this.formatToolsForModel(tools, "gemini-3");
1006
+ }
1007
+ formatToolsForModel(tools, model) {
260
1008
  const nameMap = new Map();
261
1009
  const reverseMap = new Map();
262
1010
  const usedNames = new Set();
263
1011
  const declarations = [];
264
1012
  for (const tool of tools) {
1013
+ if ((tool.name === WEB_SEARCH_TOOL_NAME &&
1014
+ this.usesNativeWebSearch(model, tools)) ||
1015
+ tool.type === "code_interpreter") {
1016
+ continue;
1017
+ }
265
1018
  const original = tool.name;
266
1019
  let unique = sanitizeToolName(original);
267
1020
  let suffix = 2;
@@ -273,7 +1026,10 @@ export class GeminiProvider extends BaseProvider {
273
1026
  usedNames.add(unique);
274
1027
  nameMap.set(original, unique);
275
1028
  reverseMap.set(unique, original);
276
- const rawParameters = tool.inputSchema ?? { type: "object", properties: {} };
1029
+ const rawParameters = tool.inputSchema ?? {
1030
+ type: "object",
1031
+ properties: {}
1032
+ };
277
1033
  declarations.push({
278
1034
  name: unique,
279
1035
  description: tool.description ?? "",
@@ -286,20 +1042,82 @@ export class GeminiProvider extends BaseProvider {
286
1042
  reverseMap
287
1043
  };
288
1044
  }
1045
+ usesNativeWebSearch(model, tools) {
1046
+ const hasWebSearch = tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
1047
+ if (!hasWebSearch)
1048
+ return false;
1049
+ const hasCustomTools = this.hasCustomFunctionTools(tools);
1050
+ return !hasCustomTools || isGemini3Model(model);
1051
+ }
1052
+ hasCustomFunctionTools(tools) {
1053
+ return tools.some((tool) => tool.name !== WEB_SEARCH_TOOL_NAME &&
1054
+ tool.type !== "code_interpreter");
1055
+ }
1056
+ validateToolCombination(model, tools) {
1057
+ if (isGemini3Model(model))
1058
+ return;
1059
+ const hasCodeExecution = tools.some((tool) => tool.type === "code_interpreter");
1060
+ if (hasCodeExecution && this.hasCustomFunctionTools(tools)) {
1061
+ throw new Error(`Gemini code execution cannot be combined with other tools on ${model}; ` +
1062
+ "only Gemini 3 models support this combination");
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Fill in `tools` and `toolConfig` on a request body.
1067
+ *
1068
+ * Built-in tools (googleSearch, codeExecution) run server side. When they are
1069
+ * combined with functionDeclarations, Gemini rejects the request unless
1070
+ * `toolConfig.includeServerSideToolInvocations` is set — the built-in calls
1071
+ * and their results have to be echoed back into the conversation for the
1072
+ * function-calling loop to stay coherent.
1073
+ */
1074
+ applyTools(body, model, tools, geminiTools, nameMap, toolChoice) {
1075
+ if (geminiTools.length > 0) {
1076
+ body.tools = geminiTools;
1077
+ }
1078
+ let hasBuiltIn = false;
1079
+ if (this.usesNativeWebSearch(model, tools)) {
1080
+ body.tools = [...(body.tools ?? []), { googleSearch: {} }];
1081
+ hasBuiltIn = true;
1082
+ }
1083
+ if (tools.some((tool) => tool.type === "code_interpreter")) {
1084
+ body.tools = [...(body.tools ?? []), { codeExecution: {} }];
1085
+ hasBuiltIn = true;
1086
+ }
1087
+ const toolConfig = {};
1088
+ if (hasBuiltIn && geminiTools.length > 0) {
1089
+ toolConfig.includeServerSideToolInvocations = true;
1090
+ }
1091
+ if (toolChoice &&
1092
+ (toolChoice === "any" ? geminiTools.length > 0 : nameMap.has(toolChoice))) {
1093
+ const selected = toolChoice === "any"
1094
+ ? undefined
1095
+ : (nameMap.get(toolChoice) ?? sanitizeToolName(toolChoice));
1096
+ const functionCallingConfig = {
1097
+ mode: "ANY"
1098
+ };
1099
+ if (selected) {
1100
+ functionCallingConfig.allowedFunctionNames = [selected];
1101
+ }
1102
+ toolConfig.functionCallingConfig = functionCallingConfig;
1103
+ }
1104
+ if (Object.keys(toolConfig).length > 0) {
1105
+ body.toolConfig = toolConfig;
1106
+ }
1107
+ }
289
1108
  // ---------------------------------------------------------------------------
290
1109
  // Non-streaming generation
291
1110
  // ---------------------------------------------------------------------------
292
1111
  async generateMessage(args) {
293
- const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
294
- const { contents, systemInstruction } = await this.convertMessages(args.messages);
295
- const { geminiTools, reverseMap } = this.formatTools(tools);
1112
+ const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
1113
+ this.validateToolCombination(model, tools);
1114
+ const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
1115
+ const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
296
1116
  const body = { contents };
297
1117
  if (systemInstruction) {
298
1118
  body.systemInstruction = { parts: [{ text: systemInstruction }] };
299
1119
  }
300
- if (geminiTools.length > 0) {
301
- body.tools = geminiTools;
302
- }
1120
+ this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
303
1121
  const generationConfig = {
304
1122
  maxOutputTokens: maxTokens
305
1123
  };
@@ -307,13 +1125,19 @@ export class GeminiProvider extends BaseProvider {
307
1125
  generationConfig.temperature = temperature;
308
1126
  if (topP != null)
309
1127
  generationConfig.topP = topP;
1128
+ if (presencePenalty != null)
1129
+ generationConfig.presencePenalty = presencePenalty;
1130
+ if (frequencyPenalty != null)
1131
+ generationConfig.frequencyPenalty = frequencyPenalty;
310
1132
  body.generationConfig = generationConfig;
311
1133
  log.debug("Gemini request", { model });
312
1134
  const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
1135
+ this.recordRequestPayload(body);
313
1136
  const response = await this._fetch(url, {
314
1137
  method: "POST",
315
1138
  headers: { "Content-Type": "application/json" },
316
- body: JSON.stringify(body)
1139
+ body: JSON.stringify(body),
1140
+ signal: args.signal
317
1141
  });
318
1142
  if (!response.ok) {
319
1143
  const text = await response.text();
@@ -323,30 +1147,33 @@ export class GeminiProvider extends BaseProvider {
323
1147
  });
324
1148
  throw new Error(`Gemini API error ${response.status}: ${text}`);
325
1149
  }
326
- const data = (await response.json());
327
- if (data.error) {
328
- throw new Error(`Gemini API error: ${data.error.message}`);
329
- }
330
- const candidate = data.candidates?.[0];
331
- if (!candidate?.content?.parts) {
332
- throw new Error("Gemini returned no candidates");
333
- }
334
- return this.extractMessage(candidate.content.parts, reverseMap);
1150
+ const decoded = decodeGeminiGenerateContent(await response.json());
1151
+ this.trackGeminiUsage(model, decoded.usage);
1152
+ return this.extractMessage(decoded.parts, reverseMap);
1153
+ }
1154
+ /** Record token usage from a Gemini usageMetadata block (if present). */
1155
+ trackGeminiUsage(model, usage) {
1156
+ if (!usage)
1157
+ return;
1158
+ this.trackUsage(model, {
1159
+ inputTokens: usage.promptTokenCount ?? 0,
1160
+ outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
1161
+ cachedTokens: usage.cachedContentTokenCount ?? 0
1162
+ });
335
1163
  }
336
1164
  // ---------------------------------------------------------------------------
337
1165
  // Streaming generation
338
1166
  // ---------------------------------------------------------------------------
339
1167
  async *generateMessages(args) {
340
- const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
341
- const { contents, systemInstruction } = await this.convertMessages(args.messages);
342
- const { geminiTools, reverseMap } = this.formatTools(tools);
1168
+ const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
1169
+ this.validateToolCombination(model, tools);
1170
+ const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
1171
+ const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
343
1172
  const body = { contents };
344
1173
  if (systemInstruction) {
345
1174
  body.systemInstruction = { parts: [{ text: systemInstruction }] };
346
1175
  }
347
- if (geminiTools.length > 0) {
348
- body.tools = geminiTools;
349
- }
1176
+ this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
350
1177
  const generationConfig = {
351
1178
  maxOutputTokens: maxTokens
352
1179
  };
@@ -354,13 +1181,19 @@ export class GeminiProvider extends BaseProvider {
354
1181
  generationConfig.temperature = temperature;
355
1182
  if (topP != null)
356
1183
  generationConfig.topP = topP;
1184
+ if (presencePenalty != null)
1185
+ generationConfig.presencePenalty = presencePenalty;
1186
+ if (frequencyPenalty != null)
1187
+ generationConfig.frequencyPenalty = frequencyPenalty;
357
1188
  body.generationConfig = generationConfig;
358
1189
  log.debug("Gemini request", { model });
359
1190
  const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
1191
+ this.recordRequestPayload(body);
360
1192
  const response = await this._fetch(url, {
361
1193
  method: "POST",
362
1194
  headers: { "Content-Type": "application/json" },
363
- body: JSON.stringify(body)
1195
+ body: JSON.stringify(body),
1196
+ signal: args.signal
364
1197
  });
365
1198
  if (!response.ok) {
366
1199
  const text = await response.text();
@@ -373,71 +1206,51 @@ export class GeminiProvider extends BaseProvider {
373
1206
  if (!response.body) {
374
1207
  throw new Error("Gemini streaming response has no body");
375
1208
  }
376
- const reader = response.body.getReader();
377
- const decoder = new TextDecoder();
378
- let buffer = "";
379
1209
  // Accumulate all parts across SSE events for raw replay.
380
1210
  // Gemini thinking models emit thought parts and function calls across
381
1211
  // separate SSE events, but they must all be sent back together.
382
1212
  const allParts = [];
383
1213
  const pendingToolCalls = [];
384
- try {
385
- while (true) {
386
- const { done, value } = await reader.read();
387
- if (done)
388
- break;
389
- buffer += decoder.decode(value, { stream: true });
390
- const lines = buffer.split("\n");
391
- buffer = lines.pop() ?? "";
392
- for (const line of lines) {
393
- if (!line.startsWith("data: "))
394
- continue;
395
- const jsonStr = line.slice(6).trim();
396
- if (!jsonStr || jsonStr === "[DONE]")
397
- continue;
398
- let event;
399
- try {
400
- event = JSON.parse(jsonStr);
401
- }
402
- catch {
403
- continue;
404
- }
405
- const parts = event.candidates?.[0]?.content?.parts;
406
- if (!parts)
407
- continue;
408
- for (const part of parts) {
409
- allParts.push(part);
410
- if (part.text !== undefined && !part.thought) {
411
- const chunk = {
412
- type: "chunk",
413
- content: part.text,
414
- done: false
415
- };
416
- yield chunk;
417
- }
418
- else if (part.functionCall) {
419
- const originalName = reverseMap.get(part.functionCall.name) ??
420
- part.functionCall.name;
421
- const toolCall = {
422
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
423
- name: originalName,
424
- args: part.functionCall.args ?? {}
425
- };
426
- if (part.thoughtSignature) {
427
- toolCall.thought_signature = part.thoughtSignature;
428
- }
429
- pendingToolCalls.push(toolCall);
430
- }
431
- // Thought text parts are silently accumulated in allParts
1214
+ // Gemini SSE reports CUMULATIVE usageMetadata; keep the last one seen and
1215
+ // record it once after the stream (accumulating each event would over-count).
1216
+ let lastUsage;
1217
+ for await (const event of decodeGeminiSse(response.body, args.signal)) {
1218
+ const eventError = geminiResponseError(event);
1219
+ if (eventError)
1220
+ throw eventError;
1221
+ if (event.usageMetadata)
1222
+ lastUsage = event.usageMetadata;
1223
+ const parts = event.candidates?.[0]?.content?.parts;
1224
+ if (!parts)
1225
+ continue;
1226
+ for (const part of parts) {
1227
+ allParts.push(part);
1228
+ if (part.text !== undefined && !part.thought) {
1229
+ const chunk = {
1230
+ type: "chunk",
1231
+ content: part.text,
1232
+ done: false
1233
+ };
1234
+ yield chunk;
1235
+ }
1236
+ else if (part.functionCall) {
1237
+ const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
1238
+ const toolCall = {
1239
+ id: part.functionCall.id ??
1240
+ `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1241
+ name: originalName,
1242
+ args: part.functionCall.args ?? {}
1243
+ };
1244
+ if (part.thoughtSignature) {
1245
+ toolCall.thought_signature = part.thoughtSignature;
432
1246
  }
1247
+ pendingToolCalls.push(toolCall);
433
1248
  }
434
1249
  }
435
1250
  }
436
- finally {
437
- reader.releaseLock();
438
- }
1251
+ this.trackGeminiUsage(model, lastUsage);
439
1252
  // Attach accumulated raw parts to tool calls for thought replay
440
- const hasThoughts = allParts.some((p) => p.thought);
1253
+ const hasThoughts = allParts.some((p) => p.thought || p.thoughtSignature);
441
1254
  for (const tc of pendingToolCalls) {
442
1255
  if (hasThoughts) {
443
1256
  tc._rawGeminiParts = allParts;
@@ -459,13 +1272,14 @@ export class GeminiProvider extends BaseProvider {
459
1272
  const textParts = [];
460
1273
  const toolCalls = [];
461
1274
  for (const part of parts) {
462
- if (part.text !== undefined) {
1275
+ if (part.text !== undefined && !part.thought) {
463
1276
  textParts.push(part.text);
464
1277
  }
465
1278
  else if (part.functionCall) {
466
1279
  const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
467
1280
  const tc = {
468
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1281
+ id: part.functionCall.id ??
1282
+ `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
469
1283
  name: originalName,
470
1284
  args: part.functionCall.args ?? {}
471
1285
  };
@@ -475,7 +1289,7 @@ export class GeminiProvider extends BaseProvider {
475
1289
  toolCalls.push(tc);
476
1290
  }
477
1291
  }
478
- const hasThoughts = parts.some((p) => p.thought);
1292
+ const hasThoughts = parts.some((p) => p.thought || p.thoughtSignature);
479
1293
  const msg = {
480
1294
  role: "assistant",
481
1295
  content: textParts.join("") || null,
@@ -492,11 +1306,50 @@ export class GeminiProvider extends BaseProvider {
492
1306
  async getAvailableImageModels() {
493
1307
  return [
494
1308
  {
495
- id: "gemini-2.0-flash-preview-image-generation",
496
- name: "Gemini 2.0 Flash Image Gen",
497
- provider: "gemini"
1309
+ id: "gemini-3.1-flash-image",
1310
+ name: "Gemini 3.1 Flash Image",
1311
+ provider: "gemini",
1312
+ supportedTasks: ["text_to_image", "image_to_image"],
1313
+ aspectRatios: [
1314
+ "1:1",
1315
+ "2:3",
1316
+ "3:2",
1317
+ "3:4",
1318
+ "4:3",
1319
+ "4:5",
1320
+ "5:4",
1321
+ "9:16",
1322
+ "16:9",
1323
+ "21:9"
1324
+ ],
1325
+ resolutions: ["1K", "2K", "4K"]
1326
+ },
1327
+ {
1328
+ id: "gemini-3.1-flash-lite-image",
1329
+ name: "Gemini 3.1 Flash-Lite Image",
1330
+ provider: "gemini",
1331
+ supportedTasks: ["text_to_image", "image_to_image"],
1332
+ aspectRatios: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"]
498
1333
  },
499
- { id: "imagen-3.0-generate-002", name: "Imagen 3.0", provider: "gemini" }
1334
+ {
1335
+ id: "gemini-3-pro-image",
1336
+ name: "Gemini 3 Pro Image",
1337
+ provider: "gemini",
1338
+ supportedTasks: ["text_to_image", "image_to_image"],
1339
+ aspectRatios: [
1340
+ "1:1",
1341
+ "2:3",
1342
+ "3:2",
1343
+ "3:4",
1344
+ "4:3",
1345
+ "4:5",
1346
+ "5:4",
1347
+ "9:16",
1348
+ "16:9",
1349
+ "21:9"
1350
+ ],
1351
+ resolutions: ["1K", "2K", "4K"]
1352
+ }
500
1353
  ];
501
1354
  }
502
1355
  async getAvailableTTSModels() {
@@ -533,6 +1386,30 @@ export class GeminiProvider extends BaseProvider {
533
1386
  "Sulafat"
534
1387
  ];
535
1388
  return [
1389
+ {
1390
+ id: "gemini-3.8-flash-tts",
1391
+ name: "Gemini 3.8 Flash TTS",
1392
+ provider: "gemini",
1393
+ voices
1394
+ },
1395
+ {
1396
+ id: "gemini-3.8-flash-lite-tts",
1397
+ name: "Gemini 3.8 Flash-Lite TTS",
1398
+ provider: "gemini",
1399
+ voices
1400
+ },
1401
+ {
1402
+ id: "gemini-3.1-flash-tts-preview",
1403
+ name: "Gemini 3.1 Flash TTS Preview",
1404
+ provider: "gemini",
1405
+ voices
1406
+ },
1407
+ {
1408
+ id: "gemini-2.5-flash-preview-tts",
1409
+ name: "Gemini 2.5 Flash TTS",
1410
+ provider: "gemini",
1411
+ voices
1412
+ },
536
1413
  {
537
1414
  id: "gemini-2.5-pro-preview-tts",
538
1415
  name: "Gemini 2.5 Pro TTS",
@@ -543,27 +1420,51 @@ export class GeminiProvider extends BaseProvider {
543
1420
  }
544
1421
  async getAvailableASRModels() {
545
1422
  return [
546
- { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", provider: "gemini" },
547
- { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", provider: "gemini" }
1423
+ {
1424
+ // The transcription-specific model. It answers `generateContent`, so
1425
+ // it runs through the same path as the general models below.
1426
+ id: "gemini-3.5-transcribe",
1427
+ name: "Gemini 3.5 Transcribe",
1428
+ provider: "gemini"
1429
+ },
1430
+ {
1431
+ id: "gemini-3.5-flash",
1432
+ name: "Gemini 3.5 Flash",
1433
+ provider: "gemini"
1434
+ },
1435
+ {
1436
+ id: "gemini-3.1-flash-lite",
1437
+ name: "Gemini 3.1 Flash-Lite",
1438
+ provider: "gemini"
1439
+ }
548
1440
  ];
549
1441
  }
550
1442
  async getAvailableVideoModels() {
1443
+ return [...GEMINI_VIDEO_MODELS];
1444
+ }
1445
+ async getAvailableMusicModels() {
551
1446
  return [
552
1447
  {
553
- id: "veo-3.1-generate-preview",
554
- name: "Veo 3.1 Preview",
555
- provider: "gemini"
1448
+ id: "lyria-3.5",
1449
+ name: "Lyria 3.5",
1450
+ provider: "gemini",
1451
+ supportedTasks: ["text_to_music"]
556
1452
  },
557
- { id: "veo-2.0-generate-001", name: "Veo 2.0", provider: "gemini" }
1453
+ {
1454
+ id: "lyria-3-clip-preview",
1455
+ name: "Lyria 3 Clip Preview",
1456
+ provider: "gemini",
1457
+ supportedTasks: ["text_to_music"]
1458
+ }
558
1459
  ];
559
1460
  }
560
1461
  async getAvailableEmbeddingModels() {
561
1462
  return [
562
1463
  {
563
- id: "text-embedding-004",
564
- name: "Text Embedding 004",
1464
+ id: "gemini-embedding-2",
1465
+ name: "Gemini Embedding 2",
565
1466
  provider: "gemini",
566
- dimensions: 768
1467
+ dimensions: 3072
567
1468
  },
568
1469
  {
569
1470
  id: "gemini-embedding-001",
@@ -581,7 +1482,7 @@ export class GeminiProvider extends BaseProvider {
581
1482
  if (!text || (Array.isArray(text) && text.length === 0)) {
582
1483
  throw new Error("text must not be empty");
583
1484
  }
584
- const texts = typeof text === "string" ? [text] : text;
1485
+ const texts = isString(text) ? [text] : text;
585
1486
  // Gemini embedContent supports a single content; batch by calling per text
586
1487
  const embeddings = [];
587
1488
  for (const t of texts) {
@@ -605,7 +1506,9 @@ export class GeminiProvider extends BaseProvider {
605
1506
  if (!data.embedding?.values) {
606
1507
  throw new Error("No embedding returned from Gemini API");
607
1508
  }
608
- embeddings.push(data.embedding.values);
1509
+ embeddings.push(dimensions && dimensions < 3072
1510
+ ? normalizeEmbedding(data.embedding.values)
1511
+ : data.embedding.values);
609
1512
  }
610
1513
  return embeddings;
611
1514
  }
@@ -619,11 +1522,20 @@ export class GeminiProvider extends BaseProvider {
619
1522
  const modelId = params.model.id;
620
1523
  if (modelId.startsWith("gemini-")) {
621
1524
  // Use generateContent with IMAGE response modality
1525
+ const imageConfig = {};
1526
+ if (params.aspectRatio)
1527
+ imageConfig.aspectRatio = params.aspectRatio;
1528
+ if (params.resolution)
1529
+ imageConfig.imageSize = params.resolution;
1530
+ const generationConfig = {
1531
+ responseModalities: ["IMAGE", "TEXT"]
1532
+ };
1533
+ if (Object.keys(imageConfig).length > 0) {
1534
+ generationConfig.imageConfig = imageConfig;
1535
+ }
622
1536
  const body = {
623
1537
  contents: [{ role: "user", parts: [{ text: params.prompt }] }],
624
- generationConfig: {
625
- responseModalities: ["IMAGE", "TEXT"]
626
- }
1538
+ generationConfig
627
1539
  };
628
1540
  const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
629
1541
  const response = await this._fetch(url, {
@@ -635,7 +1547,7 @@ export class GeminiProvider extends BaseProvider {
635
1547
  const errText = await response.text();
636
1548
  throw new Error(`Gemini text-to-image failed ${response.status}: ${errText}`);
637
1549
  }
638
- const data = (await response.json());
1550
+ const data = parseGeminiResponse(await response.json());
639
1551
  const parts = data.candidates?.[0]?.content?.parts;
640
1552
  if (!parts)
641
1553
  throw new Error("No candidates in response");
@@ -646,33 +1558,13 @@ export class GeminiProvider extends BaseProvider {
646
1558
  }
647
1559
  throw new Error("No image data returned in response");
648
1560
  }
649
- // Imagen models use generateImages endpoint
650
- const body = {
651
- instances: [{ prompt: params.prompt }],
652
- parameters: { sampleCount: 1 }
653
- };
654
- const url = `${GEMINI_API_BASE}/models/${modelId}:generateImages?key=${this.apiKey}`;
655
- const response = await this._fetch(url, {
656
- method: "POST",
657
- headers: { "Content-Type": "application/json" },
658
- body: JSON.stringify(body)
659
- });
660
- if (!response.ok) {
661
- const errText = await response.text();
662
- throw new Error(`Gemini image generation failed ${response.status}: ${errText}`);
663
- }
664
- const data = (await response.json());
665
- // Try predictions format first (Vertex-style), then generatedImages
666
- const b64 = data.predictions?.[0]?.bytesBase64Encoded ??
667
- data.generatedImages?.[0]?.image?.imageBytes;
668
- if (!b64)
669
- throw new Error("No image data in response");
670
- return Uint8Array.from(Buffer.from(b64, "base64"));
1561
+ // Google shut down Imagen in the Gemini API; `predict` now answers 404.
1562
+ throw new Error(`Model ${modelId} is not available. Use a gemini-*-image model for text-to-image.`);
671
1563
  }
672
1564
  // ---------------------------------------------------------------------------
673
1565
  // Image-to-image
674
1566
  // ---------------------------------------------------------------------------
675
- async imageToImage(image, params) {
1567
+ async imageToImage(images, params) {
676
1568
  if (!params.prompt) {
677
1569
  throw new Error("The input prompt cannot be empty.");
678
1570
  }
@@ -680,20 +1572,36 @@ export class GeminiProvider extends BaseProvider {
680
1572
  if (!modelId.startsWith("gemini-")) {
681
1573
  throw new Error(`Model ${modelId} does not support image-to-image. Only gemini-* models supported.`);
682
1574
  }
683
- const imageBase64 = Buffer.from(image).toString("base64");
1575
+ const imageParts = images
1576
+ .filter((b) => b && b.length > 0)
1577
+ .map((b) => ({
1578
+ inlineData: {
1579
+ mimeType: "image/png",
1580
+ data: Buffer.from(b).toString("base64")
1581
+ }
1582
+ }));
1583
+ if (imageParts.length === 0) {
1584
+ throw new Error("At least one input image is required");
1585
+ }
1586
+ const imageConfig = {};
1587
+ if (params.aspectRatio)
1588
+ imageConfig.aspectRatio = params.aspectRatio;
1589
+ if (params.resolution)
1590
+ imageConfig.imageSize = params.resolution;
1591
+ const generationConfig = {
1592
+ responseModalities: ["IMAGE", "TEXT"]
1593
+ };
1594
+ if (Object.keys(imageConfig).length > 0) {
1595
+ generationConfig.imageConfig = imageConfig;
1596
+ }
684
1597
  const body = {
685
1598
  contents: [
686
1599
  {
687
1600
  role: "user",
688
- parts: [
689
- { text: params.prompt },
690
- { inlineData: { mimeType: "image/png", data: imageBase64 } }
691
- ]
1601
+ parts: [{ text: params.prompt }, ...imageParts]
692
1602
  }
693
1603
  ],
694
- generationConfig: {
695
- responseModalities: ["IMAGE", "TEXT"]
696
- }
1604
+ generationConfig
697
1605
  };
698
1606
  const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
699
1607
  const response = await this._fetch(url, {
@@ -705,7 +1613,7 @@ export class GeminiProvider extends BaseProvider {
705
1613
  const errText = await response.text();
706
1614
  throw new Error(`Gemini image-to-image failed ${response.status}: ${errText}`);
707
1615
  }
708
- const data = (await response.json());
1616
+ const data = parseGeminiResponse(await response.json());
709
1617
  const parts = data.candidates?.[0]?.content?.parts;
710
1618
  if (!parts)
711
1619
  throw new Error("No candidates in response");
@@ -720,7 +1628,14 @@ export class GeminiProvider extends BaseProvider {
720
1628
  // Text-to-speech
721
1629
  // ---------------------------------------------------------------------------
722
1630
  async *textToSpeech(args) {
723
- const { text, model, voice = "Puck" } = args;
1631
+ const { model, voice = "Puck" } = args;
1632
+ const text = [
1633
+ args.language ? `Speak in ${args.language}.` : undefined,
1634
+ args.instructions,
1635
+ args.text
1636
+ ]
1637
+ .filter((value) => value !== undefined)
1638
+ .join(" ");
724
1639
  const body = {
725
1640
  contents: [{ role: "user", parts: [{ text }] }],
726
1641
  generationConfig: {
@@ -742,7 +1657,7 @@ export class GeminiProvider extends BaseProvider {
742
1657
  const errText = await response.text();
743
1658
  throw new Error(`Gemini TTS failed ${response.status}: ${errText}`);
744
1659
  }
745
- const data = (await response.json());
1660
+ const data = parseGeminiResponse(await response.json());
746
1661
  const parts = data.candidates?.[0]?.content?.parts;
747
1662
  if (!parts)
748
1663
  throw new Error("No audio in response");
@@ -763,31 +1678,34 @@ export class GeminiProvider extends BaseProvider {
763
1678
  if (!audio || audio.length === 0) {
764
1679
  throw new Error("audio must not be empty");
765
1680
  }
766
- // Detect MIME type from audio header
767
- let mimeType = "audio/wav";
1681
+ if (audio.length > 20 * 1024 * 1024) {
1682
+ throw new Error("Gemini inline audio is limited to 20 MB; upload the audio with the File API first");
1683
+ }
1684
+ // Detect MIME type from the audio header.
1685
+ let mimeType = geminiAudioMime(sniffAudioMime(audio));
768
1686
  if (audio[0] === 0x52 &&
769
1687
  audio[1] === 0x49 &&
770
1688
  audio[2] === 0x46 &&
771
1689
  audio[3] === 0x46) {
772
- mimeType = "audio/wav"; // RIFF
1690
+ mimeType = "audio/wav";
773
1691
  }
774
1692
  else if (audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) {
775
- mimeType = "audio/mp3"; // ID3
1693
+ mimeType = "audio/mp3";
776
1694
  }
777
1695
  else if (audio[0] === 0xff && (audio[1] === 0xfb || audio[1] === 0xf3)) {
778
- mimeType = "audio/mp3"; // MPEG sync
1696
+ mimeType = "audio/mp3";
779
1697
  }
780
1698
  else if (audio[0] === 0x66 &&
781
1699
  audio[1] === 0x4c &&
782
1700
  audio[2] === 0x61 &&
783
1701
  audio[3] === 0x43) {
784
- mimeType = "audio/flac"; // fLaC
1702
+ mimeType = "audio/flac";
785
1703
  }
786
1704
  else if (audio[0] === 0x4f &&
787
1705
  audio[1] === 0x67 &&
788
1706
  audio[2] === 0x67 &&
789
1707
  audio[3] === 0x53) {
790
- mimeType = "audio/ogg"; // OggS
1708
+ mimeType = "audio/ogg";
791
1709
  }
792
1710
  let promptText = args.prompt ?? "Transcribe this audio to text.";
793
1711
  if (language) {
@@ -818,7 +1736,7 @@ export class GeminiProvider extends BaseProvider {
818
1736
  const errText = await response.text();
819
1737
  throw new Error(`Gemini ASR failed ${response.status}: ${errText}`);
820
1738
  }
821
- const data = (await response.json());
1739
+ const data = parseGeminiResponse(await response.json());
822
1740
  const parts = data.candidates?.[0]?.content?.parts;
823
1741
  if (!parts)
824
1742
  return { text: "" };
@@ -828,142 +1746,423 @@ export class GeminiProvider extends BaseProvider {
828
1746
  .join("");
829
1747
  return { text };
830
1748
  }
831
- // ---------------------------------------------------------------------------
832
- // Text-to-video (Veo models — async operation with polling)
833
- // ---------------------------------------------------------------------------
834
- async textToVideo(params) {
835
- if (!params.prompt) {
836
- throw new Error("The input prompt cannot be empty.");
837
- }
838
- const modelId = params.model.id;
839
- if (!modelId.startsWith("veo-")) {
840
- throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support text-to-video.`);
841
- }
842
- const body = {
843
- instances: [{ prompt: params.prompt }]
844
- };
1749
+ buildVideoParameters(params) {
845
1750
  const parameters = {};
846
- if (params.negativePrompt)
1751
+ if (params.negativePrompt) {
847
1752
  parameters.negativePrompt = params.negativePrompt;
848
- if (params.aspectRatio)
1753
+ }
1754
+ if (params.aspectRatio) {
849
1755
  parameters.aspectRatio = params.aspectRatio;
850
- if (params.seed != null)
1756
+ }
1757
+ if (params.resolution) {
1758
+ parameters.resolution = params.resolution;
1759
+ }
1760
+ if (params.durationSeconds != null) {
1761
+ parameters.durationSeconds = params.durationSeconds;
1762
+ }
1763
+ if (params.seed != null) {
851
1764
  parameters.seed = params.seed;
852
- if (Object.keys(parameters).length > 0)
853
- body.parameters = parameters;
854
- // Initiate async generation
855
- const url = `${GEMINI_API_BASE}/models/${modelId}:generateVideos?key=${this.apiKey}`;
856
- const response = await this._fetch(url, {
857
- method: "POST",
858
- headers: { "Content-Type": "application/json" },
859
- body: JSON.stringify(body)
860
- });
861
- if (!response.ok) {
862
- const errText = await response.text();
863
- throw new Error(`Gemini video generation failed ${response.status}: ${errText}`);
864
1765
  }
865
- const operation = (await response.json());
866
- // Poll for completion
867
- const maxWait = 600_000; // 10 minutes
1766
+ return parameters;
1767
+ }
1768
+ getVideoUri(operation) {
1769
+ return (operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video
1770
+ ?.uri ?? operation.response?.generatedVideos?.[0]?.video?.uri);
1771
+ }
1772
+ /**
1773
+ * The refusal to raise when an operation came back with no video, or null
1774
+ * when nothing says the filter is why. A filtered take leaves the operation
1775
+ * successful and the samples empty, so without this the caller sees only
1776
+ * "No video URI in response" — indistinguishable from a broken response, and
1777
+ * fatal to a run that has already paid for its other shots.
1778
+ */
1779
+ videoContentFilterRefusal(operation, modelId) {
1780
+ const rai = operation.response?.generateVideoResponse ?? operation.response;
1781
+ const reasons = rai?.raiMediaFilteredReasons ?? [];
1782
+ const filtered = rai?.raiMediaFilteredCount ?? 0;
1783
+ if (filtered <= 0 && reasons.length === 0)
1784
+ return null;
1785
+ const detail = reasons.length
1786
+ ? reasons.join("; ")
1787
+ : "no reason given by the provider";
1788
+ return new ContentFilterRefusal(`Veo filtered every generated video for this prompt: ${detail}`, { provider: "gemini", model: modelId, reasons });
1789
+ }
1790
+ async waitForVideoOperation(operation, timeoutSeconds, signal) {
1791
+ const maxWait = timeoutSeconds && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 600_000;
868
1792
  const pollInterval = 10_000;
869
1793
  let elapsed = 0;
870
1794
  let current = operation;
871
1795
  while (!current.done && elapsed < maxWait) {
872
- await new Promise((r) => setTimeout(r, pollInterval));
1796
+ await new Promise((resolve, reject) => {
1797
+ const onAbort = () => {
1798
+ clearTimeout(timer);
1799
+ reject(abortError(signal));
1800
+ };
1801
+ const timer = setTimeout(() => {
1802
+ signal?.removeEventListener("abort", onAbort);
1803
+ resolve();
1804
+ }, pollInterval);
1805
+ signal?.addEventListener("abort", onAbort, { once: true });
1806
+ });
873
1807
  elapsed += pollInterval;
874
- if (!current.name)
1808
+ if (!current.name) {
875
1809
  throw new Error("No operation name for polling");
876
- const pollUrl = `${GEMINI_API_BASE}/${current.name}?key=${this.apiKey}`;
877
- const pollResp = await this._fetch(pollUrl);
1810
+ }
1811
+ const pollResp = await this._fetch(`${GEMINI_API_BASE}/${current.name}`, {
1812
+ headers: { "x-goog-api-key": this.apiKey },
1813
+ signal
1814
+ });
878
1815
  if (!pollResp.ok) {
879
1816
  const errText = await pollResp.text();
880
1817
  throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
881
1818
  }
882
1819
  current = (await pollResp.json());
883
1820
  }
884
- if (!current.done)
1821
+ if (!current.done) {
885
1822
  throw new Error("Video generation timed out");
886
- const videoUri = current.response?.generatedVideos?.[0]?.video?.uri;
887
- if (!videoUri)
1823
+ }
1824
+ if (current.error?.message) {
1825
+ // Vertex answers a filtered prompt through the operation's error slot
1826
+ // ("videos were filtered out because they violated ... usage
1827
+ // guidelines"), so the refusal has to be recognized here too.
1828
+ if (isContentFilterRefusal(current.error.message)) {
1829
+ throw new ContentFilterRefusal(current.error.message, {
1830
+ provider: "gemini"
1831
+ });
1832
+ }
1833
+ throw new Error(`Gemini video generation failed: ${current.error.message}`);
1834
+ }
1835
+ return current;
1836
+ }
1837
+ async downloadGeminiMedia(uri, signal) {
1838
+ const hostname = new URL(uri).hostname;
1839
+ const headers = hostname === "generativelanguage.googleapis.com"
1840
+ ? { "x-goog-api-key": this.apiKey }
1841
+ : undefined;
1842
+ const response = await safeFetch(uri, { headers, signal }, 5, this._fetch);
1843
+ if (!response.ok) {
1844
+ throw new Error(`Gemini media download failed: ${response.status}`);
1845
+ }
1846
+ return new Uint8Array(await response.arrayBuffer());
1847
+ }
1848
+ /**
1849
+ * Submit a Veo `predictLongRunning` request, wait for the operation, and
1850
+ * download the first generated video.
1851
+ */
1852
+ async runVeoOperation(modelId, body, label, options) {
1853
+ const signal = combineSignals(options.signal, options.timeoutSeconds);
1854
+ this.recordRequestPayload(body);
1855
+ const response = await this._fetch(`${GEMINI_API_BASE}/models/${modelId}:predictLongRunning`, {
1856
+ method: "POST",
1857
+ headers: {
1858
+ "Content-Type": "application/json",
1859
+ "x-goog-api-key": this.apiKey
1860
+ },
1861
+ body: JSON.stringify(body),
1862
+ signal
1863
+ });
1864
+ if (!response.ok) {
1865
+ const errText = await response.text();
1866
+ throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
1867
+ }
1868
+ const operation = await this.waitForVideoOperation((await response.json()), options.timeoutSeconds, signal);
1869
+ const videoUri = this.getVideoUri(operation);
1870
+ if (!videoUri) {
1871
+ const refusal = this.videoContentFilterRefusal(operation, modelId);
1872
+ if (refusal)
1873
+ throw refusal;
888
1874
  throw new Error("No video URI in response");
889
- // Download the video
890
- const dlResp = await this._fetch(`${videoUri}&key=${this.apiKey}`);
891
- if (!dlResp.ok)
892
- throw new Error(`Video download failed: ${dlResp.status}`);
893
- return new Uint8Array(await dlResp.arrayBuffer());
1875
+ }
1876
+ return this.downloadGeminiMedia(videoUri, signal);
1877
+ }
1878
+ /** Send one request to the Interactions API and return the finished interaction. */
1879
+ async createInteraction(body, label, modelId, signal) {
1880
+ this.recordRequestPayload(body);
1881
+ const response = await this._fetch(`${GEMINI_API_BASE}/interactions`, {
1882
+ method: "POST",
1883
+ headers: {
1884
+ "Content-Type": "application/json",
1885
+ "x-goog-api-key": this.apiKey
1886
+ },
1887
+ body: JSON.stringify(body),
1888
+ signal
1889
+ });
1890
+ if (!response.ok) {
1891
+ const errText = await response.text();
1892
+ if (isContentFilterRefusal(errText)) {
1893
+ throw new ContentFilterRefusal(errText, {
1894
+ provider: "gemini",
1895
+ model: modelId
1896
+ });
1897
+ }
1898
+ throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
1899
+ }
1900
+ const interaction = (await response.json());
1901
+ if (interaction.status && interaction.status !== "completed") {
1902
+ throw new Error(`Gemini ${label} ended with status ${interaction.status}`);
1903
+ }
1904
+ return interaction;
1905
+ }
1906
+ /** The first `type` output of an interaction, inline or behind a file uri. */
1907
+ async interactionMedia(interaction, type, signal) {
1908
+ for (const step of interaction.steps ?? []) {
1909
+ for (const content of step.content ?? []) {
1910
+ if (content.type !== type)
1911
+ continue;
1912
+ if (content.data) {
1913
+ return {
1914
+ data: new Uint8Array(Buffer.from(content.data, "base64")),
1915
+ mimeType: content.mime_type
1916
+ };
1917
+ }
1918
+ if (content.uri) {
1919
+ return {
1920
+ data: await this.downloadGeminiMedia(content.uri, signal),
1921
+ mimeType: content.mime_type
1922
+ };
1923
+ }
1924
+ }
1925
+ }
1926
+ throw new Error(`Gemini returned no ${type} output`);
1927
+ }
1928
+ /** An Interactions API video block: inline, or uploaded when too large. */
1929
+ async omniVideoInput(video) {
1930
+ const mimeType = sniffVideoMime(video);
1931
+ if (video.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
1932
+ const uri = await this.uploadFileToGemini(video, mimeType);
1933
+ return { type: "video", uri, mime_type: mimeType };
1934
+ }
1935
+ return {
1936
+ type: "video",
1937
+ data: Buffer.from(video).toString("base64"),
1938
+ mime_type: mimeType
1939
+ };
1940
+ }
1941
+ /** Generate or edit a video with Gemini Omni through the Interactions API. */
1942
+ async runOmniVideo(modelId, input, options) {
1943
+ const responseFormat = { type: "video" };
1944
+ if (options.aspectRatio)
1945
+ responseFormat.aspect_ratio = options.aspectRatio;
1946
+ if (options.resolution)
1947
+ responseFormat.resolution = options.resolution;
1948
+ const signal = combineSignals(options.signal, options.timeoutSeconds);
1949
+ const interaction = await this.createInteraction({ model: modelId, input, response_format: responseFormat }, "video generation", modelId, signal);
1950
+ return (await this.interactionMedia(interaction, "video", signal)).data;
894
1951
  }
895
1952
  // ---------------------------------------------------------------------------
896
- // Image-to-video (Veo models)
1953
+ // Text-to-video (Veo: long-running operation; Omni: Interactions API)
1954
+ // ---------------------------------------------------------------------------
1955
+ async textToVideo(params) {
1956
+ if (!params.prompt) {
1957
+ throw new Error("The input prompt cannot be empty.");
1958
+ }
1959
+ const modelId = params.model.id;
1960
+ if (isOmniVideoModel(modelId)) {
1961
+ return this.runOmniVideo(modelId, [{ type: "text", text: params.prompt }], params);
1962
+ }
1963
+ if (!modelId.startsWith("veo-")) {
1964
+ throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for text-to-video.`);
1965
+ }
1966
+ const body = {
1967
+ instances: [{ prompt: params.prompt }]
1968
+ };
1969
+ const parameters = this.buildVideoParameters(params);
1970
+ if (Object.keys(parameters).length > 0) {
1971
+ body.parameters = parameters;
1972
+ }
1973
+ return this.runVeoOperation(modelId, body, "video generation", params);
1974
+ }
1975
+ // ---------------------------------------------------------------------------
1976
+ // Image-to-video
897
1977
  // ---------------------------------------------------------------------------
898
1978
  async imageToVideo(image, params) {
899
1979
  if (!image || image.length === 0) {
900
1980
  throw new Error("Input image cannot be empty.");
901
1981
  }
902
1982
  const modelId = params.model.id;
1983
+ const prompt = params.prompt || "Animate this image";
1984
+ if (isOmniVideoModel(modelId)) {
1985
+ if (params.endImage?.length) {
1986
+ throw new Error(`Gemini model ${modelId} does not accept a last frame`);
1987
+ }
1988
+ return this.runOmniVideo(modelId, [
1989
+ {
1990
+ type: "image",
1991
+ data: Buffer.from(image).toString("base64"),
1992
+ mime_type: detectImageMime(image)
1993
+ },
1994
+ { type: "text", text: prompt }
1995
+ ], params);
1996
+ }
903
1997
  if (!modelId.startsWith("veo-")) {
904
- throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support image-to-video.`);
1998
+ throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for image-to-video.`);
1999
+ }
2000
+ const instance = {
2001
+ prompt,
2002
+ image: veoImage(image)
2003
+ };
2004
+ const parameters = this.buildVideoParameters(params);
2005
+ if (params.endImage?.length) {
2006
+ // Veo interpolates between the two frames only in an 8-second clip.
2007
+ instance.lastFrame = veoImage(params.endImage);
2008
+ parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
2009
+ }
2010
+ const body = { instances: [instance] };
2011
+ if (Object.keys(parameters).length > 0) {
2012
+ body.parameters = parameters;
2013
+ }
2014
+ return this.runVeoOperation(modelId, body, "image-to-video", params);
2015
+ }
2016
+ // ---------------------------------------------------------------------------
2017
+ // Reference-to-video
2018
+ // ---------------------------------------------------------------------------
2019
+ async referenceToVideo(inputs, params) {
2020
+ if (!params.prompt) {
2021
+ throw new Error("The input prompt cannot be empty.");
2022
+ }
2023
+ const modelId = params.model.id;
2024
+ requireVideoTask(modelId, "reference_to_video");
2025
+ const images = inputs.images.filter((bytes) => bytes.length > 0);
2026
+ const videos = inputs.videos.filter((bytes) => bytes.length > 0);
2027
+ if ((inputs.audios ?? []).some((bytes) => bytes.length > 0)) {
2028
+ throw new Error(`Gemini model ${modelId} does not accept reference audio`);
2029
+ }
2030
+ if (images.length === 0 && videos.length === 0) {
2031
+ throw new Error("At least one reference image or video is required");
2032
+ }
2033
+ if (isOmniVideoModel(modelId)) {
2034
+ if (videos.length > OMNI_MAX_REFERENCE_VIDEOS) {
2035
+ throw new Error(`Gemini model ${modelId} accepts at most ${OMNI_MAX_REFERENCE_VIDEOS} reference videos`);
2036
+ }
2037
+ const input = images.map((bytes) => ({
2038
+ type: "image",
2039
+ data: Buffer.from(bytes).toString("base64"),
2040
+ mime_type: detectImageMime(bytes)
2041
+ }));
2042
+ for (const video of videos) {
2043
+ input.push(await this.omniVideoInput(video));
2044
+ }
2045
+ input.push({ type: "text", text: params.prompt });
2046
+ return this.runOmniVideo(modelId, input, params);
2047
+ }
2048
+ if (videos.length > 0) {
2049
+ throw new Error(`Gemini model ${modelId} accepts reference images only`);
2050
+ }
2051
+ if (images.length > VEO_MAX_REFERENCE_IMAGES) {
2052
+ throw new Error(`Gemini model ${modelId} accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images`);
905
2053
  }
906
- const imageBase64 = Buffer.from(image).toString("base64");
907
- const prompt = params.prompt ?? "Animate this image";
2054
+ const parameters = this.buildVideoParameters(params);
2055
+ parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
908
2056
  const body = {
909
2057
  instances: [
910
2058
  {
911
- prompt,
912
- image: { bytesBase64Encoded: imageBase64, mimeType: "image/png" }
2059
+ prompt: params.prompt,
2060
+ referenceImages: images.map((bytes) => ({
2061
+ image: veoImage(bytes),
2062
+ referenceType: "asset"
2063
+ }))
913
2064
  }
914
- ]
2065
+ ],
2066
+ parameters
915
2067
  };
916
- const parameters = {};
917
- if (params.negativePrompt)
918
- parameters.negativePrompt = params.negativePrompt;
919
- if (params.aspectRatio)
920
- parameters.aspectRatio = params.aspectRatio;
921
- if (params.seed != null)
922
- parameters.seed = params.seed;
923
- if (Object.keys(parameters).length > 0)
924
- body.parameters = parameters;
925
- const url = `${GEMINI_API_BASE}/models/${modelId}:generateVideos?key=${this.apiKey}`;
926
- const response = await this._fetch(url, {
927
- method: "POST",
928
- headers: { "Content-Type": "application/json" },
929
- body: JSON.stringify(body)
2068
+ return this.runVeoOperation(modelId, body, "reference-to-video", params);
2069
+ }
2070
+ // ---------------------------------------------------------------------------
2071
+ // Video extension (Veo) and editing (Omni)
2072
+ // ---------------------------------------------------------------------------
2073
+ async extendVideo(video, params) {
2074
+ if (video.length === 0) {
2075
+ throw new Error("The input video is empty.");
2076
+ }
2077
+ const modelId = params.model.id;
2078
+ requireVideoTask(modelId, "extend_video");
2079
+ if (params.mode !== "end") {
2080
+ throw new Error(`Gemini model ${modelId} extends videos at the end only`);
2081
+ }
2082
+ if (params.durationSeconds !== VEO_EXTENSION_SECONDS) {
2083
+ throw new Error(`Gemini model ${modelId} extends a video by exactly ${VEO_EXTENSION_SECONDS} seconds`);
2084
+ }
2085
+ const body = {
2086
+ instances: [
2087
+ {
2088
+ prompt: params.prompt,
2089
+ video: {
2090
+ inlineData: {
2091
+ mimeType: sniffVideoMime(video),
2092
+ data: Buffer.from(video).toString("base64")
2093
+ }
2094
+ }
2095
+ }
2096
+ ],
2097
+ // Veo extends 720p input only and returns the input plus the new seconds.
2098
+ parameters: { numberOfVideos: 1, resolution: "720p" }
2099
+ };
2100
+ return this.runVeoOperation(modelId, body, "video extension", {
2101
+ signal: params.signal
930
2102
  });
931
- if (!response.ok) {
932
- const errText = await response.text();
933
- throw new Error(`Gemini image-to-video failed ${response.status}: ${errText}`);
2103
+ }
2104
+ async videoToVideo(video, params) {
2105
+ if (video.length === 0) {
2106
+ throw new Error("The input video is empty.");
934
2107
  }
935
- const operation = (await response.json());
936
- // Poll for completion
937
- const maxWait = 600_000;
938
- const pollInterval = 10_000;
939
- let elapsed = 0;
940
- let current = operation;
941
- while (!current.done && elapsed < maxWait) {
942
- await new Promise((r) => setTimeout(r, pollInterval));
943
- elapsed += pollInterval;
944
- if (!current.name)
945
- throw new Error("No operation name for polling");
946
- const pollUrl = `${GEMINI_API_BASE}/${current.name}?key=${this.apiKey}`;
947
- const pollResp = await this._fetch(pollUrl);
948
- if (!pollResp.ok) {
949
- const errText = await pollResp.text();
950
- throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
951
- }
952
- current = (await pollResp.json());
2108
+ if (!params.prompt) {
2109
+ throw new Error("The edit instruction cannot be empty.");
953
2110
  }
954
- if (!current.done)
955
- throw new Error("Image-to-video generation timed out");
956
- const videoUri = current.response?.generatedVideos?.[0]?.video?.uri;
957
- if (!videoUri)
958
- throw new Error("No video URI in response");
959
- const dlResp = await this._fetch(`${videoUri}&key=${this.apiKey}`);
960
- if (!dlResp.ok)
961
- throw new Error(`Video download failed: ${dlResp.status}`);
962
- return new Uint8Array(await dlResp.arrayBuffer());
2111
+ const modelId = params.model.id;
2112
+ requireVideoTask(modelId, "video_to_video");
2113
+ const input = [await this.omniVideoInput(video)];
2114
+ for (const bytes of params.referenceImages ?? []) {
2115
+ if (bytes.length === 0)
2116
+ continue;
2117
+ input.push({
2118
+ type: "image",
2119
+ data: Buffer.from(bytes).toString("base64"),
2120
+ mime_type: detectImageMime(bytes)
2121
+ });
2122
+ }
2123
+ input.push({ type: "text", text: params.prompt });
2124
+ return this.runOmniVideo(modelId, input, params);
2125
+ }
2126
+ // ---------------------------------------------------------------------------
2127
+ // Text-to-music (Lyria, Interactions API)
2128
+ // ---------------------------------------------------------------------------
2129
+ async textToMusic(params) {
2130
+ if (!params.prompt) {
2131
+ throw new Error("The input prompt cannot be empty.");
2132
+ }
2133
+ const modelId = params.model.id;
2134
+ if (!modelId.startsWith("lyria-")) {
2135
+ throw new Error(`Model ${modelId} is not a Lyria model. Use a Lyria model for text-to-music.`);
2136
+ }
2137
+ // Lyria takes length and lyrics from the prompt; it has no fields for them.
2138
+ const input = [
2139
+ params.prompt,
2140
+ params.durationSeconds
2141
+ ? `Length: about ${Math.round(params.durationSeconds)} seconds.`
2142
+ : undefined,
2143
+ params.lyrics ? `Lyrics:\n${params.lyrics}` : undefined
2144
+ ]
2145
+ .filter((part) => part !== undefined)
2146
+ .join("\n\n");
2147
+ const body = { model: modelId, input };
2148
+ // Lyria answers MP3 by default. Only Lyria 3.5 returns WAV, on request.
2149
+ if (params.audioFormat === "wav" && modelId === "lyria-3.5") {
2150
+ body.response_format = { type: "audio" };
2151
+ }
2152
+ const signal = combineSignals(undefined, params.timeoutSeconds);
2153
+ const interaction = await this.createInteraction(body, "music generation", modelId, signal);
2154
+ const audio = await this.interactionMedia(interaction, "audio", signal);
2155
+ return {
2156
+ data: audio.data,
2157
+ mimeType: audio.mimeType ?? sniffAudioMime(audio.data)
2158
+ };
963
2159
  }
964
2160
  // ---------------------------------------------------------------------------
965
2161
  // Error detection
966
2162
  // ---------------------------------------------------------------------------
2163
+ isContextExceededError(error) {
2164
+ return geminiContextExceeded(error) !== null;
2165
+ }
967
2166
  isContextLengthError(error) {
968
2167
  const msg = String(error).toLowerCase();
969
2168
  return (msg.includes("context length") ||