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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (665) hide show
  1. package/README.md +29 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/agent-memory.d.ts +118 -0
  4. package/dist/agent-memory.d.ts.map +1 -0
  5. package/dist/agent-memory.js +186 -0
  6. package/dist/agent-memory.js.map +1 -0
  7. package/dist/async-local-storage.d.ts +20 -0
  8. package/dist/async-local-storage.d.ts.map +1 -0
  9. package/dist/async-local-storage.js +28 -0
  10. package/dist/async-local-storage.js.map +1 -0
  11. package/dist/blender-executor.d.ts +93 -0
  12. package/dist/blender-executor.d.ts.map +1 -0
  13. package/dist/blender-executor.js +294 -0
  14. package/dist/blender-executor.js.map +1 -0
  15. package/dist/comfy-executor.d.ts +23 -1
  16. package/dist/comfy-executor.d.ts.map +1 -1
  17. package/dist/comfy-executor.js +234 -55
  18. package/dist/comfy-executor.js.map +1 -1
  19. package/dist/context-packer.d.ts +1 -1
  20. package/dist/context-packer.d.ts.map +1 -1
  21. package/dist/context-packer.js +23 -25
  22. package/dist/context-packer.js.map +1 -1
  23. package/dist/context.d.ts +954 -127
  24. package/dist/context.d.ts.map +1 -1
  25. package/dist/context.js +2424 -513
  26. package/dist/context.js.map +1 -1
  27. package/dist/cost-reconciler.d.ts +32 -0
  28. package/dist/cost-reconciler.d.ts.map +1 -0
  29. package/dist/cost-reconciler.js +21 -0
  30. package/dist/cost-reconciler.js.map +1 -0
  31. package/dist/entities.d.ts +33 -0
  32. package/dist/entities.d.ts.map +1 -0
  33. package/dist/entities.js +43 -0
  34. package/dist/entities.js.map +1 -0
  35. package/dist/external-media-fetch.d.ts +29 -0
  36. package/dist/external-media-fetch.d.ts.map +1 -0
  37. package/dist/external-media-fetch.js +38 -0
  38. package/dist/external-media-fetch.js.map +1 -0
  39. package/dist/generation-receipt.d.ts +86 -0
  40. package/dist/generation-receipt.d.ts.map +1 -0
  41. package/dist/generation-receipt.js +137 -0
  42. package/dist/generation-receipt.js.map +1 -0
  43. package/dist/generation-registry.d.ts +67 -0
  44. package/dist/generation-registry.d.ts.map +1 -0
  45. package/dist/generation-registry.js +126 -0
  46. package/dist/generation-registry.js.map +1 -0
  47. package/dist/google/client.d.ts +196 -0
  48. package/dist/google/client.d.ts.map +1 -0
  49. package/dist/google/client.js +396 -0
  50. package/dist/google/client.js.map +1 -0
  51. package/dist/google/index.d.ts +3 -0
  52. package/dist/google/index.d.ts.map +1 -0
  53. package/dist/google/index.js +3 -0
  54. package/dist/google/index.js.map +1 -0
  55. package/dist/google/token.d.ts +15 -0
  56. package/dist/google/token.d.ts.map +1 -0
  57. package/dist/google/token.js +24 -0
  58. package/dist/google/token.js.map +1 -0
  59. package/dist/host-binaries.d.ts +106 -0
  60. package/dist/host-binaries.d.ts.map +1 -0
  61. package/dist/host-binaries.js +344 -0
  62. package/dist/host-binaries.js.map +1 -0
  63. package/dist/image-codec.d.ts +74 -0
  64. package/dist/image-codec.d.ts.map +1 -0
  65. package/dist/image-codec.js +273 -0
  66. package/dist/image-codec.js.map +1 -0
  67. package/dist/index.d.ts +56 -6
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +43 -4
  70. package/dist/index.js.map +1 -1
  71. package/dist/invocation-account.d.ts +43 -0
  72. package/dist/invocation-account.d.ts.map +1 -0
  73. package/dist/invocation-account.js +60 -0
  74. package/dist/invocation-account.js.map +1 -0
  75. package/dist/media-ref-bytes.d.ts +19 -0
  76. package/dist/media-ref-bytes.d.ts.map +1 -0
  77. package/dist/media-ref-bytes.js +181 -0
  78. package/dist/media-ref-bytes.js.map +1 -0
  79. package/dist/node-executor.d.ts +113 -2
  80. package/dist/node-executor.d.ts.map +1 -1
  81. package/dist/permission-gate.d.ts +152 -0
  82. package/dist/permission-gate.d.ts.map +1 -0
  83. package/dist/permission-gate.js +87 -0
  84. package/dist/permission-gate.js.map +1 -0
  85. package/dist/prefixed-storage-adapter.d.ts +33 -0
  86. package/dist/prefixed-storage-adapter.d.ts.map +1 -0
  87. package/dist/prefixed-storage-adapter.js +70 -0
  88. package/dist/prefixed-storage-adapter.js.map +1 -0
  89. package/dist/prompt-asset-refs.d.ts +222 -0
  90. package/dist/prompt-asset-refs.d.ts.map +1 -0
  91. package/dist/prompt-asset-refs.js +751 -0
  92. package/dist/prompt-asset-refs.js.map +1 -0
  93. package/dist/provider-cache.d.ts +3 -0
  94. package/dist/provider-cache.d.ts.map +1 -0
  95. package/dist/provider-cache.js +8 -0
  96. package/dist/provider-cache.js.map +1 -0
  97. package/dist/providers/aki-provider.d.ts +33 -10
  98. package/dist/providers/aki-provider.d.ts.map +1 -1
  99. package/dist/providers/aki-provider.js +84 -77
  100. package/dist/providers/aki-provider.js.map +1 -1
  101. package/dist/providers/alibaba-provider.d.ts +39 -0
  102. package/dist/providers/alibaba-provider.d.ts.map +1 -0
  103. package/dist/providers/alibaba-provider.js +86 -0
  104. package/dist/providers/alibaba-provider.js.map +1 -0
  105. package/dist/providers/anthropic-provider.d.ts +52 -3
  106. package/dist/providers/anthropic-provider.d.ts.map +1 -1
  107. package/dist/providers/anthropic-provider.js +1020 -338
  108. package/dist/providers/anthropic-provider.js.map +1 -1
  109. package/dist/providers/atlascloud-provider.d.ts +101 -0
  110. package/dist/providers/atlascloud-provider.d.ts.map +1 -0
  111. package/dist/providers/atlascloud-provider.js +1009 -0
  112. package/dist/providers/atlascloud-provider.js.map +1 -0
  113. package/dist/providers/atlascloud-transport.d.ts +100 -0
  114. package/dist/providers/atlascloud-transport.d.ts.map +1 -0
  115. package/dist/providers/atlascloud-transport.js +364 -0
  116. package/dist/providers/atlascloud-transport.js.map +1 -0
  117. package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
  118. package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
  119. package/dist/providers/atlascloud-webhook-registry.js +104 -0
  120. package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
  121. package/dist/providers/audio-mime.d.ts +11 -0
  122. package/dist/providers/audio-mime.d.ts.map +1 -0
  123. package/dist/providers/audio-mime.js +46 -0
  124. package/dist/providers/audio-mime.js.map +1 -0
  125. package/dist/providers/base-provider.d.ts +481 -24
  126. package/dist/providers/base-provider.d.ts.map +1 -1
  127. package/dist/providers/base-provider.js +1259 -42
  128. package/dist/providers/base-provider.js.map +1 -1
  129. package/dist/providers/cassette-provider.d.ts +217 -0
  130. package/dist/providers/cassette-provider.d.ts.map +1 -0
  131. package/dist/providers/cassette-provider.js +502 -0
  132. package/dist/providers/cassette-provider.js.map +1 -0
  133. package/dist/providers/cerebras-provider.d.ts +6 -12
  134. package/dist/providers/cerebras-provider.d.ts.map +1 -1
  135. package/dist/providers/cerebras-provider.js +8 -33
  136. package/dist/providers/cerebras-provider.js.map +1 -1
  137. package/dist/providers/claude-agent-provider.d.ts +131 -68
  138. package/dist/providers/claude-agent-provider.d.ts.map +1 -1
  139. package/dist/providers/claude-agent-provider.js +1095 -515
  140. package/dist/providers/claude-agent-provider.js.map +1 -1
  141. package/dist/providers/codex-provider.d.ts +82 -0
  142. package/dist/providers/codex-provider.d.ts.map +1 -0
  143. package/dist/providers/codex-provider.js +492 -0
  144. package/dist/providers/codex-provider.js.map +1 -0
  145. package/dist/providers/cohere-provider.d.ts +5 -2
  146. package/dist/providers/cohere-provider.d.ts.map +1 -1
  147. package/dist/providers/cohere-provider.js +9 -2
  148. package/dist/providers/cohere-provider.js.map +1 -1
  149. package/dist/providers/comfy-cloud-provider.d.ts +30 -0
  150. package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
  151. package/dist/providers/comfy-cloud-provider.js +42 -0
  152. package/dist/providers/comfy-cloud-provider.js.map +1 -0
  153. package/dist/providers/content-filter.d.ts +46 -0
  154. package/dist/providers/content-filter.d.ts.map +1 -0
  155. package/dist/providers/content-filter.js +92 -0
  156. package/dist/providers/content-filter.js.map +1 -0
  157. package/dist/providers/context-exceeded.d.ts +37 -0
  158. package/dist/providers/context-exceeded.d.ts.map +1 -0
  159. package/dist/providers/context-exceeded.js +114 -0
  160. package/dist/providers/context-exceeded.js.map +1 -0
  161. package/dist/providers/context-window.d.ts +44 -0
  162. package/dist/providers/context-window.d.ts.map +1 -0
  163. package/dist/providers/context-window.js +145 -0
  164. package/dist/providers/context-window.js.map +1 -0
  165. package/dist/providers/contract/index.d.ts +4 -0
  166. package/dist/providers/contract/index.d.ts.map +1 -0
  167. package/dist/providers/contract/index.js +4 -0
  168. package/dist/providers/contract/index.js.map +1 -0
  169. package/dist/providers/contract/probe-manifest.d.ts +75 -0
  170. package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
  171. package/dist/providers/contract/probe-manifest.js +379 -0
  172. package/dist/providers/contract/probe-manifest.js.map +1 -0
  173. package/dist/providers/contract/probe-runner.d.ts +57 -0
  174. package/dist/providers/contract/probe-runner.d.ts.map +1 -0
  175. package/dist/providers/contract/probe-runner.js +180 -0
  176. package/dist/providers/contract/probe-runner.js.map +1 -0
  177. package/dist/providers/contract/redact.d.ts +29 -0
  178. package/dist/providers/contract/redact.d.ts.map +1 -0
  179. package/dist/providers/contract/redact.js +113 -0
  180. package/dist/providers/contract/redact.js.map +1 -0
  181. package/dist/providers/core-tools.d.ts +74 -0
  182. package/dist/providers/core-tools.d.ts.map +1 -0
  183. package/dist/providers/core-tools.js +127 -0
  184. package/dist/providers/core-tools.js.map +1 -0
  185. package/dist/providers/cost-calculator.d.ts +69 -50
  186. package/dist/providers/cost-calculator.d.ts.map +1 -1
  187. package/dist/providers/cost-calculator.js +207 -397
  188. package/dist/providers/cost-calculator.js.map +1 -1
  189. package/dist/providers/credential-check.d.ts +45 -0
  190. package/dist/providers/credential-check.d.ts.map +1 -0
  191. package/dist/providers/credential-check.js +159 -0
  192. package/dist/providers/credential-check.js.map +1 -0
  193. package/dist/providers/custom-model-kinds.d.ts +33 -0
  194. package/dist/providers/custom-model-kinds.d.ts.map +1 -0
  195. package/dist/providers/custom-model-kinds.js +96 -0
  196. package/dist/providers/custom-model-kinds.js.map +1 -0
  197. package/dist/providers/custom-openai-provider.d.ts +81 -0
  198. package/dist/providers/custom-openai-provider.d.ts.map +1 -0
  199. package/dist/providers/custom-openai-provider.js +182 -0
  200. package/dist/providers/custom-openai-provider.js.map +1 -0
  201. package/dist/providers/custom-provider-registry.d.ts +19 -0
  202. package/dist/providers/custom-provider-registry.d.ts.map +1 -0
  203. package/dist/providers/custom-provider-registry.js +52 -0
  204. package/dist/providers/custom-provider-registry.js.map +1 -0
  205. package/dist/providers/deepseek-provider.d.ts +8 -14
  206. package/dist/providers/deepseek-provider.d.ts.map +1 -1
  207. package/dist/providers/deepseek-provider.js +6 -35
  208. package/dist/providers/deepseek-provider.js.map +1 -1
  209. package/dist/providers/defaults.d.ts +12 -0
  210. package/dist/providers/defaults.d.ts.map +1 -0
  211. package/dist/providers/defaults.js +12 -0
  212. package/dist/providers/defaults.js.map +1 -0
  213. package/dist/providers/elevenlabs-provider.d.ts +49 -0
  214. package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
  215. package/dist/providers/elevenlabs-provider.js +142 -0
  216. package/dist/providers/elevenlabs-provider.js.map +1 -0
  217. package/dist/providers/entity-references.d.ts +31 -0
  218. package/dist/providers/entity-references.d.ts.map +1 -0
  219. package/dist/providers/entity-references.js +113 -0
  220. package/dist/providers/entity-references.js.map +1 -0
  221. package/dist/providers/evolink-provider.d.ts +38 -0
  222. package/dist/providers/evolink-provider.d.ts.map +1 -0
  223. package/dist/providers/evolink-provider.js +304 -0
  224. package/dist/providers/evolink-provider.js.map +1 -0
  225. package/dist/providers/fake-provider.d.ts +72 -1
  226. package/dist/providers/fake-provider.d.ts.map +1 -1
  227. package/dist/providers/fake-provider.js +203 -1
  228. package/dist/providers/fake-provider.js.map +1 -1
  229. package/dist/providers/fal-generations.d.ts +106 -0
  230. package/dist/providers/fal-generations.d.ts.map +1 -0
  231. package/dist/providers/fal-generations.js +286 -0
  232. package/dist/providers/fal-generations.js.map +1 -0
  233. package/dist/providers/fal-provider.d.ts +171 -7
  234. package/dist/providers/fal-provider.d.ts.map +1 -1
  235. package/dist/providers/fal-provider.js +1369 -64
  236. package/dist/providers/fal-provider.js.map +1 -1
  237. package/dist/providers/fal-queue.d.ts +47 -0
  238. package/dist/providers/fal-queue.d.ts.map +1 -0
  239. package/dist/providers/fal-queue.js +231 -0
  240. package/dist/providers/fal-queue.js.map +1 -0
  241. package/dist/providers/gemini-provider.d.ts +148 -8
  242. package/dist/providers/gemini-provider.d.ts.map +1 -1
  243. package/dist/providers/gemini-provider.js +1500 -316
  244. package/dist/providers/gemini-provider.js.map +1 -1
  245. package/dist/providers/gmi-provider.d.ts +21 -0
  246. package/dist/providers/gmi-provider.d.ts.map +1 -0
  247. package/dist/providers/gmi-provider.js +34 -0
  248. package/dist/providers/gmi-provider.js.map +1 -0
  249. package/dist/providers/gpt-image-size.d.ts +3 -0
  250. package/dist/providers/gpt-image-size.d.ts.map +1 -0
  251. package/dist/providers/gpt-image-size.js +37 -0
  252. package/dist/providers/gpt-image-size.js.map +1 -0
  253. package/dist/providers/groq-provider.d.ts +9 -12
  254. package/dist/providers/groq-provider.d.ts.map +1 -1
  255. package/dist/providers/groq-provider.js +19 -33
  256. package/dist/providers/groq-provider.js.map +1 -1
  257. package/dist/providers/groq-request.d.ts +38 -0
  258. package/dist/providers/groq-request.d.ts.map +1 -0
  259. package/dist/providers/groq-request.js +138 -0
  260. package/dist/providers/groq-request.js.map +1 -0
  261. package/dist/providers/higgsfield-provider.d.ts +30 -0
  262. package/dist/providers/higgsfield-provider.d.ts.map +1 -0
  263. package/dist/providers/higgsfield-provider.js +131 -0
  264. package/dist/providers/higgsfield-provider.js.map +1 -0
  265. package/dist/providers/higgsfield-transport.d.ts +106 -0
  266. package/dist/providers/higgsfield-transport.d.ts.map +1 -0
  267. package/dist/providers/higgsfield-transport.js +182 -0
  268. package/dist/providers/higgsfield-transport.js.map +1 -0
  269. package/dist/providers/http-transport.d.ts +74 -0
  270. package/dist/providers/http-transport.d.ts.map +1 -0
  271. package/dist/providers/http-transport.js +152 -0
  272. package/dist/providers/http-transport.js.map +1 -0
  273. package/dist/providers/huggingface-provider.d.ts +71 -3
  274. package/dist/providers/huggingface-provider.d.ts.map +1 -1
  275. package/dist/providers/huggingface-provider.js +230 -34
  276. package/dist/providers/huggingface-provider.js.map +1 -1
  277. package/dist/providers/image-mime.d.ts +39 -0
  278. package/dist/providers/image-mime.d.ts.map +1 -0
  279. package/dist/providers/image-mime.js +111 -0
  280. package/dist/providers/image-mime.js.map +1 -0
  281. package/dist/providers/image-ref.d.ts +21 -0
  282. package/dist/providers/image-ref.d.ts.map +1 -0
  283. package/dist/providers/image-ref.js +44 -0
  284. package/dist/providers/image-ref.js.map +1 -0
  285. package/dist/providers/index.d.ts +84 -8
  286. package/dist/providers/index.d.ts.map +1 -1
  287. package/dist/providers/index.js +239 -56
  288. package/dist/providers/index.js.map +1 -1
  289. package/dist/providers/jina-provider.d.ts +10 -2
  290. package/dist/providers/jina-provider.d.ts.map +1 -1
  291. package/dist/providers/jina-provider.js +16 -7
  292. package/dist/providers/jina-provider.js.map +1 -1
  293. package/dist/providers/kie-provider.d.ts +170 -9
  294. package/dist/providers/kie-provider.d.ts.map +1 -1
  295. package/dist/providers/kie-provider.js +1176 -228
  296. package/dist/providers/kie-provider.js.map +1 -1
  297. package/dist/providers/kie-webhook-registry.d.ts +28 -0
  298. package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
  299. package/dist/providers/kie-webhook-registry.js +91 -0
  300. package/dist/providers/kie-webhook-registry.js.map +1 -0
  301. package/dist/providers/llama-provider.d.ts +21 -37
  302. package/dist/providers/llama-provider.d.ts.map +1 -1
  303. package/dist/providers/llama-provider.js +43 -354
  304. package/dist/providers/llama-provider.js.map +1 -1
  305. package/dist/providers/lmstudio-provider.d.ts +4 -7
  306. package/dist/providers/lmstudio-provider.d.ts.map +1 -1
  307. package/dist/providers/lmstudio-provider.js +24 -15
  308. package/dist/providers/lmstudio-provider.js.map +1 -1
  309. package/dist/providers/manifest-models.d.ts +341 -1
  310. package/dist/providers/manifest-models.d.ts.map +1 -1
  311. package/dist/providers/manifest-models.js +1106 -22
  312. package/dist/providers/manifest-models.js.map +1 -1
  313. package/dist/providers/media-mime.d.ts +14 -0
  314. package/dist/providers/media-mime.d.ts.map +1 -0
  315. package/dist/providers/media-mime.js +35 -0
  316. package/dist/providers/media-mime.js.map +1 -0
  317. package/dist/providers/meshy-provider.d.ts +14 -1
  318. package/dist/providers/meshy-provider.d.ts.map +1 -1
  319. package/dist/providers/meshy-provider.js +76 -46
  320. package/dist/providers/meshy-provider.js.map +1 -1
  321. package/dist/providers/meta-provider.d.ts +22 -0
  322. package/dist/providers/meta-provider.d.ts.map +1 -0
  323. package/dist/providers/meta-provider.js +54 -0
  324. package/dist/providers/meta-provider.js.map +1 -0
  325. package/dist/providers/minimax-provider.d.ts +25 -18
  326. package/dist/providers/minimax-provider.d.ts.map +1 -1
  327. package/dist/providers/minimax-provider.js +211 -72
  328. package/dist/providers/minimax-provider.js.map +1 -1
  329. package/dist/providers/mistral-provider.d.ts +6 -12
  330. package/dist/providers/mistral-provider.d.ts.map +1 -1
  331. package/dist/providers/mistral-provider.js +10 -34
  332. package/dist/providers/mistral-provider.js.map +1 -1
  333. package/dist/providers/moonshot-provider.d.ts +11 -15
  334. package/dist/providers/moonshot-provider.d.ts.map +1 -1
  335. package/dist/providers/moonshot-provider.js +33 -23
  336. package/dist/providers/moonshot-provider.js.map +1 -1
  337. package/dist/providers/node-llama-cpp-provider.d.ts +98 -0
  338. package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
  339. package/dist/providers/node-llama-cpp-provider.js +479 -0
  340. package/dist/providers/node-llama-cpp-provider.js.map +1 -0
  341. package/dist/providers/nodetool-provider.d.ts +49 -0
  342. package/dist/providers/nodetool-provider.d.ts.map +1 -0
  343. package/dist/providers/nodetool-provider.js +224 -0
  344. package/dist/providers/nodetool-provider.js.map +1 -0
  345. package/dist/providers/oauth/browser-launcher.d.ts +23 -0
  346. package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
  347. package/dist/providers/oauth/browser-launcher.js +45 -0
  348. package/dist/providers/oauth/browser-launcher.js.map +1 -0
  349. package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
  350. package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
  351. package/dist/providers/oauth/claude-code-credentials.js +133 -0
  352. package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
  353. package/dist/providers/oauth/claude-code-login.d.ts +124 -0
  354. package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
  355. package/dist/providers/oauth/claude-code-login.js +230 -0
  356. package/dist/providers/oauth/claude-code-login.js.map +1 -0
  357. package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
  358. package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
  359. package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
  360. package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
  361. package/dist/providers/oauth/errors.d.ts +54 -0
  362. package/dist/providers/oauth/errors.d.ts.map +1 -0
  363. package/dist/providers/oauth/errors.js +79 -0
  364. package/dist/providers/oauth/errors.js.map +1 -0
  365. package/dist/providers/oauth/index.d.ts +105 -0
  366. package/dist/providers/oauth/index.d.ts.map +1 -0
  367. package/dist/providers/oauth/index.js +167 -0
  368. package/dist/providers/oauth/index.js.map +1 -0
  369. package/dist/providers/oauth/jwt-claims.d.ts +23 -0
  370. package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
  371. package/dist/providers/oauth/jwt-claims.js +67 -0
  372. package/dist/providers/oauth/jwt-claims.js.map +1 -0
  373. package/dist/providers/oauth/local-callback-server.d.ts +60 -0
  374. package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
  375. package/dist/providers/oauth/local-callback-server.js +148 -0
  376. package/dist/providers/oauth/local-callback-server.js.map +1 -0
  377. package/dist/providers/oauth/oauth-client.d.ts +55 -0
  378. package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
  379. package/dist/providers/oauth/oauth-client.js +161 -0
  380. package/dist/providers/oauth/oauth-client.js.map +1 -0
  381. package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
  382. package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
  383. package/dist/providers/oauth/openai-oauth-provider.js +265 -0
  384. package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
  385. package/dist/providers/oauth/pkce-helper.d.ts +43 -0
  386. package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
  387. package/dist/providers/oauth/pkce-helper.js +57 -0
  388. package/dist/providers/oauth/pkce-helper.js.map +1 -0
  389. package/dist/providers/oauth/redaction.d.ts +22 -0
  390. package/dist/providers/oauth/redaction.d.ts.map +1 -0
  391. package/dist/providers/oauth/redaction.js +59 -0
  392. package/dist/providers/oauth/redaction.js.map +1 -0
  393. package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
  394. package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
  395. package/dist/providers/oauth/secure-credential-store.js +73 -0
  396. package/dist/providers/oauth/secure-credential-store.js.map +1 -0
  397. package/dist/providers/oauth/token-store.d.ts +53 -0
  398. package/dist/providers/oauth/token-store.d.ts.map +1 -0
  399. package/dist/providers/oauth/token-store.js +78 -0
  400. package/dist/providers/oauth/token-store.js.map +1 -0
  401. package/dist/providers/oauth/types.d.ts +64 -0
  402. package/dist/providers/oauth/types.d.ts.map +1 -0
  403. package/dist/providers/oauth/types.js +12 -0
  404. package/dist/providers/oauth/types.js.map +1 -0
  405. package/dist/providers/object-tracking.d.ts +47 -0
  406. package/dist/providers/object-tracking.d.ts.map +1 -0
  407. package/dist/providers/object-tracking.js +46 -0
  408. package/dist/providers/object-tracking.js.map +1 -0
  409. package/dist/providers/offline-model-index.d.ts +20 -0
  410. package/dist/providers/offline-model-index.d.ts.map +1 -0
  411. package/dist/providers/offline-model-index.js +112 -0
  412. package/dist/providers/offline-model-index.js.map +1 -0
  413. package/dist/providers/ollama-provider.d.ts +28 -1
  414. package/dist/providers/ollama-provider.d.ts.map +1 -1
  415. package/dist/providers/ollama-provider.js +193 -43
  416. package/dist/providers/ollama-provider.js.map +1 -1
  417. package/dist/providers/openai-compat/client.d.ts +57 -0
  418. package/dist/providers/openai-compat/client.d.ts.map +1 -0
  419. package/dist/providers/openai-compat/client.js +157 -0
  420. package/dist/providers/openai-compat/client.js.map +1 -0
  421. package/dist/providers/openai-compat/decode.d.ts +36 -0
  422. package/dist/providers/openai-compat/decode.d.ts.map +1 -0
  423. package/dist/providers/openai-compat/decode.js +55 -0
  424. package/dist/providers/openai-compat/decode.js.map +1 -0
  425. package/dist/providers/openai-compat/errors.d.ts +54 -0
  426. package/dist/providers/openai-compat/errors.d.ts.map +1 -0
  427. package/dist/providers/openai-compat/errors.js +144 -0
  428. package/dist/providers/openai-compat/errors.js.map +1 -0
  429. package/dist/providers/openai-compat/index.d.ts +8 -0
  430. package/dist/providers/openai-compat/index.d.ts.map +1 -0
  431. package/dist/providers/openai-compat/index.js +5 -0
  432. package/dist/providers/openai-compat/index.js.map +1 -0
  433. package/dist/providers/openai-compat/sse.d.ts +14 -0
  434. package/dist/providers/openai-compat/sse.d.ts.map +1 -0
  435. package/dist/providers/openai-compat/sse.js +69 -0
  436. package/dist/providers/openai-compat/sse.js.map +1 -0
  437. package/dist/providers/openai-compat/types.d.ts +75 -0
  438. package/dist/providers/openai-compat/types.d.ts.map +1 -0
  439. package/dist/providers/openai-compat/types.js +9 -0
  440. package/dist/providers/openai-compat/types.js.map +1 -0
  441. package/dist/providers/openai-compat-provider.d.ts +90 -0
  442. package/dist/providers/openai-compat-provider.d.ts.map +1 -0
  443. package/dist/providers/openai-compat-provider.js +282 -0
  444. package/dist/providers/openai-compat-provider.js.map +1 -0
  445. package/dist/providers/openai-provider.d.ts +89 -26
  446. package/dist/providers/openai-provider.d.ts.map +1 -1
  447. package/dist/providers/openai-provider.js +990 -105
  448. package/dist/providers/openai-provider.js.map +1 -1
  449. package/dist/providers/openrouter-provider.d.ts +6 -12
  450. package/dist/providers/openrouter-provider.d.ts.map +1 -1
  451. package/dist/providers/openrouter-provider.js +24 -53
  452. package/dist/providers/openrouter-provider.js.map +1 -1
  453. package/dist/providers/provider-call-failure.d.ts +39 -0
  454. package/dist/providers/provider-call-failure.d.ts.map +1 -0
  455. package/dist/providers/provider-call-failure.js +132 -0
  456. package/dist/providers/provider-call-failure.js.map +1 -0
  457. package/dist/providers/provider-error.d.ts +59 -0
  458. package/dist/providers/provider-error.d.ts.map +1 -0
  459. package/dist/providers/provider-error.js +165 -0
  460. package/dist/providers/provider-error.js.map +1 -0
  461. package/dist/providers/provider-generations.d.ts +99 -0
  462. package/dist/providers/provider-generations.d.ts.map +1 -0
  463. package/dist/providers/provider-generations.js +71 -0
  464. package/dist/providers/provider-generations.js.map +1 -0
  465. package/dist/providers/provider-queue.d.ts +62 -0
  466. package/dist/providers/provider-queue.d.ts.map +1 -0
  467. package/dist/providers/provider-queue.js +28 -0
  468. package/dist/providers/provider-queue.js.map +1 -0
  469. package/dist/providers/provider-registry.d.ts +33 -14
  470. package/dist/providers/provider-registry.d.ts.map +1 -1
  471. package/dist/providers/provider-registry.js +85 -68
  472. package/dist/providers/provider-registry.js.map +1 -1
  473. package/dist/providers/provider-request-log.d.ts +53 -0
  474. package/dist/providers/provider-request-log.d.ts.map +1 -0
  475. package/dist/providers/provider-request-log.js +157 -0
  476. package/dist/providers/provider-request-log.js.map +1 -0
  477. package/dist/providers/provider-session.d.ts +3 -0
  478. package/dist/providers/provider-session.d.ts.map +1 -0
  479. package/dist/providers/provider-session.js +10 -0
  480. package/dist/providers/provider-session.js.map +1 -0
  481. package/dist/providers/provider-transport.d.ts +11 -0
  482. package/dist/providers/provider-transport.d.ts.map +1 -0
  483. package/dist/providers/provider-transport.js +11 -0
  484. package/dist/providers/provider-transport.js.map +1 -0
  485. package/dist/providers/python-provider.d.ts +20 -12
  486. package/dist/providers/python-provider.d.ts.map +1 -1
  487. package/dist/providers/python-provider.js +185 -25
  488. package/dist/providers/python-provider.js.map +1 -1
  489. package/dist/providers/replicate-provider.d.ts +89 -7
  490. package/dist/providers/replicate-provider.d.ts.map +1 -1
  491. package/dist/providers/replicate-provider.js +801 -65
  492. package/dist/providers/replicate-provider.js.map +1 -1
  493. package/dist/providers/replicate-retry.d.ts +30 -0
  494. package/dist/providers/replicate-retry.d.ts.map +1 -0
  495. package/dist/providers/replicate-retry.js +97 -0
  496. package/dist/providers/replicate-retry.js.map +1 -0
  497. package/dist/providers/responses-api.d.ts +27 -0
  498. package/dist/providers/responses-api.d.ts.map +1 -0
  499. package/dist/providers/responses-api.js +297 -0
  500. package/dist/providers/responses-api.js.map +1 -0
  501. package/dist/providers/reve-provider.d.ts +45 -0
  502. package/dist/providers/reve-provider.d.ts.map +1 -0
  503. package/dist/providers/reve-provider.js +159 -0
  504. package/dist/providers/reve-provider.js.map +1 -0
  505. package/dist/providers/rodin-provider.d.ts +23 -1
  506. package/dist/providers/rodin-provider.d.ts.map +1 -1
  507. package/dist/providers/rodin-provider.js +89 -51
  508. package/dist/providers/rodin-provider.js.map +1 -1
  509. package/dist/providers/safe-url.d.ts +70 -0
  510. package/dist/providers/safe-url.d.ts.map +1 -0
  511. package/dist/providers/safe-url.js +277 -0
  512. package/dist/providers/safe-url.js.map +1 -0
  513. package/dist/providers/scripted-provider.d.ts +9 -0
  514. package/dist/providers/scripted-provider.d.ts.map +1 -1
  515. package/dist/providers/scripted-provider.js +86 -10
  516. package/dist/providers/scripted-provider.js.map +1 -1
  517. package/dist/providers/structured-output.d.ts +42 -0
  518. package/dist/providers/structured-output.d.ts.map +1 -0
  519. package/dist/providers/structured-output.js +83 -0
  520. package/dist/providers/structured-output.js.map +1 -0
  521. package/dist/providers/together-provider.d.ts +19 -16
  522. package/dist/providers/together-provider.d.ts.map +1 -1
  523. package/dist/providers/together-provider.js +103 -225
  524. package/dist/providers/together-provider.js.map +1 -1
  525. package/dist/providers/topaz-provider.d.ts +44 -0
  526. package/dist/providers/topaz-provider.d.ts.map +1 -0
  527. package/dist/providers/topaz-provider.js +239 -0
  528. package/dist/providers/topaz-provider.js.map +1 -0
  529. package/dist/providers/types.d.ts +603 -4
  530. package/dist/providers/types.d.ts.map +1 -1
  531. package/dist/providers/types.js +81 -1
  532. package/dist/providers/types.js.map +1 -1
  533. package/dist/providers/useapi-provider.d.ts +27 -0
  534. package/dist/providers/useapi-provider.d.ts.map +1 -0
  535. package/dist/providers/useapi-provider.js +266 -0
  536. package/dist/providers/useapi-provider.js.map +1 -0
  537. package/dist/providers/video-duration.d.ts +6 -0
  538. package/dist/providers/video-duration.d.ts.map +1 -0
  539. package/dist/providers/video-duration.js +63 -0
  540. package/dist/providers/video-duration.js.map +1 -0
  541. package/dist/providers/video-frame-fallback.d.ts +39 -0
  542. package/dist/providers/video-frame-fallback.d.ts.map +1 -0
  543. package/dist/providers/video-frame-fallback.js +155 -0
  544. package/dist/providers/video-frame-fallback.js.map +1 -0
  545. package/dist/providers/video-frames.d.ts +61 -0
  546. package/dist/providers/video-frames.d.ts.map +1 -0
  547. package/dist/providers/video-frames.js +208 -0
  548. package/dist/providers/video-frames.js.map +1 -0
  549. package/dist/providers/video-mime.d.ts +14 -0
  550. package/dist/providers/video-mime.d.ts.map +1 -0
  551. package/dist/providers/video-mime.js +44 -0
  552. package/dist/providers/video-mime.js.map +1 -0
  553. package/dist/providers/video-to-audio.d.ts +34 -0
  554. package/dist/providers/video-to-audio.d.ts.map +1 -0
  555. package/dist/providers/video-to-audio.js +62 -0
  556. package/dist/providers/video-to-audio.js.map +1 -0
  557. package/dist/providers/vllm-provider.d.ts +22 -8
  558. package/dist/providers/vllm-provider.d.ts.map +1 -1
  559. package/dist/providers/vllm-provider.js +48 -23
  560. package/dist/providers/vllm-provider.js.map +1 -1
  561. package/dist/providers/voyage-provider.d.ts +10 -2
  562. package/dist/providers/voyage-provider.d.ts.map +1 -1
  563. package/dist/providers/voyage-provider.js +16 -7
  564. package/dist/providers/voyage-provider.js.map +1 -1
  565. package/dist/providers/xai-provider.d.ts +54 -13
  566. package/dist/providers/xai-provider.d.ts.map +1 -1
  567. package/dist/providers/xai-provider.js +299 -22
  568. package/dist/providers/xai-provider.js.map +1 -1
  569. package/dist/python-bridge-base.d.ts +358 -0
  570. package/dist/python-bridge-base.d.ts.map +1 -0
  571. package/dist/python-bridge-base.js +1557 -0
  572. package/dist/python-bridge-base.js.map +1 -0
  573. package/dist/python-bridge-codec.d.ts +28 -0
  574. package/dist/python-bridge-codec.d.ts.map +1 -0
  575. package/dist/python-bridge-codec.js +39 -0
  576. package/dist/python-bridge-codec.js.map +1 -0
  577. package/dist/python-bridge-factory.d.ts +52 -0
  578. package/dist/python-bridge-factory.d.ts.map +1 -0
  579. package/dist/python-bridge-factory.js +69 -0
  580. package/dist/python-bridge-factory.js.map +1 -0
  581. package/dist/python-bridge-framing.d.ts +68 -0
  582. package/dist/python-bridge-framing.d.ts.map +1 -0
  583. package/dist/python-bridge-framing.js +91 -0
  584. package/dist/python-bridge-framing.js.map +1 -0
  585. package/dist/python-bridge-types.d.ts +545 -2
  586. package/dist/python-bridge-types.d.ts.map +1 -1
  587. package/dist/python-bridge-types.js +49 -1
  588. package/dist/python-bridge-types.js.map +1 -1
  589. package/dist/python-graph-resolver.d.ts +57 -0
  590. package/dist/python-graph-resolver.d.ts.map +1 -0
  591. package/dist/python-graph-resolver.js +79 -0
  592. package/dist/python-graph-resolver.js.map +1 -0
  593. package/dist/python-node-executor.d.ts +36 -4
  594. package/dist/python-node-executor.d.ts.map +1 -1
  595. package/dist/python-node-executor.js +177 -86
  596. package/dist/python-node-executor.js.map +1 -1
  597. package/dist/python-stdio-bridge.d.ts +18 -37
  598. package/dist/python-stdio-bridge.d.ts.map +1 -1
  599. package/dist/python-stdio-bridge.js +148 -475
  600. package/dist/python-stdio-bridge.js.map +1 -1
  601. package/dist/python-websocket-bridge.d.ts +140 -0
  602. package/dist/python-websocket-bridge.d.ts.map +1 -0
  603. package/dist/python-websocket-bridge.js +561 -0
  604. package/dist/python-websocket-bridge.js.map +1 -0
  605. package/dist/python-worker-stderr.d.ts +4 -0
  606. package/dist/python-worker-stderr.d.ts.map +1 -0
  607. package/dist/python-worker-stderr.js +24 -0
  608. package/dist/python-worker-stderr.js.map +1 -0
  609. package/dist/recommended-models.d.ts +2 -2
  610. package/dist/recommended-models.d.ts.map +1 -1
  611. package/dist/recommended-models.js +24 -4
  612. package/dist/recommended-models.js.map +1 -1
  613. package/dist/recoverable-node-error.d.ts +31 -0
  614. package/dist/recoverable-node-error.d.ts.map +1 -0
  615. package/dist/recoverable-node-error.js +37 -0
  616. package/dist/recoverable-node-error.js.map +1 -0
  617. package/dist/redact-params.d.ts +13 -0
  618. package/dist/redact-params.d.ts.map +1 -0
  619. package/dist/redact-params.js +57 -0
  620. package/dist/redact-params.js.map +1 -0
  621. package/dist/sandbox-module-catalog.d.ts +43 -0
  622. package/dist/sandbox-module-catalog.d.ts.map +1 -0
  623. package/dist/sandbox-module-catalog.js +25 -0
  624. package/dist/sandbox-module-catalog.js.map +1 -0
  625. package/dist/storage-workspace.d.ts +59 -0
  626. package/dist/storage-workspace.d.ts.map +1 -0
  627. package/dist/storage-workspace.js +426 -0
  628. package/dist/storage-workspace.js.map +1 -0
  629. package/dist/swappable-python-bridge.d.ts +83 -0
  630. package/dist/swappable-python-bridge.d.ts.map +1 -0
  631. package/dist/swappable-python-bridge.js +246 -0
  632. package/dist/swappable-python-bridge.js.map +1 -0
  633. package/dist/telemetry.d.ts +7 -0
  634. package/dist/telemetry.d.ts.map +1 -1
  635. package/dist/telemetry.js +23 -0
  636. package/dist/telemetry.js.map +1 -1
  637. package/dist/testing.d.ts +81 -0
  638. package/dist/testing.d.ts.map +1 -0
  639. package/dist/testing.js +130 -0
  640. package/dist/testing.js.map +1 -0
  641. package/dist/token-counter.d.ts +30 -0
  642. package/dist/token-counter.d.ts.map +1 -0
  643. package/dist/token-counter.js +156 -0
  644. package/dist/token-counter.js.map +1 -0
  645. package/dist/trace-exporters.d.ts +10 -0
  646. package/dist/trace-exporters.d.ts.map +1 -1
  647. package/dist/trace-exporters.js +59 -20
  648. package/dist/trace-exporters.js.map +1 -1
  649. package/dist/tracing-helpers.d.ts +45 -4
  650. package/dist/tracing-helpers.d.ts.map +1 -1
  651. package/dist/tracing-helpers.js +62 -19
  652. package/dist/tracing-helpers.js.map +1 -1
  653. package/dist/turn-budget.d.ts +278 -0
  654. package/dist/turn-budget.d.ts.map +1 -0
  655. package/dist/turn-budget.js +365 -0
  656. package/dist/turn-budget.js.map +1 -0
  657. package/dist/variable-channel.d.ts +48 -0
  658. package/dist/variable-channel.d.ts.map +1 -0
  659. package/dist/variable-channel.js +110 -0
  660. package/dist/variable-channel.js.map +1 -0
  661. package/dist/workspace.d.ts +119 -0
  662. package/dist/workspace.d.ts.map +1 -0
  663. package/dist/workspace.js +112 -0
  664. package/dist/workspace.js.map +1 -0
  665. package/package.json +107 -19
