ruby_llm 1.16.0 → 2.0.0.rc2

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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Gemini
6
+ # Models methods for the Gemini API integration
7
+ module Models
8
+ def list_models
9
+ models = []
10
+ page_token = nil
11
+
12
+ loop do
13
+ response = @connection.get(models_url) do |req|
14
+ req.params = { pageSize: 1000 }
15
+ req.params[:pageToken] = page_token if page_token
16
+ end
17
+
18
+ models.concat(parse_list_models_response(response, @provider.slug))
19
+ page_token = response.body['nextPageToken']
20
+ break unless page_token
21
+ end
22
+
23
+ models
24
+ end
25
+
26
+ private
27
+
28
+ def models_url
29
+ 'models'
30
+ end
31
+
32
+ def parse_list_models_response(response, slug)
33
+ Array(response.body['models']).map do |model_data|
34
+ model_id = model_data['name'].gsub('models/', '')
35
+ methods = Array(model_data['supportedGenerationMethods'])
36
+
37
+ Model.new(
38
+ id: model_id,
39
+ name: model_data['displayName'] || model_id,
40
+ provider: slug,
41
+ created_at: nil,
42
+ context_window: model_data['inputTokenLimit'],
43
+ max_output_tokens: model_data['outputTokenLimit'],
44
+ modalities: modalities_from(methods),
45
+ capabilities: capabilities_from(methods),
46
+ metadata: {
47
+ version: model_data['version'],
48
+ description: model_data['description'],
49
+ supported_generation_methods: methods
50
+ }
51
+ )
52
+ end
53
+ end
54
+
55
+ def modalities_from(methods)
56
+ return unless methods.include?('embedContent')
57
+
58
+ { input: ['text'], output: ['embeddings'] }
59
+ end
60
+
61
+ def capabilities_from(methods)
62
+ capabilities = []
63
+ capabilities << 'batch' if methods.intersect?(%w[batchGenerateContent asyncBatchEmbedContent])
64
+ capabilities << 'caching' if methods.include?('createCachedContent')
65
+ capabilities.push('streaming', 'realtime') if methods.include?('bidiGenerateContent')
66
+ capabilities
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Gemini
8
+ # Speech generation methods for the Gemini API implementation
9
+ module Speech
10
+ def speech_url(model:)
11
+ "models/#{model}:generateContent"
12
+ end
13
+
14
+ def render_speech_payload(input, model:, voice:, format:, provider_options: {})
15
+ RubyLLM.logger.debug { "Ignoring speech format #{format}. Gemini returns PCM audio." } if format
16
+
17
+ payload = {
18
+ contents: [
19
+ {
20
+ role: 'user',
21
+ parts: [
22
+ { text: input }
23
+ ]
24
+ }
25
+ ],
26
+ generationConfig: {
27
+ responseModalities: ['AUDIO'],
28
+ speechConfig: {
29
+ voiceConfig: {
30
+ prebuiltVoiceConfig: {
31
+ voiceName: voice || 'Kore'
32
+ }
33
+ }
34
+ }
35
+ },
36
+ model: model
37
+ }
38
+
39
+ Support::Utils.deep_merge(payload, provider_options)
40
+ end
41
+
42
+ def parse_speech_response(response, model:, voice:, format:) # rubocop:disable Lint/UnusedMethodArgument
43
+ audio = response.body.dig('candidates', 0, 'content', 'parts', 0, 'inlineData', 'data')
44
+ raise Error, 'Unexpected response format from Gemini speech generation API' unless audio
45
+
46
+ RubyLLM::Speech.new(
47
+ data: Base64.decode64(audio),
48
+ model: model,
49
+ voice: voice || 'Kore',
50
+ format: 'pcm'
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Gemini
8
+ # Streaming methods for the Gemini API implementation
9
+ module Streaming
10
+ def stream_url
11
+ "models/#{@model.id}:streamGenerateContent?alt=sse"
12
+ end
13
+
14
+ def build_chunk(data)
15
+ parts = data.dig('candidates', 0, 'content', 'parts') || []
16
+ track_stream_parts(data, parts)
17
+
18
+ Chunk.new(
19
+ role: :assistant,
20
+ model: data['modelVersion'],
21
+ content: extract_text_content(parts),
22
+ citations: extract_citations(data, nil),
23
+ thinking: Thinking.build(
24
+ text: extract_thought_parts(parts),
25
+ signature: extract_thought_signature(parts)
26
+ ),
27
+ input_tokens: input_tokens(data),
28
+ output_tokens: extract_output_tokens(data),
29
+ cache_read_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
30
+ thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
31
+ finish_reason: normalize_finish_reason(
32
+ data.dig('candidates', 0, 'finishReason') || data.dig('promptFeedback', 'blockReason')
33
+ ),
34
+ tool_calls: extract_tool_calls(data),
35
+ **stream_end_fields(data)
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ # Accumulates streamed parts so code-execution turns can be replayed
42
+ # verbatim, mirroring the non-streaming path.
43
+ def track_stream_parts(data, parts)
44
+ @stream_parts = (@stream_parts || []).concat(parts)
45
+ @saw_server_part = true if parts.any? { |part| server_tool_part?(part) }
46
+ metadata_calls = metadata_server_tool_calls(data)
47
+ @stream_metadata_calls = metadata_calls if metadata_calls.any?
48
+ end
49
+
50
+ def stream_end_fields(data)
51
+ return {} unless data.dig('candidates', 0, 'finishReason')
52
+
53
+ part_calls = @stream_parts.to_a.select { |part| server_tool_part?(part) }.map do |part|
54
+ ServerToolCall.new(
55
+ type: part.key?('executableCode') ? 'executable_code' : 'code_execution_result',
56
+ input: part['executableCode'],
57
+ result: part['codeExecutionResult'],
58
+ raw: part
59
+ )
60
+ end
61
+ calls = part_calls + @stream_metadata_calls.to_a
62
+ return {} if calls.empty?
63
+
64
+ {
65
+ server_tool_calls: calls,
66
+ raw_content: @saw_server_part ? @stream_parts : nil
67
+ }
68
+ end
69
+
70
+ def extract_text_content(parts)
71
+ text_parts = parts.reject { |p| p['thought'] }
72
+ text = text_parts.filter_map { |p| p['text'] }.join
73
+ text.empty? ? nil : text
74
+ end
75
+
76
+ def extract_output_tokens(data)
77
+ candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
78
+ thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
79
+ total = candidates + thoughts
80
+ total.positive? ? total : nil
81
+ end
82
+
83
+ def parse_streaming_error(data)
84
+ error_data = JSON.parse(data)
85
+ error = error_data.is_a?(Hash) ? error_data['error'] || error_data : error_data
86
+ return [nil, error.to_s] unless error.is_a?(Hash)
87
+
88
+ [error['code'], error['message']]
89
+ rescue JSON::ParserError => e
90
+ RubyLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
91
+ [500, "Failed to parse error: #{data}"]
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems/version'
4
+ require 'securerandom'
5
+
6
+ module RubyLLM
7
+ module Protocols
8
+ class Gemini
9
+ # Tools methods for the Gemini API implementation
10
+ module Tools
11
+ MULTIMODAL_FUNCTION_RESPONSE_GENERATION = Gem::Version.new('3')
12
+
13
+ def format_tools(tools)
14
+ return [] if tools.empty?
15
+
16
+ [{
17
+ functionDeclarations: tools.values.map { |tool| function_declaration_for(tool) }
18
+ }]
19
+ end
20
+
21
+ def format_tool_call(msg) # rubocop:disable Metrics/PerceivedComplexity
22
+ parts = []
23
+
24
+ parts.concat(Media.format_content(msg.content, msg.attachments)) if msg.content && !msg.content.empty?
25
+
26
+ fallback_signature = msg.thinking&.signature
27
+ used_fallback = false
28
+
29
+ msg.tool_calls.each_value do |tool_call|
30
+ part = {
31
+ functionCall: {
32
+ name: tool_call.name,
33
+ args: tool_call.arguments
34
+ }
35
+ }
36
+
37
+ signature = tool_call.thought_signature
38
+ if signature.nil? && fallback_signature && !used_fallback
39
+ signature = fallback_signature
40
+ used_fallback = true
41
+ end
42
+ part[:thoughtSignature] = signature if signature
43
+ parts << part
44
+ end
45
+
46
+ parts
47
+ end
48
+
49
+ def format_tool_result(msg, function_name = nil)
50
+ function_name ||= msg.tool_call_id
51
+ content = msg.content
52
+ content = nil if content && content.empty?
53
+ content = '(no output)' if content.nil? && msg.attachments.empty?
54
+
55
+ function_response = {
56
+ name: function_name,
57
+ response: {
58
+ name: function_name,
59
+ content: Media.format_content(content)
60
+ }
61
+ }
62
+
63
+ media_parts, sibling_parts = partition_tool_result_attachments(msg.attachments)
64
+ function_response[:parts] = media_parts if media_parts.any?
65
+
66
+ [{ functionResponse: function_response }, *sibling_parts]
67
+ end
68
+
69
+ def extract_tool_calls(data) # rubocop:disable Metrics/PerceivedComplexity
70
+ return nil unless data
71
+
72
+ candidate = data.is_a?(Hash) ? data.dig('candidates', 0) : nil
73
+ return nil unless candidate
74
+
75
+ parts = candidate.dig('content', 'parts')
76
+ return nil unless parts.is_a?(Array)
77
+
78
+ tool_calls = parts.each_with_object({}) do |part, result|
79
+ function_data = part['functionCall']
80
+ next unless function_data
81
+
82
+ id = SecureRandom.uuid
83
+ thought_signature = part['thoughtSignature'] || part['thought_signature']
84
+
85
+ result[id] = ToolCall.new(
86
+ id:,
87
+ name: function_data['name'],
88
+ arguments: function_data['args'] || {},
89
+ thought_signature: thought_signature
90
+ )
91
+ end
92
+
93
+ tool_calls.empty? ? nil : tool_calls
94
+ end
95
+
96
+ private
97
+
98
+ # functionResponse.parts only accepts inline bytes, and pre-Gemini 3 models reject it
99
+ def partition_tool_result_attachments(attachments)
100
+ return [[], []] if attachments.empty?
101
+
102
+ parts = attachments.map { |attachment| Media.format_content_attachment(attachment) }
103
+ return [[], parts] unless multimodal_function_responses_supported?(@model)
104
+
105
+ parts.partition { |part| part.key?(:inline_data) }
106
+ end
107
+
108
+ # Neither the model listing nor the registry distinguishes a Gemini
109
+ # generation, so the id is the only signal available. The -latest
110
+ # aliases always track the newest release.
111
+ def multimodal_function_responses_supported?(model)
112
+ id = model.respond_to?(:id) ? model.id.to_s : model.to_s
113
+ return false unless id.start_with?('gemini-')
114
+ return true if id.end_with?('-latest')
115
+
116
+ generation = id[/\Agemini-(\d+(?:\.\d+)?)(?:-|\z)/, 1]
117
+ generation ? Gem::Version.new(generation) >= MULTIMODAL_FUNCTION_RESPONSE_GENERATION : false
118
+ end
119
+
120
+ def function_declaration_for(tool)
121
+ parameters_schema = tool.parameters_schema ||
122
+ RubyLLM::Tool::SchemaDefinition.from_parameters(tool.declared_parameters)&.json_schema
123
+
124
+ declaration = {
125
+ name: tool.name,
126
+ description: tool.description
127
+ }
128
+
129
+ declaration[:parametersJsonSchema] = parameters_schema if parameters_schema
130
+
131
+ return declaration if tool.provider_options.empty?
132
+
133
+ RubyLLM::Support::Utils.deep_merge(declaration, tool.provider_options)
134
+ end
135
+
136
+ def build_tool_config(tool_choice)
137
+ {
138
+ functionCallingConfig: {
139
+ mode: forced_tool_choice?(tool_choice) ? 'any' : tool_choice
140
+ }.tap do |config|
141
+ # Use allowedFunctionNames to simulate specific tool choice
142
+ config[:allowedFunctionNames] = [tool_choice] if specific_tool_choice?(tool_choice)
143
+ end
144
+ }
145
+ end
146
+
147
+ def forced_tool_choice?(tool_choice)
148
+ tool_choice == :required || specific_tool_choice?(tool_choice)
149
+ end
150
+
151
+ def specific_tool_choice?(tool_choice)
152
+ !%i[auto none required].include?(tool_choice)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -1,17 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Providers
4
+ module Protocols
5
5
  class Gemini
6
6
  # Audio transcription helpers for the Gemini API implementation
7
7
  module Transcription
8
8
  DEFAULT_PROMPT = 'Transcribe the provided audio and respond with only the transcript text.'
9
9
 
10
- def transcribe(audio_file, model:, language:, **options)
11
- attachment = Attachment.new(audio_file)
12
- payload = render_transcription_payload(attachment, language:, **options)
13
- response = @connection.post(transcription_url(model), payload)
14
- parse_transcription_response(response, model:)
10
+ # rubocop:disable-next Lint/UnusedMethodArgument
11
+ def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
12
+ speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil)
13
+ raise_transcription_streaming_unsupported if block_given?
14
+
15
+ track_usage(:transcription) do
16
+ attachment = Attachment.new(audio_file, config: @config)
17
+ payload = render_transcription_payload(attachment, language:, format:, provider_options:, prompt:,
18
+ temperature:)
19
+ response = @connection.post(transcription_url(model), payload, usage: @usage_tracker)
20
+ parse_transcription_response(response, model:)
21
+ end
15
22
  end
16
23
 
17
24
  private
@@ -20,8 +27,9 @@ module RubyLLM
20
27
  "models/#{model}:generateContent"
21
28
  end
22
29
 
23
- def render_transcription_payload(attachment, language:, **options)
24
- prompt = build_prompt(options[:prompt], language)
30
+ def render_transcription_payload(attachment, language:, format: nil, provider_options: {}, prompt: nil,
31
+ temperature: nil)
32
+ prompt = build_prompt(prompt, language)
25
33
  audio_part = format_audio_part(attachment)
26
34
 
27
35
  raise UnsupportedAttachmentError, attachment.mime_type unless attachment.audio?
@@ -35,24 +43,16 @@ module RubyLLM
35
43
  audio_part
36
44
  ]
