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,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+ require 'io/wait'
6
+
7
+ module RubyLLM
8
+ module Transport # :nodoc:
9
+ class WebsocketConnection # :nodoc: all
10
+ MAX_FRAME_BYTES = 16 * 1024 * 1024
11
+
12
+ attr_reader :url
13
+
14
+ def self.open(url, headers:, config:)
15
+ connection = new(url, headers:, config:)
16
+ connection.connect
17
+ yield connection
18
+ ensure
19
+ connection&.close
20
+ end
21
+
22
+ def initialize(url, headers:, config:)
23
+ @url = url
24
+ @uri = URI.parse(url)
25
+ raise ArgumentError, 'WebSocket URL must use ws or wss' unless %w[ws wss].include?(@uri.scheme)
26
+ if @uri.userinfo || @uri.fragment
27
+ raise ArgumentError, 'WebSocket URL must not contain credentials or a fragment'
28
+ end
29
+ raise ArgumentError, 'WebSocket connections do not support HTTP proxies' if config.http_proxy
30
+
31
+ @headers = headers
32
+ @timeout = config.request_timeout
33
+ @messages = []
34
+ @write_lock = Mutex.new
35
+ end
36
+
37
+ def connect
38
+ load_driver
39
+ deadline = monotonic_time + @timeout
40
+ open_socket(deadline)
41
+ configure_driver
42
+ @driver.start
43
+ read_frame(deadline) until @opened || @closed || @error
44
+ raise @error if @error
45
+ raise Error, 'WebSocket closed before opening' unless @opened
46
+
47
+ self
48
+ rescue StandardError
49
+ close
50
+ raise
51
+ end
52
+
53
+ def send_text(text)
54
+ ensure_open
55
+ @driver.text(text)
56
+ end
57
+
58
+ def send_binary(data)
59
+ ensure_open
60
+ @driver.binary(data.b)
61
+ end
62
+
63
+ def read(timeout: @timeout)
64
+ deadline = monotonic_time + timeout
65
+ read_frame(deadline) while @messages.empty? && !@closed && !@error
66
+ raise @error if @error
67
+
68
+ @messages.shift
69
+ rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
70
+ raise unless @closed
71
+ end
72
+
73
+ def read_available
74
+ ensure_open
75
+ until @messages.any? || @closed || @error
76
+ break unless read_available_frame
77
+ end
78
+ ensure_open
79
+
80
+ @messages.shift
81
+ rescue EOFError, Errno::ECONNRESET
82
+ @closed = true
83
+ raise Error, 'WebSocket connection ended without a close frame'
84
+ rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
85
+ raise unless @closed
86
+ end
87
+
88
+ def each_message(write:)
89
+ writer_error = nil
90
+ writer = Thread.new do
91
+ write.call(self)
92
+ rescue StandardError => e
93
+ writer_error = e
94
+ close
95
+ end
96
+ writer.report_on_exception = false
97
+
98
+ while (message = read)
99
+ yield message
100
+ end
101
+ finish_writer(writer)
102
+ raise writer_error if writer_error
103
+ rescue StandardError => e
104
+ raise writer_error || e
105
+ ensure
106
+ close
107
+ writer.kill.join if writer&.alive?
108
+ end
109
+
110
+ def close
111
+ @driver.close if @driver && @opened && !@closed
112
+ rescue IOError, SystemCallError, OpenSSL::SSL::SSLError, Error
113
+ nil
114
+ ensure
115
+ @closed = true
116
+ close_socket
117
+ end
118
+
119
+ def write(data)
120
+ @write_lock.synchronize do
121
+ deadline = monotonic_time + @timeout
122
+ offset = 0
123
+ while offset < data.bytesize
124
+ written = socket_operation(deadline) { @socket.write_nonblock(data.byteslice(offset..)) }
125
+ offset += written
126
+ end
127
+ end
128
+ data.bytesize
129
+ end
130
+
131
+ private
132
+
133
+ def read_available_frame
134
+ data = @socket.read_nonblock(16_384, exception: false)
135
+ return if %i[wait_readable wait_writable].include?(data)
136
+
137
+ raise EOFError unless data
138
+
139
+ @driver.parse(data)
140
+ data
141
+ end
142
+
143
+ def finish_writer(writer)
144
+ raise Error, 'WebSocket closed before outgoing messages finished' unless writer.join(1)
145
+ end
146
+
147
+ def configure_driver
148
+ @driver = WebSocket::Driver.client(self, max_length: MAX_FRAME_BYTES)
149
+ @headers.each { |name, value| @driver.set_header(name, value) if value }
150
+ @driver.on(:open) { @opened = true }
151
+ @driver.on(:message) { |event| @messages << event.data }
152
+ @driver.on(:error) { |event| @error = Error.new(event.message) }
153
+ @driver.on(:close) do |event|
154
+ @closed = true
155
+ @error ||= Error.new("WebSocket closed with code #{event.code}: #{event.reason}") unless event.code == 1000
156
+ end
157
+ end
158
+
159
+ def open_socket(deadline)
160
+ port = @uri.port || (@uri.scheme == 'wss' ? 443 : 80)
161
+ @socket = Socket.tcp(@uri.host, port, connect_timeout: @timeout)
162
+ secure_socket(deadline) if @uri.scheme == 'wss'
163
+ end
164
+
165
+ def close_socket
166
+ @socket.close if @socket && !@socket.closed?
167
+ end
168
+
169
+ def load_driver
170
+ require 'websocket/driver'
171
+ rescue LoadError
172
+ raise LoadError, 'WebSocket streaming requires websocket-driver. Add gem "websocket-driver" to your Gemfile.'
173
+ end
174
+
175
+ def secure_socket(deadline)
176
+ context = OpenSSL::SSL::SSLContext.new
177
+ context.set_params
178
+ socket = OpenSSL::SSL::SSLSocket.new(@socket, context)
179
+ socket.hostname = @uri.host
180
+ socket.sync_close = true
181
+ @socket = socket
182
+ socket_operation(deadline) { @socket.connect_nonblock }
183
+ @socket.post_connection_check(@uri.host)
184
+ end
185
+
186
+ def ensure_open
187
+ raise Error, 'WebSocket connection is closed' unless @opened && !@closed
188
+ raise @error if @error
189
+ end
190
+
191
+ def read_frame(deadline)
192
+ data = socket_operation(deadline) { @socket.read_nonblock(16_384) }
193
+ @driver.parse(data)
194
+ rescue EOFError, Errno::ECONNRESET
195
+ @closed = true
196
+ @error ||= Error.new('WebSocket connection ended without a close frame')
197
+ end
198
+
199
+ def socket_operation(deadline)
200
+ yield
201
+ rescue IO::WaitReadable
202
+ wait_for_socket(:wait_readable, deadline)
203
+ retry
204
+ rescue IO::WaitWritable
205
+ wait_for_socket(:wait_writable, deadline)
206
+ retry
207
+ end
208
+
209
+ def wait_for_socket(method, deadline)
210
+ remaining = deadline - monotonic_time
211
+ raise Error, 'WebSocket operation timed out' unless remaining.positive?
212
+ raise Error, 'WebSocket operation timed out' unless @socket.to_io.public_send(method, remaining)
213
+ end
214
+
215
+ def monotonic_time
216
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # An UploadedFile is the metadata record for a file stored with a provider
5
+ # through its Files API. Upload a file once with ::upload, then reuse its
6
+ # provider id or URI, for example as a chat attachment or in a batch.
7
+ #
8
+ # file = RubyLLM.upload("batch.jsonl", purpose: "batch")
9
+ # file.id # => "file_..."
10
+ # file.filename # => "batch.jsonl"
11
+ # file.byte_size # => 1234
12
+ #
13
+ # File ids are provider-owned. Persist #provider alongside #id and pass it
14
+ # back when finding or downloading the file later.
15
+ class UploadedFile
16
+ include Support::Inspectable
17
+
18
+ def inspect_attributes # :nodoc:
19
+ { id: id, provider: provider, filename: filename, byte_size: byte_size }
20
+ end
21
+
22
+ # The provider-assigned file identifier, such as <tt>"file_..."</tt>.
23
+ attr_reader :id
24
+
25
+ # The slug of the provider that stores the file.
26
+ attr_reader :provider
27
+
28
+ # The filename reported by the provider.
29
+ attr_reader :filename
30
+
31
+ # The file size in bytes.
32
+ attr_reader :byte_size
33
+
34
+ # The Time the provider stored the file.
35
+ attr_reader :created_at
36
+
37
+ # The Time the provider will delete the file, or +nil+ if it does not
38
+ # expire.
39
+ attr_reader :expires_at
40
+
41
+ # The provider-reported processing status of the file.
42
+ attr_reader :status
43
+
44
+ # The MIME type of the stored file.
45
+ attr_reader :mime_type
46
+
47
+ # The purpose the file was uploaded for, such as <tt>"batch"</tt>, when
48
+ # the provider tracks one.
49
+ attr_reader :purpose
50
+
51
+ # The provider URI for the file, such as a Gemini Files API URI or a
52
+ # <tt>gs://</tt> or <tt>s3://</tt> location for storage-backed providers.
53
+ attr_reader :uri
54
+
55
+ # Whether the provider allows downloading the file's content.
56
+ attr_reader :downloadable
57
+
58
+ # The raw provider response data for the file, as a Hash.
59
+ attr_reader :metadata
60
+
61
+ def initialize(id:, **attributes) # :nodoc:
62
+ @id = id
63
+ @provider = attributes[:provider]
64
+ @filename = attributes[:filename]
65
+ @byte_size = attributes[:byte_size]
66
+ @created_at = attributes[:created_at]
67
+ @expires_at = attributes[:expires_at]
68
+ @status = attributes[:status]
69
+ @mime_type = attributes[:mime_type]
70
+ @purpose = attributes[:purpose]
71
+ @uri = attributes[:uri]
72
+ @downloadable = attributes[:downloadable]
73
+ @metadata = attributes[:metadata] || {}
74
+ end
75
+
76
+ EXPIRY_MARGIN = 60 # :nodoc:
77
+
78
+ # Returns +true+ once the provider's retention window for this file has
79
+ # passed or is about to; a file expiring within the next minute cannot
80
+ # safely serve a request. Files without a reported expiry never expire.
81
+ def expired?
82
+ !expires_at.nil? && expires_at <= Time.now + EXPIRY_MARGIN
83
+ end
84
+
85
+ # Uploads +file+ to the provider's Files API and returns an UploadedFile.
86
+ # +file+ may be a path, an IO object, or an Attachment. When +provider:+
87
+ # is omitted, the provider of the configured default model is used.
88
+ # Also available as RubyLLM.upload.
89
+ #
90
+ # RubyLLM::UploadedFile.upload("document.pdf", provider: :anthropic)
91
+ # RubyLLM::UploadedFile.upload(io, provider: :openai, purpose: "batch",
92
+ # filename: "batch.jsonl")
93
+ #
94
+ # OpenAI and Azure require +purpose:+. Cohere uses +purpose:+ for its
95
+ # dataset type, such as <tt>"embed-input"</tt>.
96
+ # Pass +expires_in:+ as a number of
97
+ # seconds to have the provider delete the file automatically; OpenAI,
98
+ # xAI, and Mistral support it, and Mistral rounds up to whole hours.
99
+ # Storage-backed providers (Vertex AI and Bedrock) store the file at
100
+ # +uri:+ when given, otherwise at a generated object name under the
101
+ # configured bucket. +content_type:+ overrides the MIME type RubyLLM
102
+ # detects. +provider_options:+ carries the rest in the provider's own
103
+ # vocabulary: +visibility:+ on Mistral, +display_name:+ on Gemini.
104
+ def self.upload(file, provider: nil, context: nil, filename: nil, purpose: nil, expires_in: nil,
105
+ uri: nil, content_type: nil, provider_options: {})
106
+ options = { filename:, purpose:, expires_in:, uri:, content_type: }.compact
107
+ options[:provider_options] = provider_options unless provider_options.empty?
108
+ provider_for(provider, context).upload_file(file, **options)
109
+ end
110
+
111
+ # Fetches metadata for an existing provider file by +id+ and returns an
112
+ # UploadedFile. When +provider:+ is omitted, the provider of the
113
+ # configured default model is used.
114
+ #
115
+ # file = RubyLLM::UploadedFile.find("file_123")
116
+ #
117
+ def self.find(id, provider: nil, context: nil)
118
+ provider_for(provider, context).find_file(id)
119
+ end
120
+
121
+ # Downloads the provider file +id+ and returns a DownloadedFile.
122
+ # Also available as RubyLLM.download.
123
+ #
124
+ # RubyLLM.download(file.id, provider: :openai).save("report.pdf")
125
+ #
126
+ # Not every provider allows downloads; see #downloadable. Cohere
127
+ # downloads original uploaded bytes when preserved; generated datasets
128
+ # return JSONL and require the optional +avro+ gem.
129
+ def self.download(id, provider: nil, context: nil)
130
+ DownloadedFile.new(provider_for(provider, context).download_file(id))
131
+ end
132
+
133
+ def self.provider_for(provider, context)
134
+ config = context&.config || RubyLLM.config
135
+
136
+ if provider
137
+ Provider.resolve!(provider).new(config)
138
+ else
139
+ Models.resolve(config.default_model, config:).last
140
+ end
141
+ end
142
+ private_class_method :provider_for
143
+ end
144
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.16.0'
4
+ # The version of the ruby_llm gem, as a string.
5
+ VERSION = '2.0.0.rc2'
5
6
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A Video is a generated clip. Save it with #save or read the raw bytes
5
+ # with #to_blob. Both handle hosted URLs and inline data.
6
+ #
7
+ # video = RubyLLM.animate("a paper boat sailing down a rainy gutter")
8
+ # video.save("boat.mp4")
9
+ #
10
+ class Video
11
+ include Support::Inspectable
12
+
13
+ # The URL of the hosted video, for providers that return one, or +nil+.
14
+ attr_reader :url
15
+
16
+ # The raw video bytes, returned inline or downloaded with the
17
+ # provider's credentials, or +nil+ when the video is hosted at #url.
18
+ attr_reader :data
19
+
20
+ # The MIME type of the video, such as <tt>"video/mp4"</tt>.
21
+ attr_reader :mime_type
22
+
23
+ # The id of the model that generated the video.
24
+ attr_reader :model
25
+
26
+ # The clip length in seconds, when the provider reports one.
27
+ attr_reader :duration
28
+
29
+ # The provider's raw job response, for provider-specific fields such
30
+ # as reported cost.
31
+ attr_reader :raw
32
+
33
+ # Generates a video from +prompt+, blocks until the provider finishes
34
+ # rendering it, and returns a Video. Most code calls this through
35
+ # RubyLLM.animate. Video generation is asynchronous on every provider,
36
+ # so this submits a job with VideoJob.animate_later and polls until it
37
+ # is done, honoring <tt>config.video_generation_timeout</tt> and
38
+ # <tt>config.video_generation_poll_interval</tt>.
39
+ #
40
+ # +model:+ selects the video model and defaults to the configured
41
+ # +default_video_model+. +provider:+ forces a specific provider, and
42
+ # +assume_model_exists:+ skips the registry lookup. +with:+ passes a
43
+ # reference image or a video to edit on models that support it. Models
44
+ # driven by image and audio input can omit the prompt and pass both
45
+ # attachments through +with:+.
46
+ # +extend:+ continues a source video instead. It accepts a Video,
47
+ # file path, URL, or Attachment and cannot be combined with +with:+.
48
+ # +provider_options:+ takes options in the provider's request
49
+ # vocabulary, such as durations and resolutions, and merges them into
50
+ # the request as-is. +context:+ supplies a Context whose configuration
51
+ # replaces the global one. +metadata:+ is included in the
52
+ # instrumentation payload.
53
+ #
54
+ # video = RubyLLM.animate("a hummingbird in slow motion", model: "veo-3.1-fast-generate-preview")
55
+ #
56
+ # RubyLLM.animate(
57
+ # "Make the waterfall crash down",
58
+ # model: "grok-imagine-video-1.5",
59
+ # with: "waterfall.png",
60
+ # provider_options: { duration: 5 }
61
+ # )
62
+ #
63
+ def self.animate(prompt = nil,
64
+ model: nil,
65
+ provider: nil,
66
+ assume_model_exists: false,
67
+ context: nil,
68
+ with: nil,
69
+ extend: nil,
70
+ provider_options: {},
71
+ metadata: nil)
72
+ config = context&.config || RubyLLM.config
73
+ payload = { model:, prompt:, provider_options:, metadata: }
74
+
75
+ RubyLLM.instrument('video.ruby_llm', payload, config: config) do |event|
76
+ job = VideoJob.animate_later(prompt, model:, provider:, assume_model_exists:,
77
+ context:, with:, extend:, provider_options:, metadata:)
78
+ event[:model] = job.model
79
+ event[:job_id] = job.id
80
+ job.wait
81
+ result = job.video
82
+ event[:result] = result
83
+ event[:response_model] = result.model
84
+ result
85
+ end
86
+ end
87
+
88
+ # Set by the protocol that generated the video, so a Context's
89
+ # connection settings reach #to_blob.
90
+ attr_writer :config # :nodoc:
91
+
92
+ def config # :nodoc:
93
+ @config || RubyLLM.config
94
+ end
95
+
96
+ def initialize(url: nil, data: nil, mime_type: nil, model: nil, duration: nil, raw: nil) # :nodoc:
97
+ @url = url
98
+ @data = data
99
+ @mime_type = mime_type
100
+ @model = model
101
+ @duration = duration
102
+ @raw = raw
103
+ end
104
+
105
+ # Returns the raw binary video bytes, downloading from #url when the
106
+ # provider returned a hosted video.
107
+ #
108
+ # video_bytes = video.to_blob
109
+ #
110
+ def to_blob
111
+ data || Transport::Connection.basic(config).get(url).body
112
+ end
113
+
114
+ # Writes the binary video to +path+, expanding it first. Returns
115
+ # +path+ as given.
116
+ #
117
+ # video.save("clip.mp4")
118
+ #
119
+ def save(path)
120
+ File.binwrite(File.expand_path(path), to_blob)
121
+ path
122
+ end
123
+
124
+ private
125
+
126
+ def inspect_attributes # :nodoc:
127
+ {
128
+ model: model,
129
+ mime_type: mime_type,
130
+ url: url,
131
+ data: data && "#{data.bytesize} bytes",
132
+ duration: duration
133
+ }
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A VideoJob is an in-flight video generation. RubyLLM.animate_later
5
+ # returns one immediately; poll it with #refresh and read the finished
6
+ # clip with #video. RubyLLM.animate runs the same job through #wait.
7
+ #
8
+ # job = RubyLLM.animate_later("a paper boat sailing down a gutter")
9
+ # job.wait
10
+ # job.video.save("boat.mp4")
11
+ #
12
+ class VideoJob
13
+ include Support::Inspectable
14
+
15
+ # The provider's id for the job.
16
+ attr_reader :id
17
+
18
+ # The job state: +:pending+, +:completed+, or +:failed+.
19
+ attr_reader :status
20
+
21
+ # The id of the model rendering the video.
22
+ attr_reader :model
23
+
24
+ # The provider's failure message when #failed?, or +nil+.
25
+ attr_reader :error
26
+
27
+ # The provider's raw response from the last submit or poll.
28
+ attr_reader :raw
29
+
30
+ # Submits a video generation job and returns a VideoJob without
31
+ # waiting for the result. Most code calls this through
32
+ # RubyLLM.animate_later. Takes the same arguments as Video.animate.
33
+ #
34
+ # job = RubyLLM.animate_later("a hummingbird in slow motion")
35
+ # job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
36
+ # job.done? # => false
37
+ #
38
+ def self.animate_later(prompt = nil,
39
+ model: nil,
40
+ provider: nil,
41
+ assume_model_exists: false,
42
+ context: nil,
43
+ with: nil,
44
+ extend: nil,
45
+ provider_options: {},
46
+ metadata: nil)
47
+ config = context&.config || RubyLLM.config
48
+ model ||= config.default_video_model
49
+ model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
50
+ config: config)
51
+ payload = {
52
+ provider: provider_instance.slug,
53
+ provider_class: provider_instance.class.display_name,
54
+ model: model.id,
55
+ model_info: model,
56
+ prompt: prompt,
57
+ provider_options: provider_options,
58
+ metadata: metadata
59
+ }
60
+
61
+ RubyLLM.instrument('video_job.ruby_llm', payload, config: config) do |event|
62
+ job = provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
63
+ event[:job_id] = job.id
64
+ job
65
+ end
66
+ end
67
+
68
+ def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil) # :nodoc:
69
+ @id = id
70
+ @protocol = protocol
71
+ @model = model
72
+ @status = status
73
+ @raw = raw
74
+ @error = error
75
+ end
76
+
77
+ # Returns +true+ while the provider is still rendering the video.
78
+ def pending?
79
+ status == :pending
80
+ end
81
+
82
+ # Returns +true+ once the job finished, successfully or not.
83
+ def done?
84
+ !pending?
85
+ end
86
+
87
+ # Returns +true+ when the job finished with a video.
88
+ def completed?
89
+ status == :completed
90
+ end
91
+
92
+ # Returns +true+ when the job finished without a video. #error carries
93
+ # the provider's failure message.
94
+ def failed?
95
+ status == :failed
96
+ end
97
+
98
+ # Re-fetches the job from the provider, updating #status, #error, and
99
+ # #raw. Does nothing once the job is #done?. Returns self.
100
+ def refresh
101
+ return self if done?
102
+
103
+ state = @protocol.refresh_video_job(self)
104
+ @status = state.fetch(:status)
105
+ @raw = state[:raw]
106
+ @error = state[:error]
107
+ self
108
+ end
109
+
110
+ # Polls the job until it finishes, then returns self. Raises Error
111
+ # when the job fails or +timeout+ elapses first. +timeout+ and
112
+ # +interval+ are seconds and default to the configured
113
+ # +video_generation_timeout+ and +video_generation_poll_interval+.
114
+ def wait(timeout: nil, interval: nil)
115
+ timeout ||= @protocol.config.video_generation_timeout
116
+ interval ||= @protocol.config.video_generation_poll_interval
117
+ deadline = monotonic_time + timeout
118
+
119
+ until done?
120
+ raise Error, "Video generation timed out after #{timeout} seconds" if monotonic_time > deadline
121
+
122
+ sleep interval
123
+ refresh
124
+ end
125
+ raise Error, "Video generation failed: #{error}" if failed?
126
+
127
+ self
128
+ end
129
+
130
+ # Returns the finished Video, downloading it from the provider when
131
+ # its API requires that. Returns +nil+ while #pending? and raises
132
+ # Error when #failed?.
133
+ def video
134
+ raise Error, "Video generation failed: #{error}" if failed?
135
+ return unless completed?
136
+
137
+ @video ||= @protocol.download_video(self).tap { |video| video.config = @protocol.config }
138
+ end
139
+
140
+ private
141
+
142
+ def monotonic_time
143
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
144
+ end
145
+
146
+ def inspect_attributes # :nodoc:
147
+ { id: id, status: status, model: model }
148
+ end
149
+ end
150
+ end