@@ -1,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,174 @@ 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 {
51
555
  static requiredSecrets() {
52
556
  return ["GEMINI_API_KEY"];
53
557
  }
54
558
  apiKey;
55
559
  _fetch;
560
+ _sleep;
56
561
  constructor(secrets, options = {}) {
57
562
  super("gemini");
58
563
  const apiKey = secrets.GEMINI_API_KEY;
@@ -61,6 +566,9 @@ export class GeminiProvider extends BaseProvider {
61
566
  }
62
567
  this.apiKey = apiKey;
63
568
  this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
569
+ this._sleep =
570
+ options.sleepFn ??
571
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
64
572
  }
65
573
  getContainerEnv() {
66
574
  return { GEMINI_API_KEY: this.apiKey };
@@ -68,33 +576,55 @@ export class GeminiProvider extends BaseProvider {
68
576
  async hasToolSupport(_model) {
69
577
  return true;
70
578
  }
579
+ get supportsNativeWebSearch() {
580
+ return true;
581
+ }
582
+ /** Gemini reads a whole clip: see {@link videoContentToGeminiPart}. */
583
+ get supportsVideoInput() {
584
+ return true;
585
+ }
71
586
  // ---------------------------------------------------------------------------
72
587
  // Model listing
73
588
  // ---------------------------------------------------------------------------
74
589
  async getAvailableLanguageModels() {
75
- const url = `${GEMINI_API_BASE}/models?key=${this.apiKey}`;
76
- let response;
590
+ const items = [];
591
+ let pageToken;
77
592
  try {
78
- response = await this._fetch(url);
593
+ do {
594
+ const query = new URLSearchParams({
595
+ key: this.apiKey,
596
+ pageSize: "1000"
597
+ });
598
+ if (pageToken)
599
+ query.set("pageToken", pageToken);
600
+ const response = await this._fetch(`${GEMINI_API_BASE}/models?${query}`);
601
+ if (!response.ok)
602
+ return [];
603
+ const payload = decodeGeminiModelsPage(await response.json());
604
+ items.push(...(payload.models ?? []));
605
+ pageToken = payload.nextPageToken;
606
+ } while (pageToken);
79
607
  }
80
608
  catch {
81
609
  return [];
82
610
  }
83
- if (!response.ok)
84
- return [];
85
- const payload = (await response.json());
86
- const items = payload.models ?? [];
611
+ const seen = new Set();
87
612
  return items
88
613
  .filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent"))
89
614
  .filter((m) => !!m.name)
615
+ .filter((m) => !/(embedding|aqa|imagen|veo|image|tts)/i.test(m.name ?? ""))
90
616
  .map((m) => {
91
617
  const id = m.name.split("/").pop();
618
+ if (seen.has(id))
619
+ return null;
620
+ seen.add(id);
92
621
  return {
93
622
  id,
94
623
  name: m.displayName ?? id,
95
624
  provider: "gemini"
96
625
  };
97
- });
626
+ })
627
+ .filter((model) => model !== null);
98
628
  }
99
629
  // ---------------------------------------------------------------------------
100
630
  // Message conversion helpers
@@ -107,26 +637,43 @@ export class GeminiProvider extends BaseProvider {
107
637
  const img = content.image;
108
638
  let base64Data;
109
639
  let mimeType = img.mimeType ?? "image/jpeg";
110
- if (img.data) {
111
- if (typeof img.data === "string") {
112
- base64Data = img.data;
640
+ const parseImageDataUri = (uri) => {
641
+ const idx = uri.indexOf(",");
642
+ if (idx < 0)
643
+ throw new Error("Invalid image data URI");
644
+ const header = uri.slice(5, idx);
645
+ mimeType = header.split(";")[0] || mimeType;
646
+ return uri.slice(idx + 1);
647
+ };
648
+ if (isNonEmptyString(img.data) ||
649
+ (img.data instanceof Uint8Array && img.data.length > 0)) {
650
+ if (isString(img.data)) {
651
+ // Inline data may itself be a data: URI — strip the prefix and take
652
+ // the real mime type from it rather than shipping the header as
653
+ // base64 payload.
654
+ base64Data = img.data.startsWith("data:")
655
+ ? parseImageDataUri(img.data)
656
+ : img.data;
113
657
  }
114
658
  else {
115
659
  base64Data = Buffer.from(img.data).toString("base64");
116
660
  }
117
661
  }
118
662
  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);
663
+ // resolveUri turns asset file:// URIs (what the chat pipeline produces)
664
+ // into data: URIs; http(s) URIs pass through to safeFetch.
665
+ const resolvedUri = img.uri.startsWith("data:")
666
+ ? img.uri
667
+ : await this.resolveUri(img.uri);
668
+ if (resolvedUri.startsWith("data:")) {
669
+ base64Data = parseImageDataUri(resolvedUri);
124
670
  }
125
671
  else {
126
- const resp = await this._fetch(img.uri);
672
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
127
673
  if (!resp.ok)
128
674
  throw new Error(`Failed to fetch image: ${resp.status}`);
129
- mimeType = resp.headers.get("content-type") ?? mimeType;
675
+ mimeType =
676
+ stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
130
677
  base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
131
678
  }
132
679
  }
@@ -138,87 +685,271 @@ export class GeminiProvider extends BaseProvider {
138
685
  if (content.type === "audio") {
139
686
  const aud = content.audio;
140
687
  let base64Data;
141
- let mimeType = aud.mimeType ?? "audio/mp3";
142
- if (aud.data) {
143
- if (typeof aud.data === "string") {
144
- base64Data = aud.data;
688
+ let mimeType = aud.mimeType;
689
+ const parseAudioDataUri = (uri) => {
690
+ const idx = uri.indexOf(",");
691
+ if (idx < 0)
692
+ throw new Error("Invalid audio data URI");
693
+ const header = uri.slice(5, idx);
694
+ mimeType = mimeType ?? header.split(";")[0];
695
+ return uri.slice(idx + 1);
696
+ };
697
+ if (isNonEmptyString(aud.data) ||
698
+ (aud.data instanceof Uint8Array && aud.data.length > 0)) {
699
+ if (isString(aud.data)) {
700
+ base64Data = aud.data.startsWith("data:")
701
+ ? parseAudioDataUri(aud.data)
702
+ : aud.data;
703
+ mimeType =
704
+ mimeType ?? sniffAudioMime(Buffer.from(base64Data, "base64"));
145
705
  }
146
706
  else {
147
- base64Data = Buffer.from(aud.data).toString("base64");
707
+ const bytes = Buffer.from(aud.data);
708
+ base64Data = bytes.toString("base64");
709
+ mimeType = mimeType ?? sniffAudioMime(bytes);
148
710
  }
149
711
  }
150
712
  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);
713
+ const resolvedUri = aud.uri.startsWith("data:")
714
+ ? aud.uri
715
+ : await this.resolveUri(aud.uri);
716
+ if (resolvedUri.startsWith("data:")) {
717
+ base64Data = parseAudioDataUri(resolvedUri);
156
718
  }
157
719
  else {
158
- const resp = await this._fetch(aud.uri);
720
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
159
721
  if (!resp.ok)
160
722
  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");
723
+ const bytes = Buffer.from(await resp.arrayBuffer());
724
+ mimeType =
725
+ stripMimeParams(resp.headers.get("content-type")) ??
726
+ mimeType ??
727
+ sniffAudioMime(bytes);
728
+ base64Data = bytes.toString("base64");
163
729
  }
164
730
  }
165
731
  else {
166
732
  base64Data = "";
167
733
  }
168
- return { inlineData: { mimeType, data: base64Data } };
734
+ return {
735
+ inlineData: { mimeType: geminiAudioMime(mimeType), data: base64Data }
736
+ };
737
+ }
738
+ if (content.type === "video") {
739
+ return await this.videoContentToGeminiPart(content.video);
169
740
  }
170
741
  return { text: "[unsupported content type]" };
171
742
  }
743
+ /**
744
+ * Source a video's bytes the way the image and audio branches do, then send
745
+ * them inline when small enough and through the Files API when not.
746
+ */
747
+ async videoContentToGeminiPart(video) {
748
+ let bytes;
749
+ let mimeType = video.mimeType;
750
+ const parseVideoDataUri = (uri) => {
751
+ const idx = uri.indexOf(",");
752
+ if (idx < 0)
753
+ throw new Error("Invalid video data URI");
754
+ const header = uri.slice(5, idx);
755
+ mimeType = mimeType ?? stripMimeParams(header.split(";base64")[0]);
756
+ return Buffer.from(uri.slice(idx + 1), "base64");
757
+ };
758
+ if (isNonEmptyString(video.data) ||
759
+ (video.data instanceof Uint8Array && video.data.length > 0)) {
760
+ if (isString(video.data)) {
761
+ bytes = video.data.startsWith("data:")
762
+ ? parseVideoDataUri(video.data)
763
+ : Buffer.from(video.data, "base64");
764
+ }
765
+ else {
766
+ bytes = Buffer.from(video.data);
767
+ }
768
+ }
769
+ else if (video.uri) {
770
+ const resolvedUri = video.uri.startsWith("data:")
771
+ ? video.uri
772
+ : await this.resolveUri(video.uri);
773
+ if (resolvedUri.startsWith("data:")) {
774
+ bytes = parseVideoDataUri(resolvedUri);
775
+ }
776
+ else {
777
+ const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
778
+ if (!resp.ok)
779
+ throw new Error(`Failed to fetch video: ${resp.status}`);
780
+ mimeType =
781
+ stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
782
+ bytes = Buffer.from(await resp.arrayBuffer());
783
+ }
784
+ }
785
+ else {
786
+ bytes = Buffer.alloc(0);
787
+ }
788
+ const resolvedMime = mimeType ?? sniffVideoMime(bytes);
789
+ if (bytes.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
790
+ const fileUri = await this.uploadFileToGemini(bytes, resolvedMime);
791
+ return { fileData: { mimeType: resolvedMime, fileUri } };
792
+ }
793
+ return {
794
+ inlineData: { mimeType: resolvedMime, data: bytes.toString("base64") }
795
+ };
796
+ }
797
+ /**
798
+ * Upload bytes with the Files API resumable protocol and wait until the file
799
+ * is ACTIVE — Gemini rejects a `fileData` part that references a file still
800
+ * being processed. Returns the file's uri.
801
+ */
802
+ async uploadFileToGemini(bytes, mimeType) {
803
+ const startResp = await this._fetch(`${GEMINI_UPLOAD_BASE}?key=${this.apiKey}`, {
804
+ method: "POST",
805
+ headers: {
806
+ "X-Goog-Upload-Protocol": "resumable",
807
+ "X-Goog-Upload-Command": "start",
808
+ "X-Goog-Upload-Header-Content-Length": String(bytes.length),
809
+ "X-Goog-Upload-Header-Content-Type": mimeType,
810
+ "Content-Type": "application/json"
811
+ },
812
+ body: JSON.stringify({
813
+ file: { display_name: `nodetool-upload.${mimeType.split("/")[1]}` }
814
+ })
815
+ });
816
+ if (!startResp.ok) {
817
+ const errText = await startResp.text();
818
+ throw new Error(`Gemini file upload failed to start ${startResp.status}: ${errText}`);
819
+ }
820
+ const uploadUrl = startResp.headers.get("x-goog-upload-url");
821
+ if (!uploadUrl) {
822
+ throw new Error("Gemini file upload returned no X-Goog-Upload-URL");
823
+ }
824
+ const uploadResp = await this._fetch(uploadUrl, {
825
+ method: "POST",
826
+ headers: {
827
+ "Content-Length": String(bytes.length),
828
+ "X-Goog-Upload-Offset": "0",
829
+ "X-Goog-Upload-Command": "upload, finalize"
830
+ },
831
+ body: new Blob([new Uint8Array(bytes)], {
832
+ type: mimeType
833
+ })
834
+ });
835
+ if (!uploadResp.ok) {
836
+ const errText = await uploadResp.text();
837
+ throw new Error(`Gemini file upload failed ${uploadResp.status}: ${errText}`);
838
+ }
839
+ const uploaded = (await uploadResp.json());
840
+ let file = uploaded.file;
841
+ if (!file?.name || !file.uri) {
842
+ throw new Error("Gemini file upload returned no file record");
843
+ }
844
+ const fileUri = file.uri;
845
+ const fileName = file.name;
846
+ for (let poll = 0; poll < GEMINI_FILE_MAX_POLLS; poll++) {
847
+ if (file?.state === "ACTIVE")
848
+ return fileUri;
849
+ if (file?.state === "FAILED") {
850
+ throw new Error(`Gemini file processing failed: ${file.error?.message ?? "unknown error"}`);
851
+ }
852
+ await this._sleep(GEMINI_FILE_POLL_INTERVAL_MS);
853
+ const pollResp = await this._fetch(`${GEMINI_API_BASE}/${fileName}`, {
854
+ headers: { "x-goog-api-key": this.apiKey }
855
+ });
856
+ if (!pollResp.ok) {
857
+ const errText = await pollResp.text();
858
+ throw new Error(`Gemini file poll failed ${pollResp.status}: ${errText}`);
859
+ }
860
+ file = (await pollResp.json());
861
+ }
862
+ if (file?.state === "ACTIVE")
863
+ return fileUri;
864
+ throw new Error(`Gemini file ${fileName} did not become ACTIVE in time`);
865
+ }
172
866
  /**
173
867
  * Convert our Message array into Gemini contents + optional system instruction.
174
868
  */
175
- async convertMessages(messages) {
869
+ async convertMessages(messages, nameMap = new Map()) {
176
870
  let systemInstruction;
177
871
  const contents = [];
872
+ // Gemini correlates a tool result to its call by the function *name*, not
873
+ // by id (and our tool-call ids are synthesized — never valid Gemini
874
+ // function names). Map each tool-call id back to its function name so the
875
+ // `functionResponse.name` below matches the earlier `functionCall.name`.
876
+ const toolCallNames = new Map();
877
+ for (const m of messages) {
878
+ if (m.role === "assistant" && m.toolCalls) {
879
+ for (const tc of m.toolCalls) {
880
+ if (tc.id)
881
+ toolCallNames.set(tc.id, nameMap.get(tc.name) ?? tc.name);
882
+ }
883
+ }
884
+ }
178
885
  for (const msg of messages) {
179
886
  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(" ");
887
+ const instruction = isString(msg.content)
888
+ ? msg.content
889
+ : (msg.content ?? [])
890
+ .filter((c) => c.type === "text")
891
+ .map((c) => c.text)
892
+ .join(" ");
893
+ systemInstruction = systemInstruction
894
+ ? `${systemInstruction}\n${instruction}`
895
+ : instruction;
187
896
  continue;
188
897
  }
189
898
  if (msg.role === "tool") {
190
- // Tool result → model role with functionResponse part
191
- const responseText = typeof msg.content === "string"
899
+ // Tool result → user role with functionResponse part. The name must
900
+ // match the originating functionCall's name, resolved from the call id.
901
+ const responseText = isString(msg.content)
192
902
  ? msg.content
193
903
  : 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
- });
904
+ const functionName = (msg.toolCallId ? toolCallNames.get(msg.toolCallId) : undefined) ??
905
+ msg.toolCallId ??
906
+ "unknown";
907
+ const responsePart = {
908
+ functionResponse: {
909
+ name: functionName,
910
+ id: msg.toolCallId ?? undefined,
911
+ response: { result: responseText }
912
+ }
913
+ };
914
+ // Merge parallel tool results into a single user turn so the request
915
+ // keeps alternating user/model roles.
916
+ const prev = contents[contents.length - 1];
917
+ if (prev &&
918
+ prev.role === "user" &&
919
+ prev.parts.length > 0 &&
920
+ prev.parts.every((p) => p.functionResponse !== undefined)) {
921
+ prev.parts.push(responsePart);
922
+ }
923
+ else {
924
+ appendGeminiContent(contents, {
925
+ role: "user",
926
+ parts: [responsePart]
927
+ });
928
+ }
205
929
  continue;
206
930
  }
207
931
  if (msg.role === "assistant") {
208
932
  // If we have raw Gemini parts (with thought content), replay them exactly
209
933
  if (msg._rawGeminiParts && Array.isArray(msg._rawGeminiParts)) {
210
- contents.push({
934
+ // Copy: the contents we build get merged and stamped below, and the
935
+ // message's own parts must survive that untouched for the next turn.
936
+ appendGeminiContent(contents, {
211
937
  role: "model",
212
- parts: msg._rawGeminiParts
938
+ parts: msg._rawGeminiParts.map((part) => ({
939
+ ...part
940
+ }))
213
941
  });
214
942
  continue;
215
943
  }
216
944
  const parts = [];
217
- // Tool calls
218
945
  if (msg.toolCalls && msg.toolCalls.length > 0) {
219
946
  for (const tc of msg.toolCalls) {
220
947
  const part = {
221
- functionCall: { name: tc.name, args: tc.args }
948
+ functionCall: {
949
+ id: tc.id,
950
+ name: nameMap.get(tc.name) ?? tc.name,
951
+ args: tc.args
952
+ }
222
953
  };
223
954
  if (tc.thought_signature) {
224
955
  part.thoughtSignature = tc.thought_signature;
@@ -226,8 +957,7 @@ export class GeminiProvider extends BaseProvider {
226
957
  parts.push(part);
227
958
  }
228
959
  }
229
- // Text / content
230
- if (typeof msg.content === "string" && msg.content) {
960
+ if (isNonEmptyString(msg.content)) {
231
961
  parts.push({ text: msg.content });
232
962
  }
233
963
  else if (Array.isArray(msg.content)) {
@@ -236,13 +966,12 @@ export class GeminiProvider extends BaseProvider {
236
966
  }
237
967
  }
238
968
  if (parts.length > 0) {
239
- contents.push({ role: "model", parts });
969
+ appendGeminiContent(contents, { role: "model", parts });
240
970
  }
241
971
  continue;
242
972
  }
243
- // user
244
973
  const parts = [];
245
- if (typeof msg.content === "string") {
974
+ if (isString(msg.content)) {
246
975
  parts.push({ text: msg.content });
247
976
  }
248
977
  else if (Array.isArray(msg.content)) {
@@ -251,17 +980,26 @@ export class GeminiProvider extends BaseProvider {
251
980
  }
252
981
  }
253
982
  if (parts.length > 0) {
254
- contents.push({ role: "user", parts });
983
+ appendGeminiContent(contents, { role: "user", parts });
255
984
  }
256
985
  }
986
+ signUnsignedFunctionCalls(contents);
257
987
  return { contents, systemInstruction };
258
988
  }
259
989
  formatTools(tools) {
990
+ return this.formatToolsForModel(tools, "gemini-3");
991
+ }
992
+ formatToolsForModel(tools, model) {
260
993
  const nameMap = new Map();
261
994
  const reverseMap = new Map();
262
995
  const usedNames = new Set();
263
996
  const declarations = [];
264
997
  for (const tool of tools) {
998
+ if ((tool.name === WEB_SEARCH_TOOL_NAME &&
999
+ this.usesNativeWebSearch(model, tools)) ||
1000
+ tool.type === "code_interpreter") {
1001
+ continue;
1002
+ }
265
1003
  const original = tool.name;
266
1004
  let unique = sanitizeToolName(original);
267
1005
  let suffix = 2;
@@ -273,7 +1011,10 @@ export class GeminiProvider extends BaseProvider {
273
1011
  usedNames.add(unique);
274
1012
  nameMap.set(original, unique);
275
1013
  reverseMap.set(unique, original);
276
- const rawParameters = tool.inputSchema ?? { type: "object", properties: {} };
1014
+ const rawParameters = tool.inputSchema ?? {
1015
+ type: "object",
1016
+ properties: {}
1017
+ };
277
1018
  declarations.push({
278
1019
  name: unique,
279
1020
  description: tool.description ?? "",
@@ -286,20 +1027,82 @@ export class GeminiProvider extends BaseProvider {
286
1027
  reverseMap
287
1028
  };
288
1029
  }
1030
+ usesNativeWebSearch(model, tools) {
1031
+ const hasWebSearch = tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
1032
+ if (!hasWebSearch)
1033
+ return false;
1034
+ const hasCustomTools = this.hasCustomFunctionTools(tools);
1035
+ return !hasCustomTools || isGemini3Model(model);
1036
+ }
1037
+ hasCustomFunctionTools(tools) {
1038
+ return tools.some((tool) => tool.name !== WEB_SEARCH_TOOL_NAME &&
1039
+ tool.type !== "code_interpreter");
1040
+ }
1041
+ validateToolCombination(model, tools) {
1042
+ if (isGemini3Model(model))
1043
+ return;
1044
+ const hasCodeExecution = tools.some((tool) => tool.type === "code_interpreter");
1045
+ if (hasCodeExecution && this.hasCustomFunctionTools(tools)) {
1046
+ throw new Error(`Gemini code execution cannot be combined with other tools on ${model}; ` +
1047
+ "only Gemini 3 models support this combination");
1048
+ }
1049
+ }
1050
+ /**
1051
+ * Fill in `tools` and `toolConfig` on a request body.
1052
+ *
1053
+ * Built-in tools (googleSearch, codeExecution) run server side. When they are
1054
+ * combined with functionDeclarations, Gemini rejects the request unless
1055
+ * `toolConfig.includeServerSideToolInvocations` is set — the built-in calls
1056
+ * and their results have to be echoed back into the conversation for the
1057
+ * function-calling loop to stay coherent.
1058
+ */
1059
+ applyTools(body, model, tools, geminiTools, nameMap, toolChoice) {
1060
+ if (geminiTools.length > 0) {
1061
+ body.tools = geminiTools;
1062
+ }
1063
+ let hasBuiltIn = false;
1064
+ if (this.usesNativeWebSearch(model, tools)) {
1065
+ body.tools = [...(body.tools ?? []), { googleSearch: {} }];
1066
+ hasBuiltIn = true;
1067
+ }
1068
+ if (tools.some((tool) => tool.type === "code_interpreter")) {
1069
+ body.tools = [...(body.tools ?? []), { codeExecution: {} }];
1070
+ hasBuiltIn = true;
1071
+ }
1072
+ const toolConfig = {};
1073
+ if (hasBuiltIn && geminiTools.length > 0) {
1074
+ toolConfig.includeServerSideToolInvocations = true;
1075
+ }
1076
+ if (toolChoice &&
1077
+ (toolChoice === "any" ? geminiTools.length > 0 : nameMap.has(toolChoice))) {
1078
+ const selected = toolChoice === "any"
1079
+ ? undefined
1080
+ : (nameMap.get(toolChoice) ?? sanitizeToolName(toolChoice));
1081
+ const functionCallingConfig = {
1082
+ mode: "ANY"
1083
+ };
1084
+ if (selected) {
1085
+ functionCallingConfig.allowedFunctionNames = [selected];
1086
+ }
1087
+ toolConfig.functionCallingConfig = functionCallingConfig;
1088
+ }
1089
+ if (Object.keys(toolConfig).length > 0) {
1090
+ body.toolConfig = toolConfig;
1091
+ }
1092
+ }
289
1093
  // ---------------------------------------------------------------------------
290
1094
  // Non-streaming generation
291
1095
  // ---------------------------------------------------------------------------
292
1096
  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);
1097
+ const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
1098
+ this.validateToolCombination(model, tools);
1099
+ const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
1100
+ const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
296
1101
  const body = { contents };
297
1102
  if (systemInstruction) {
298
1103
  body.systemInstruction = { parts: [{ text: systemInstruction }] };
299
1104
  }
300
- if (geminiTools.length > 0) {
301
- body.tools = geminiTools;
302
- }
1105
+ this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
303
1106
  const generationConfig = {
304
1107
  maxOutputTokens: maxTokens
305
1108
  };
@@ -307,13 +1110,19 @@ export class GeminiProvider extends BaseProvider {
307
1110
  generationConfig.temperature = temperature;
308
1111
  if (topP != null)
309
1112
  generationConfig.topP = topP;
1113
+ if (presencePenalty != null)
1114
+ generationConfig.presencePenalty = presencePenalty;
1115
+ if (frequencyPenalty != null)
1116
+ generationConfig.frequencyPenalty = frequencyPenalty;
310
1117
  body.generationConfig = generationConfig;
311
1118
  log.debug("Gemini request", { model });
312
1119
  const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
1120
+ this.recordRequestPayload(body);
313
1121
  const response = await this._fetch(url, {
314
1122
  method: "POST",
315
1123
  headers: { "Content-Type": "application/json" },
316
- body: JSON.stringify(body)
1124
+ body: JSON.stringify(body),
1125
+ signal: args.signal
317
1126
  });
318
1127
  if (!response.ok) {
319
1128
  const text = await response.text();
@@ -323,30 +1132,33 @@ export class GeminiProvider extends BaseProvider {
323
1132
  });
324
1133
  throw new Error(`Gemini API error ${response.status}: ${text}`);
325
1134
  }
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);
1135
+ const decoded = decodeGeminiGenerateContent(await response.json());
1136
+ this.trackGeminiUsage(model, decoded.usage);
1137
+ return this.extractMessage(decoded.parts, reverseMap);
1138
+ }
1139
+ /** Record token usage from a Gemini usageMetadata block (if present). */
1140
+ trackGeminiUsage(model, usage) {
1141
+ if (!usage)
1142
+ return;
1143
+ this.trackUsage(model, {
1144
+ inputTokens: usage.promptTokenCount ?? 0,
1145
+ outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
1146
+ cachedTokens: usage.cachedContentTokenCount ?? 0
1147
+ });
335
1148
  }
336
1149
  // ---------------------------------------------------------------------------
337
1150
  // Streaming generation
338
1151
  // ---------------------------------------------------------------------------
339
1152
  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);
1153
+ const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
1154
+ this.validateToolCombination(model, tools);
1155
+ const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
1156
+ const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
343
1157
  const body = { contents };
344
1158
  if (systemInstruction) {
345
1159
  body.systemInstruction = { parts: [{ text: systemInstruction }] };
346
1160
  }
347
- if (geminiTools.length > 0) {
348
- body.tools = geminiTools;
349
- }
1161
+ this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
350
1162
  const generationConfig = {
351
1163
  maxOutputTokens: maxTokens
352
1164
  };
@@ -354,13 +1166,19 @@ export class GeminiProvider extends BaseProvider {
354
1166
  generationConfig.temperature = temperature;
355
1167
  if (topP != null)
356
1168
  generationConfig.topP = topP;
1169
+ if (presencePenalty != null)
1170
+ generationConfig.presencePenalty = presencePenalty;
1171
+ if (frequencyPenalty != null)
1172
+ generationConfig.frequencyPenalty = frequencyPenalty;
357
1173
  body.generationConfig = generationConfig;
358
1174
  log.debug("Gemini request", { model });
359
1175
  const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
1176
+ this.recordRequestPayload(body);
360
1177
  const response = await this._fetch(url, {
361
1178
  method: "POST",
362
1179
  headers: { "Content-Type": "application/json" },
363
- body: JSON.stringify(body)
1180
+ body: JSON.stringify(body),
1181
+ signal: args.signal
364
1182
  });
365
1183
  if (!response.ok) {
366
1184
  const text = await response.text();
@@ -373,71 +1191,51 @@ export class GeminiProvider extends BaseProvider {
373
1191
  if (!response.body) {
374
1192
  throw new Error("Gemini streaming response has no body");
375
1193
  }
376
- const reader = response.body.getReader();
377
- const decoder = new TextDecoder();
378
- let buffer = "";
379
1194
  // Accumulate all parts across SSE events for raw replay.
380
1195
  // Gemini thinking models emit thought parts and function calls across
381
1196
  // separate SSE events, but they must all be sent back together.
382
1197
  const allParts = [];
383
1198
  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
1199
+ // Gemini SSE reports CUMULATIVE usageMetadata; keep the last one seen and
1200
+ // record it once after the stream (accumulating each event would over-count).
1201
+ let lastUsage;
1202
+ for await (const event of decodeGeminiSse(response.body, args.signal)) {
1203
+ const eventError = geminiResponseError(event);
1204
+ if (eventError)
1205
+ throw eventError;
1206
+ if (event.usageMetadata)
1207
+ lastUsage = event.usageMetadata;
1208
+ const parts = event.candidates?.[0]?.content?.parts;
1209
+ if (!parts)
1210
+ continue;
1211
+ for (const part of parts) {
1212
+ allParts.push(part);
1213
+ if (part.text !== undefined && !part.thought) {
1214
+ const chunk = {
1215
+ type: "chunk",
1216
+ content: part.text,
1217
+ done: false
1218
+ };
1219
+ yield chunk;
1220
+ }
1221
+ else if (part.functionCall) {
1222
+ const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
1223
+ const toolCall = {
1224
+ id: part.functionCall.id ??
1225
+ `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1226
+ name: originalName,
1227
+ args: part.functionCall.args ?? {}
1228
+ };
1229
+ if (part.thoughtSignature) {
1230
+ toolCall.thought_signature = part.thoughtSignature;
432
1231
  }
1232
+ pendingToolCalls.push(toolCall);
433
1233
  }
434
1234
  }
435
1235
  }
436
- finally {
437
- reader.releaseLock();
438
- }
1236
+ this.trackGeminiUsage(model, lastUsage);
439
1237
  // Attach accumulated raw parts to tool calls for thought replay
440
- const hasThoughts = allParts.some((p) => p.thought);
1238
+ const hasThoughts = allParts.some((p) => p.thought || p.thoughtSignature);
441
1239
  for (const tc of pendingToolCalls) {
442
1240
  if (hasThoughts) {
443
1241
  tc._rawGeminiParts = allParts;
@@ -459,13 +1257,14 @@ export class GeminiProvider extends BaseProvider {
459
1257
  const textParts = [];
460
1258
  const toolCalls = [];
461
1259
  for (const part of parts) {
462
- if (part.text !== undefined) {
1260
+ if (part.text !== undefined && !part.thought) {
463
1261
  textParts.push(part.text);
464
1262
  }
465
1263
  else if (part.functionCall) {
466
1264
  const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
467
1265
  const tc = {
468
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1266
+ id: part.functionCall.id ??
1267
+ `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
469
1268
  name: originalName,
470
1269
  args: part.functionCall.args ?? {}
471
1270
  };
@@ -475,7 +1274,7 @@ export class GeminiProvider extends BaseProvider {
475
1274
  toolCalls.push(tc);
476
1275
  }
477
1276
  }
478
- const hasThoughts = parts.some((p) => p.thought);
1277
+ const hasThoughts = parts.some((p) => p.thought || p.thoughtSignature);
479
1278
  const msg = {
480
1279
  role: "assistant",
481
1280
  content: textParts.join("") || null,
@@ -492,11 +1291,50 @@ export class GeminiProvider extends BaseProvider {
492
1291
  async getAvailableImageModels() {
493
1292
  return [
494
1293
  {
495
- id: "gemini-2.0-flash-preview-image-generation",
496
- name: "Gemini 2.0 Flash Image Gen",
497
- provider: "gemini"
1294
+ id: "gemini-3.1-flash-image",
1295
+ name: "Gemini 3.1 Flash Image",
1296
+ provider: "gemini",
1297
+ supportedTasks: ["text_to_image", "image_to_image"],
1298
+ aspectRatios: [
1299
+ "1:1",
1300
+ "2:3",
1301
+ "3:2",
1302
+ "3:4",
1303
+ "4:3",
1304
+ "4:5",
1305
+ "5:4",
1306
+ "9:16",
1307
+ "16:9",
1308
+ "21:9"
1309
+ ],
1310
+ resolutions: ["1K", "2K", "4K"]
498
1311
  },
499
- { id: "imagen-3.0-generate-002", name: "Imagen 3.0", provider: "gemini" }
1312
+ {
1313
+ id: "gemini-3.1-flash-lite-image",
1314
+ name: "Gemini 3.1 Flash-Lite Image",
1315
+ provider: "gemini",
1316
+ supportedTasks: ["text_to_image", "image_to_image"],
1317
+ aspectRatios: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"]
1318
+ },
1319
+ {
1320
+ id: "gemini-3-pro-image",
1321
+ name: "Gemini 3 Pro Image",
1322
+ provider: "gemini",
1323
+ supportedTasks: ["text_to_image", "image_to_image"],
1324
+ aspectRatios: [
1325
+ "1:1",
1326
+ "2:3",
1327
+ "3:2",
1328
+ "3:4",
1329
+ "4:3",
1330
+ "4:5",
1331
+ "5:4",
1332
+ "9:16",
1333
+ "16:9",
1334
+ "21:9"
1335
+ ],
1336
+ resolutions: ["1K", "2K", "4K"]
1337
+ }
500
1338
  ];
501
1339
  }
502
1340
  async getAvailableTTSModels() {
@@ -533,6 +1371,30 @@ export class GeminiProvider extends BaseProvider {
533
1371
  "Sulafat"
534
1372
  ];
535
1373
  return [
1374
+ {
1375
+ id: "gemini-3.8-flash-tts",
1376
+ name: "Gemini 3.8 Flash TTS",
1377
+ provider: "gemini",
1378
+ voices
1379
+ },
1380
+ {
1381
+ id: "gemini-3.8-flash-lite-tts",
1382
+ name: "Gemini 3.8 Flash-Lite TTS",
1383
+ provider: "gemini",
1384
+ voices
1385
+ },
1386
+ {
1387
+ id: "gemini-3.1-flash-tts-preview",
1388
+ name: "Gemini 3.1 Flash TTS Preview",
1389
+ provider: "gemini",
1390
+ voices
1391
+ },
1392
+ {
1393
+ id: "gemini-2.5-flash-preview-tts",
1394
+ name: "Gemini 2.5 Flash TTS",
1395
+ provider: "gemini",
1396
+ voices
1397
+ },
536
1398
  {
537
1399
  id: "gemini-2.5-pro-preview-tts",
538
1400
  name: "Gemini 2.5 Pro TTS",
@@ -543,27 +1405,51 @@ export class GeminiProvider extends BaseProvider {
543
1405
  }
544
1406
  async getAvailableASRModels() {
545
1407
  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" }
1408
+ {
1409
+ // The transcription-specific model. It answers `generateContent`, so
1410
+ // it runs through the same path as the general models below.
1411
+ id: "gemini-3.5-transcribe",
1412
+ name: "Gemini 3.5 Transcribe",
1413
+ provider: "gemini"
1414
+ },
1415
+ {
1416
+ id: "gemini-3.5-flash",
1417
+ name: "Gemini 3.5 Flash",
1418
+ provider: "gemini"
1419
+ },
1420
+ {
1421
+ id: "gemini-3.1-flash-lite",
1422
+ name: "Gemini 3.1 Flash-Lite",
1423
+ provider: "gemini"
1424
+ }
548
1425
  ];
549
1426
  }
550
1427
  async getAvailableVideoModels() {
1428
+ return [...GEMINI_VIDEO_MODELS];
1429
+ }
1430
+ async getAvailableMusicModels() {
551
1431
  return [
552
1432
  {
553
- id: "veo-3.1-generate-preview",
554
- name: "Veo 3.1 Preview",
555
- provider: "gemini"
1433
+ id: "lyria-3.5",
1434
+ name: "Lyria 3.5",
1435
+ provider: "gemini",
1436
+ supportedTasks: ["text_to_music"]
556
1437
  },
557
- { id: "veo-2.0-generate-001", name: "Veo 2.0", provider: "gemini" }
1438
+ {
1439
+ id: "lyria-3-clip-preview",
1440
+ name: "Lyria 3 Clip Preview",
1441
+ provider: "gemini",
1442
+ supportedTasks: ["text_to_music"]
1443
+ }
558
1444
  ];
559
1445
  }
560
1446
  async getAvailableEmbeddingModels() {
561
1447
  return [
562
1448
  {
563
- id: "text-embedding-004",
564
- name: "Text Embedding 004",
1449
+ id: "gemini-embedding-2",
1450
+ name: "Gemini Embedding 2",
565
1451
  provider: "gemini",
566
- dimensions: 768
1452
+ dimensions: 3072
567
1453
  },
568
1454
  {
569
1455
  id: "gemini-embedding-001",
@@ -581,7 +1467,7 @@ export class GeminiProvider extends BaseProvider {
581
1467
  if (!text || (Array.isArray(text) && text.length === 0)) {
582
1468
  throw new Error("text must not be empty");
583
1469
  }
584
- const texts = typeof text === "string" ? [text] : text;
1470
+ const texts = isString(text) ? [text] : text;
585
1471
  // Gemini embedContent supports a single content; batch by calling per text
586
1472
  const embeddings = [];
587
1473
  for (const t of texts) {
@@ -605,7 +1491,9 @@ export class GeminiProvider extends BaseProvider {
605
1491
  if (!data.embedding?.values) {
606
1492
  throw new Error("No embedding returned from Gemini API");
607
1493
  }
608
- embeddings.push(data.embedding.values);
1494
+ embeddings.push(dimensions && dimensions < 3072
1495
+ ? normalizeEmbedding(data.embedding.values)
1496
+ : data.embedding.values);
609
1497
  }
610
1498
  return embeddings;
611
1499
  }
@@ -619,11 +1507,20 @@ export class GeminiProvider extends BaseProvider {
619
1507
  const modelId = params.model.id;
620
1508
  if (modelId.startsWith("gemini-")) {
621
1509
  // Use generateContent with IMAGE response modality
1510
+ const imageConfig = {};
1511
+ if (params.aspectRatio)
1512
+ imageConfig.aspectRatio = params.aspectRatio;
1513
+ if (params.resolution)
1514
+ imageConfig.imageSize = params.resolution;
1515
+ const generationConfig = {
1516
+ responseModalities: ["IMAGE", "TEXT"]
1517
+ };
1518
+ if (Object.keys(imageConfig).length > 0) {
1519
+ generationConfig.imageConfig = imageConfig;
1520
+ }
622
1521
  const body = {
623
1522
  contents: [{ role: "user", parts: [{ text: params.prompt }] }],
624
- generationConfig: {
625
- responseModalities: ["IMAGE", "TEXT"]
626
- }
1523
+ generationConfig
627
1524
  };
628
1525
  const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
629
1526
  const response = await this._fetch(url, {
@@ -635,7 +1532,7 @@ export class GeminiProvider extends BaseProvider {
635
1532
  const errText = await response.text();
636
1533
  throw new Error(`Gemini text-to-image failed ${response.status}: ${errText}`);
637
1534
  }
638
- const data = (await response.json());
1535
+ const data = parseGeminiResponse(await response.json());
639
1536
  const parts = data.candidates?.[0]?.content?.parts;
640
1537
  if (!parts)
641
1538
  throw new Error("No candidates in response");
@@ -646,33 +1543,13 @@ export class GeminiProvider extends BaseProvider {
646
1543
  }
647
1544
  throw new Error("No image data returned in response");
648
1545
  }
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"));
1546
+ // Google shut down Imagen in the Gemini API; `predict` now answers 404.
1547
+ throw new Error(`Model ${modelId} is not available. Use a gemini-*-image model for text-to-image.`);
671
1548
  }
672
1549
  // ---------------------------------------------------------------------------
673
1550
  // Image-to-image
674
1551
  // ---------------------------------------------------------------------------
675
- async imageToImage(image, params) {
1552
+ async imageToImage(images, params) {
676
1553
  if (!params.prompt) {
677
1554
  throw new Error("The input prompt cannot be empty.");
678
1555
  }
@@ -680,20 +1557,36 @@ export class GeminiProvider extends BaseProvider {
680
1557
  if (!modelId.startsWith("gemini-")) {
681
1558
  throw new Error(`Model ${modelId} does not support image-to-image. Only gemini-* models supported.`);
682
1559
  }
683
- const imageBase64 = Buffer.from(image).toString("base64");
1560
+ const imageParts = images
1561
+ .filter((b) => b && b.length > 0)
1562
+ .map((b) => ({
1563
+ inlineData: {
1564
+ mimeType: "image/png",
1565
+ data: Buffer.from(b).toString("base64")
1566
+ }
1567
+ }));
1568
+ if (imageParts.length === 0) {
1569
+ throw new Error("At least one input image is required");
1570
+ }
1571
+ const imageConfig = {};
1572
+ if (params.aspectRatio)
1573
+ imageConfig.aspectRatio = params.aspectRatio;
1574
+ if (params.resolution)
1575
+ imageConfig.imageSize = params.resolution;
1576
+ const generationConfig = {
1577
+ responseModalities: ["IMAGE", "TEXT"]
1578
+ };
1579
+ if (Object.keys(imageConfig).length > 0) {
1580
+ generationConfig.imageConfig = imageConfig;
1581
+ }
684
1582
  const body = {
685
1583
  contents: [
686
1584
  {
687
1585
  role: "user",
688
- parts: [
689
- { text: params.prompt },
690
- { inlineData: { mimeType: "image/png", data: imageBase64 } }
691
- ]
1586
+ parts: [{ text: params.prompt }, ...imageParts]
692
1587
  }
693
1588
  ],
694
- generationConfig: {
695
- responseModalities: ["IMAGE", "TEXT"]
696
- }
1589
+ generationConfig
697
1590
  };
698
1591
  const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
699
1592
  const response = await this._fetch(url, {
@@ -705,7 +1598,7 @@ export class GeminiProvider extends BaseProvider {
705
1598
  const errText = await response.text();
706
1599
  throw new Error(`Gemini image-to-image failed ${response.status}: ${errText}`);
707
1600
  }
708
- const data = (await response.json());
1601
+ const data = parseGeminiResponse(await response.json());
709
1602
  const parts = data.candidates?.[0]?.content?.parts;
710
1603
  if (!parts)
711
1604
  throw new Error("No candidates in response");
@@ -720,7 +1613,14 @@ export class GeminiProvider extends BaseProvider {
720
1613
  // Text-to-speech
721
1614
  // ---------------------------------------------------------------------------
722
1615
  async *textToSpeech(args) {
723
- const { text, model, voice = "Puck" } = args;
1616
+ const { model, voice = "Puck" } = args;
1617
+ const text = [
1618
+ args.language ? `Speak in ${args.language}.` : undefined,
1619
+ args.instructions,
1620
+ args.text
1621
+ ]
1622
+ .filter((value) => value !== undefined)
1623
+ .join(" ");
724
1624
  const body = {
725
1625
  contents: [{ role: "user", parts: [{ text }] }],
726
1626
  generationConfig: {
@@ -742,7 +1642,7 @@ export class GeminiProvider extends BaseProvider {
742
1642
  const errText = await response.text();
743
1643
  throw new Error(`Gemini TTS failed ${response.status}: ${errText}`);
744
1644
  }
745
- const data = (await response.json());
1645
+ const data = parseGeminiResponse(await response.json());
746
1646
  const parts = data.candidates?.[0]?.content?.parts;
747
1647
  if (!parts)
748
1648
  throw new Error("No audio in response");
@@ -763,31 +1663,34 @@ export class GeminiProvider extends BaseProvider {
763
1663
  if (!audio || audio.length === 0) {
764
1664
  throw new Error("audio must not be empty");
765
1665
  }
766
- // Detect MIME type from audio header
767
- let mimeType = "audio/wav";
1666
+ if (audio.length > 20 * 1024 * 1024) {
1667
+ throw new Error("Gemini inline audio is limited to 20 MB; upload the audio with the File API first");
1668
+ }
1669
+ // Detect MIME type from the audio header.
1670
+ let mimeType = geminiAudioMime(sniffAudioMime(audio));
768
1671
  if (audio[0] === 0x52 &&
769
1672
  audio[1] === 0x49 &&
770
1673
  audio[2] === 0x46 &&
771
1674
  audio[3] === 0x46) {
772
- mimeType = "audio/wav"; // RIFF
1675
+ mimeType = "audio/wav";
773
1676
  }
774
1677
  else if (audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) {
775
- mimeType = "audio/mp3"; // ID3
1678
+ mimeType = "audio/mp3";
776
1679
  }
777
1680
  else if (audio[0] === 0xff && (audio[1] === 0xfb || audio[1] === 0xf3)) {
778
- mimeType = "audio/mp3"; // MPEG sync
1681
+ mimeType = "audio/mp3";
779
1682
  }
780
1683
  else if (audio[0] === 0x66 &&
781
1684
  audio[1] === 0x4c &&
782
1685
  audio[2] === 0x61 &&
783
1686
  audio[3] === 0x43) {
784
- mimeType = "audio/flac"; // fLaC
1687
+ mimeType = "audio/flac";
785
1688
  }
786
1689
  else if (audio[0] === 0x4f &&
787
1690
  audio[1] === 0x67 &&
788
1691
  audio[2] === 0x67 &&
789
1692
  audio[3] === 0x53) {
790
- mimeType = "audio/ogg"; // OggS
1693
+ mimeType = "audio/ogg";
791
1694
  }
792
1695
  let promptText = args.prompt ?? "Transcribe this audio to text.";
793
1696
  if (language) {
@@ -818,7 +1721,7 @@ export class GeminiProvider extends BaseProvider {
818
1721
  const errText = await response.text();
819
1722
  throw new Error(`Gemini ASR failed ${response.status}: ${errText}`);
820
1723
  }
821
- const data = (await response.json());
1724
+ const data = parseGeminiResponse(await response.json());
822
1725
  const parts = data.candidates?.[0]?.content?.parts;
823
1726
  if (!parts)
824
1727
  return { text: "" };
@@ -828,142 +1731,423 @@ export class GeminiProvider extends BaseProvider {
828
1731
  .join("");
829
1732
  return { text };
830
1733
  }
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
- };
1734
+ buildVideoParameters(params) {
845
1735
  const parameters = {};
846
- if (params.negativePrompt)
1736
+ if (params.negativePrompt) {
847
1737
  parameters.negativePrompt = params.negativePrompt;
848
- if (params.aspectRatio)
1738
+ }
1739
+ if (params.aspectRatio) {
849
1740
  parameters.aspectRatio = params.aspectRatio;
850
- if (params.seed != null)
1741
+ }
1742
+ if (params.resolution) {
1743
+ parameters.resolution = params.resolution;
1744
+ }
1745
+ if (params.durationSeconds != null) {
1746
+ parameters.durationSeconds = params.durationSeconds;
1747
+ }
1748
+ if (params.seed != null) {
851
1749
  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
1750
  }
865
- const operation = (await response.json());
866
- // Poll for completion
867
- const maxWait = 600_000; // 10 minutes
1751
+ return parameters;
1752
+ }
1753
+ getVideoUri(operation) {
1754
+ return (operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video
1755
+ ?.uri ?? operation.response?.generatedVideos?.[0]?.video?.uri);
1756
+ }
1757
+ /**
1758
+ * The refusal to raise when an operation came back with no video, or null
1759
+ * when nothing says the filter is why. A filtered take leaves the operation
1760
+ * successful and the samples empty, so without this the caller sees only
1761
+ * "No video URI in response" — indistinguishable from a broken response, and
1762
+ * fatal to a run that has already paid for its other shots.
1763
+ */
1764
+ videoContentFilterRefusal(operation, modelId) {
1765
+ const rai = operation.response?.generateVideoResponse ?? operation.response;
1766
+ const reasons = rai?.raiMediaFilteredReasons ?? [];
1767
+ const filtered = rai?.raiMediaFilteredCount ?? 0;
1768
+ if (filtered <= 0 && reasons.length === 0)
1769
+ return null;
1770
+ const detail = reasons.length
1771
+ ? reasons.join("; ")
1772
+ : "no reason given by the provider";
1773
+ return new ContentFilterRefusal(`Veo filtered every generated video for this prompt: ${detail}`, { provider: "gemini", model: modelId, reasons });
1774
+ }
1775
+ async waitForVideoOperation(operation, timeoutSeconds, signal) {
1776
+ const maxWait = timeoutSeconds && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 600_000;
868
1777
  const pollInterval = 10_000;
869
1778
  let elapsed = 0;
870
1779
  let current = operation;
871
1780
  while (!current.done && elapsed < maxWait) {
872
- await new Promise((r) => setTimeout(r, pollInterval));
1781
+ await new Promise((resolve, reject) => {
1782
+ const onAbort = () => {
1783
+ clearTimeout(timer);
1784
+ reject(abortError(signal));
1785
+ };
1786
+ const timer = setTimeout(() => {
1787
+ signal?.removeEventListener("abort", onAbort);
1788
+ resolve();
1789
+ }, pollInterval);
1790
+ signal?.addEventListener("abort", onAbort, { once: true });
1791
+ });
873
1792
  elapsed += pollInterval;
874
- if (!current.name)
1793
+ if (!current.name) {
875
1794
  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);
1795
+ }
1796
+ const pollResp = await this._fetch(`${GEMINI_API_BASE}/${current.name}`, {
1797
+ headers: { "x-goog-api-key": this.apiKey },
1798
+ signal
1799
+ });
878
1800
  if (!pollResp.ok) {
879
1801
  const errText = await pollResp.text();
880
1802
  throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
881
1803
  }
882
1804
  current = (await pollResp.json());
883
1805
  }
884
- if (!current.done)
1806
+ if (!current.done) {
885
1807
  throw new Error("Video generation timed out");
886
- const videoUri = current.response?.generatedVideos?.[0]?.video?.uri;
887
- if (!videoUri)
1808
+ }
1809
+ if (current.error?.message) {
1810
+ // Vertex answers a filtered prompt through the operation's error slot
1811
+ // ("videos were filtered out because they violated ... usage
1812
+ // guidelines"), so the refusal has to be recognized here too.
1813
+ if (isContentFilterRefusal(current.error.message)) {
1814
+ throw new ContentFilterRefusal(current.error.message, {
1815
+ provider: "gemini"
1816
+ });
1817
+ }
1818
+ throw new Error(`Gemini video generation failed: ${current.error.message}`);
1819
+ }
1820
+ return current;
1821
+ }
1822
+ async downloadGeminiMedia(uri, signal) {
1823
+ const hostname = new URL(uri).hostname;
1824
+ const headers = hostname === "generativelanguage.googleapis.com"
1825
+ ? { "x-goog-api-key": this.apiKey }
1826
+ : undefined;
1827
+ const response = await safeFetch(uri, { headers, signal }, 5, this._fetch);
1828
+ if (!response.ok) {
1829
+ throw new Error(`Gemini media download failed: ${response.status}`);
1830
+ }
1831
+ return new Uint8Array(await response.arrayBuffer());
1832
+ }
1833
+ /**
1834
+ * Submit a Veo `predictLongRunning` request, wait for the operation, and
1835
+ * download the first generated video.
1836
+ */
1837
+ async runVeoOperation(modelId, body, label, options) {
1838
+ const signal = combineSignals(options.signal, options.timeoutSeconds);
1839
+ this.recordRequestPayload(body);
1840
+ const response = await this._fetch(`${GEMINI_API_BASE}/models/${modelId}:predictLongRunning`, {
1841
+ method: "POST",
1842
+ headers: {
1843
+ "Content-Type": "application/json",
1844
+ "x-goog-api-key": this.apiKey
1845
+ },
1846
+ body: JSON.stringify(body),
1847
+ signal
1848
+ });
1849
+ if (!response.ok) {
1850
+ const errText = await response.text();
1851
+ throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
1852
+ }
1853
+ const operation = await this.waitForVideoOperation((await response.json()), options.timeoutSeconds, signal);
1854
+ const videoUri = this.getVideoUri(operation);
1855
+ if (!videoUri) {
1856
+ const refusal = this.videoContentFilterRefusal(operation, modelId);
1857
+ if (refusal)
1858
+ throw refusal;
888
1859
  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());
1860
+ }
1861
+ return this.downloadGeminiMedia(videoUri, signal);
1862
+ }
1863
+ /** Send one request to the Interactions API and return the finished interaction. */
1864
+ async createInteraction(body, label, modelId, signal) {
1865
+ this.recordRequestPayload(body);
1866
+ const response = await this._fetch(`${GEMINI_API_BASE}/interactions`, {
1867
+ method: "POST",
1868
+ headers: {
1869
+ "Content-Type": "application/json",
1870
+ "x-goog-api-key": this.apiKey
1871
+ },
1872
+ body: JSON.stringify(body),
1873
+ signal
1874
+ });
1875
+ if (!response.ok) {
1876
+ const errText = await response.text();
1877
+ if (isContentFilterRefusal(errText)) {
1878
+ throw new ContentFilterRefusal(errText, {
1879
+ provider: "gemini",
1880
+ model: modelId
1881
+ });
1882
+ }
1883
+ throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
1884
+ }
1885
+ const interaction = (await response.json());
1886
+ if (interaction.status && interaction.status !== "completed") {
1887
+ throw new Error(`Gemini ${label} ended with status ${interaction.status}`);
1888
+ }
1889
+ return interaction;
1890
+ }
1891
+ /** The first `type` output of an interaction, inline or behind a file uri. */
1892
+ async interactionMedia(interaction, type, signal) {
1893
+ for (const step of interaction.steps ?? []) {
1894
+ for (const content of step.content ?? []) {
1895
+ if (content.type !== type)
1896
+ continue;
1897
+ if (content.data) {
1898
+ return {
1899
+ data: new Uint8Array(Buffer.from(content.data, "base64")),
1900
+ mimeType: content.mime_type
1901
+ };
1902
+ }
1903
+ if (content.uri) {
1904
+ return {
1905
+ data: await this.downloadGeminiMedia(content.uri, signal),
1906
+ mimeType: content.mime_type
1907
+ };
1908
+ }
1909
+ }
1910
+ }
1911
+ throw new Error(`Gemini returned no ${type} output`);
1912
+ }
1913
+ /** An Interactions API video block: inline, or uploaded when too large. */
1914
+ async omniVideoInput(video) {
1915
+ const mimeType = sniffVideoMime(video);
1916
+ if (video.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
1917
+ const uri = await this.uploadFileToGemini(video, mimeType);
1918
+ return { type: "video", uri, mime_type: mimeType };
1919
+ }
1920
+ return {
1921
+ type: "video",
1922
+ data: Buffer.from(video).toString("base64"),
1923
+ mime_type: mimeType
1924
+ };
1925
+ }
1926
+ /** Generate or edit a video with Gemini Omni through the Interactions API. */
1927
+ async runOmniVideo(modelId, input, options) {
1928
+ const responseFormat = { type: "video" };
1929
+ if (options.aspectRatio)
1930
+ responseFormat.aspect_ratio = options.aspectRatio;
1931
+ if (options.resolution)
1932
+ responseFormat.resolution = options.resolution;
1933
+ const signal = combineSignals(options.signal, options.timeoutSeconds);
1934
+ const interaction = await this.createInteraction({ model: modelId, input, response_format: responseFormat }, "video generation", modelId, signal);
1935
+ return (await this.interactionMedia(interaction, "video", signal)).data;
1936
+ }
1937
+ // ---------------------------------------------------------------------------
1938
+ // Text-to-video (Veo: long-running operation; Omni: Interactions API)
1939
+ // ---------------------------------------------------------------------------
1940
+ async textToVideo(params) {
1941
+ if (!params.prompt) {
1942
+ throw new Error("The input prompt cannot be empty.");
1943
+ }
1944
+ const modelId = params.model.id;
1945
+ if (isOmniVideoModel(modelId)) {
1946
+ return this.runOmniVideo(modelId, [{ type: "text", text: params.prompt }], params);
1947
+ }
1948
+ if (!modelId.startsWith("veo-")) {
1949
+ throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for text-to-video.`);
1950
+ }
1951
+ const body = {
1952
+ instances: [{ prompt: params.prompt }]
1953
+ };
1954
+ const parameters = this.buildVideoParameters(params);
1955
+ if (Object.keys(parameters).length > 0) {
1956
+ body.parameters = parameters;
1957
+ }
1958
+ return this.runVeoOperation(modelId, body, "video generation", params);
894
1959
  }
895
1960
  // ---------------------------------------------------------------------------
896
- // Image-to-video (Veo models)
1961
+ // Image-to-video
897
1962
  // ---------------------------------------------------------------------------
898
1963
  async imageToVideo(image, params) {
899
1964
  if (!image || image.length === 0) {
900
1965
  throw new Error("Input image cannot be empty.");
901
1966
  }
902
1967
  const modelId = params.model.id;
1968
+ const prompt = params.prompt || "Animate this image";
1969
+ if (isOmniVideoModel(modelId)) {
1970
+ if (params.endImage?.length) {
1971
+ throw new Error(`Gemini model ${modelId} does not accept a last frame`);
1972
+ }
1973
+ return this.runOmniVideo(modelId, [
1974
+ {
1975
+ type: "image",
1976
+ data: Buffer.from(image).toString("base64"),
1977
+ mime_type: detectImageMime(image)
1978
+ },
1979
+ { type: "text", text: prompt }
1980
+ ], params);
1981
+ }
903
1982
  if (!modelId.startsWith("veo-")) {
904
- throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support image-to-video.`);
1983
+ throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for image-to-video.`);
1984
+ }
1985
+ const instance = {
1986
+ prompt,
1987
+ image: veoImage(image)
1988
+ };
1989
+ const parameters = this.buildVideoParameters(params);
1990
+ if (params.endImage?.length) {
1991
+ // Veo interpolates between the two frames only in an 8-second clip.
1992
+ instance.lastFrame = veoImage(params.endImage);
1993
+ parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
1994
+ }
1995
+ const body = { instances: [instance] };
1996
+ if (Object.keys(parameters).length > 0) {
1997
+ body.parameters = parameters;
1998
+ }
1999
+ return this.runVeoOperation(modelId, body, "image-to-video", params);
2000
+ }
2001
+ // ---------------------------------------------------------------------------
2002
+ // Reference-to-video
2003
+ // ---------------------------------------------------------------------------
2004
+ async referenceToVideo(inputs, params) {
2005
+ if (!params.prompt) {
2006
+ throw new Error("The input prompt cannot be empty.");
2007
+ }
2008
+ const modelId = params.model.id;
2009
+ requireVideoTask(modelId, "reference_to_video");
2010
+ const images = inputs.images.filter((bytes) => bytes.length > 0);
2011
+ const videos = inputs.videos.filter((bytes) => bytes.length > 0);
2012
+ if ((inputs.audios ?? []).some((bytes) => bytes.length > 0)) {
2013
+ throw new Error(`Gemini model ${modelId} does not accept reference audio`);
2014
+ }
2015
+ if (images.length === 0 && videos.length === 0) {
2016
+ throw new Error("At least one reference image or video is required");
2017
+ }
2018
+ if (isOmniVideoModel(modelId)) {
2019
+ if (videos.length > OMNI_MAX_REFERENCE_VIDEOS) {
2020
+ throw new Error(`Gemini model ${modelId} accepts at most ${OMNI_MAX_REFERENCE_VIDEOS} reference videos`);
2021
+ }
2022
+ const input = images.map((bytes) => ({
2023
+ type: "image",
2024
+ data: Buffer.from(bytes).toString("base64"),
2025
+ mime_type: detectImageMime(bytes)
2026
+ }));
2027
+ for (const video of videos) {
2028
+ input.push(await this.omniVideoInput(video));
2029
+ }
2030
+ input.push({ type: "text", text: params.prompt });
2031
+ return this.runOmniVideo(modelId, input, params);
2032
+ }
2033
+ if (videos.length > 0) {
2034
+ throw new Error(`Gemini model ${modelId} accepts reference images only`);
2035
+ }
2036
+ if (images.length > VEO_MAX_REFERENCE_IMAGES) {
2037
+ throw new Error(`Gemini model ${modelId} accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images`);
905
2038
  }
906
- const imageBase64 = Buffer.from(image).toString("base64");
907
- const prompt = params.prompt ?? "Animate this image";
2039
+ const parameters = this.buildVideoParameters(params);
2040
+ parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
908
2041
  const body = {
909
2042
  instances: [
910
2043
  {
911
- prompt,
912
- image: { bytesBase64Encoded: imageBase64, mimeType: "image/png" }
2044
+ prompt: params.prompt,
2045
+ referenceImages: images.map((bytes) => ({
2046
+ image: veoImage(bytes),
2047
+ referenceType: "asset"
2048
+ }))
913
2049
  }
914
- ]
2050
+ ],
2051
+ parameters
915
2052
  };
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)
2053
+ return this.runVeoOperation(modelId, body, "reference-to-video", params);
2054
+ }
2055
+ // ---------------------------------------------------------------------------
2056
+ // Video extension (Veo) and editing (Omni)
2057
+ // ---------------------------------------------------------------------------
2058
+ async extendVideo(video, params) {
2059
+ if (video.length === 0) {
2060
+ throw new Error("The input video is empty.");
2061
+ }
2062
+ const modelId = params.model.id;
2063
+ requireVideoTask(modelId, "extend_video");
2064
+ if (params.mode !== "end") {
2065
+ throw new Error(`Gemini model ${modelId} extends videos at the end only`);
2066
+ }
2067
+ if (params.durationSeconds !== VEO_EXTENSION_SECONDS) {
2068
+ throw new Error(`Gemini model ${modelId} extends a video by exactly ${VEO_EXTENSION_SECONDS} seconds`);
2069
+ }
2070
+ const body = {
2071
+ instances: [
2072
+ {
2073
+ prompt: params.prompt,
2074
+ video: {
2075
+ inlineData: {
2076
+ mimeType: sniffVideoMime(video),
2077
+ data: Buffer.from(video).toString("base64")
2078
+ }
2079
+ }
2080
+ }
2081
+ ],
2082
+ // Veo extends 720p input only and returns the input plus the new seconds.
2083
+ parameters: { numberOfVideos: 1, resolution: "720p" }
2084
+ };
2085
+ return this.runVeoOperation(modelId, body, "video extension", {
2086
+ signal: params.signal
930
2087
  });
931
- if (!response.ok) {
932
- const errText = await response.text();
933
- throw new Error(`Gemini image-to-video failed ${response.status}: ${errText}`);
2088
+ }
2089
+ async videoToVideo(video, params) {
2090
+ if (video.length === 0) {
2091
+ throw new Error("The input video is empty.");
934
2092
  }
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());
2093
+ if (!params.prompt) {
2094
+ throw new Error("The edit instruction cannot be empty.");
953
2095
  }
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());
2096
+ const modelId = params.model.id;
2097
+ requireVideoTask(modelId, "video_to_video");
2098
+ const input = [await this.omniVideoInput(video)];
2099
+ for (const bytes of params.referenceImages ?? []) {
2100
+ if (bytes.length === 0)
2101
+ continue;
2102
+ input.push({
2103
+ type: "image",
2104
+ data: Buffer.from(bytes).toString("base64"),
2105
+ mime_type: detectImageMime(bytes)
2106
+ });
2107
+ }
2108
+ input.push({ type: "text", text: params.prompt });
2109
+ return this.runOmniVideo(modelId, input, params);
2110
+ }
2111
+ // ---------------------------------------------------------------------------
2112
+ // Text-to-music (Lyria, Interactions API)
2113
+ // ---------------------------------------------------------------------------
2114
+ async textToMusic(params) {
2115
+ if (!params.prompt) {
2116
+ throw new Error("The input prompt cannot be empty.");
2117
+ }
2118
+ const modelId = params.model.id;
2119
+ if (!modelId.startsWith("lyria-")) {
2120
+ throw new Error(`Model ${modelId} is not a Lyria model. Use a Lyria model for text-to-music.`);
2121
+ }
2122
+ // Lyria takes length and lyrics from the prompt; it has no fields for them.
2123
+ const input = [
2124
+ params.prompt,
2125
+ params.durationSeconds
2126
+ ? `Length: about ${Math.round(params.durationSeconds)} seconds.`
2127
+ : undefined,
2128
+ params.lyrics ? `Lyrics:\n${params.lyrics}` : undefined
2129
+ ]
2130
+ .filter((part) => part !== undefined)
2131
+ .join("\n\n");
2132
+ const body = { model: modelId, input };
2133
+ // Lyria answers MP3 by default. Only Lyria 3.5 returns WAV, on request.
2134
+ if (params.audioFormat === "wav" && modelId === "lyria-3.5") {
2135
+ body.response_format = { type: "audio" };
2136
+ }
2137
+ const signal = combineSignals(undefined, params.timeoutSeconds);
2138
+ const interaction = await this.createInteraction(body, "music generation", modelId, signal);
2139
+ const audio = await this.interactionMedia(interaction, "audio", signal);
2140
+ return {
2141
+ data: audio.data,
2142
+ mimeType: audio.mimeType ?? sniffAudioMime(audio.data)
2143
+ };
963
2144
  }
964
2145
  // ---------------------------------------------------------------------------
965
2146
  // Error detection
966
2147
  // ---------------------------------------------------------------------------
2148
+ isContextExceededError(error) {
2149
+ return geminiContextExceeded(error) !== null;
2150
+ }
967
2151
  isContextLengthError(error) {
968
2152
  const msg = String(error).toLowerCase();
969
2153
  return (msg.includes("context length") ||