37
45
  }
38
- ]
46
+ ],
47
+ generationConfig: build_generation_config(format:, temperature:)
39
48
  }
40
49
 
41
- generation_config = build_generation_config(options)
42
- payload[:generationConfig] = generation_config unless generation_config.empty?
43
- payload[:safetySettings] = options[:safety_settings] if options[:safety_settings]
44
-
45
- payload
50
+ Support::Utils.deep_merge(payload, provider_options)
46
51
  end
47
52
 
48
- def build_generation_config(options)
49
- config = {}
50
- response_mime_type = options.fetch(:response_mime_type, 'text/plain')
51
-
52
- config[:responseMimeType] = response_mime_type if response_mime_type
53
- config[:temperature] = options[:temperature] if options.key?(:temperature)
54
- config[:maxOutputTokens] = options[:max_output_tokens] if options[:max_output_tokens]
55
-
53
+ def build_generation_config(format:, temperature:)
54
+ config = { responseMimeType: format || 'text/plain' }
55
+ config[:temperature] = temperature if temperature
56
56
  config
57
57
  end
58
58
 
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Gemini
6
+ # Veo video generation through the Gemini API's long-running predict
7
+ # operations. The finished video is a Files API URI that requires the
8
+ # API key to download.
9
+ module Videos
10
+ def video_url
11
+ "models/#{model_id(@model)}:predictLongRunning"
12
+ end
13
+
14
+ def render_video_payload(prompt, model:, with: [], provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
15
+ instance = { prompt: prompt }
16
+ instance[:image] = render_video_image(with.first) if with.first
17
+
18
+ Support::Utils.deep_merge({ instances: [instance] }, provider_options)
19
+ end
20
+
21
+ def parse_video_job(response, model:)
22
+ name = response.body['name']
23
+ raise Error.new('Gemini did not return a video generation operation', response:) unless name
24
+
25
+ VideoJob.new(id: name, protocol: self, model: model, raw: response.body)
26
+ end
27
+
28
+ def render_video_extension_payload(prompt, extend:, provider_options: {}, **)
29
+ video = render_video_extension(extend)
30
+ Support::Utils.deep_merge({ instances: [{ prompt: prompt, video: video }] }, provider_options)
31
+ end
32
+
33
+ def video_job_url(job)
34
+ job.id
35
+ end
36
+
37
+ def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
38
+ body = response.body
39
+ if body['error']
40
+ { status: :failed, raw: body, error: body.dig('error', 'message') }
41
+ elsif body['done']
42
+ video = generated_video(body)
43
+ video ? { status: :completed, raw: body } : filtered_video_failure(body)
44
+ else
45
+ { status: :pending, raw: body }
46
+ end
47
+ end
48
+
49
+ # The file URI answers with a redirect to the download host, and
50
+ # both hops require the API key.
51
+ def download_video(job)
52
+ video = generated_video(job.raw)
53
+ response = @connection.get video['uri']
54
+ response = @connection.get response.headers['location'] if response.status / 100 == 3
55
+
56
+ Video.new(
57
+ data: response.body,
58
+ mime_type: video['mimeType'] || 'video/mp4',
59
+ model: job.model,
60
+ raw: job.raw
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def render_video_extension(source)
67
+ if source.is_a?(Video) && source.raw && (video = generated_video(source.raw)) && video['uri']
68
+ return { uri: video['uri'] }
69
+ end
70
+
71
+ video = video_extension_attachment(source)
72
+ return { uri: video.source.to_s } if video.url?
73
+ return { uri: video.provider_file_uri } if video.provider_file?
74
+
75
+ raise ArgumentError, 'Gemini extends generated Veo videos; pass the returned Video or its URI'
76
+ end
77
+
78
+ def render_video_image(image)
79
+ { inlineData: { mimeType: image.mime_type, data: image.encoded } }
80
+ end
81
+
82
+ def validate_animate_inputs!(with:)
83
+ raise Error, 'Veo takes a single reference image' if with.size > 1
84
+
85
+ with.each do |attachment|
86
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
87
+ end
88
+ end
89
+
90
+ def generated_video(body)
91
+ body.dig('response', 'generateVideoResponse', 'generatedSamples', 0, 'video')
92
+ end
93
+
94
+ def filtered_video_failure(body)
95
+ reasons = body.dig('response', 'generateVideoResponse', 'raiMediaFilteredReasons')
96
+ error = Array(reasons).join(' ')
97
+ error = 'Gemini returned no video' if error.empty?
98
+ { status: :failed, raw: body, error: error }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The Google Gemini generateContent API.
6
+ class Gemini < Protocol
7
+ include Gemini::Caches
8
+ include Gemini::Chat
9
+ include Gemini::Embeddings
10
+ include Gemini::Images
11
+ include Gemini::Videos
12
+ include Gemini::Media
13
+ include Gemini::Models
14
+ include Gemini::Streaming
15
+ include Gemini::Tools
16
+ include Gemini::Speech
17
+ include Gemini::Transcription
18
+
19
+ # Gemini nests each tool's options inside its key, so aliases are
20
+ # lambdas placing the options there.
21
+ SERVER_TOOL_ALIASES = %i[
22
+ google_search url_context code_execution file_search google_maps
23
+ ].to_h do |tool_key|
24
+ [tool_key, ->(options) { { tool: { tool_key => Support::Utils.deep_symbolize_keys(options) } } }]
25
+ end.merge(
26
+ web_search: ->(options) { { tool: { google_search: Support::Utils.deep_symbolize_keys(options) } } },
27
+ web_fetch: ->(options) { { tool: { url_context: Support::Utils.deep_symbolize_keys(options) } } }
28
+ ).freeze
29
+
30
+ def server_tool_aliases
31
+ SERVER_TOOL_ALIASES
32
+ end
33
+ end
34
+ end
35
+ end