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
@@ -1,56 +1,169 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Identify potentially harmful content in text.
5
- # https://platform.openai.com/docs/guides/moderation
4
+ # A Moderation holds the result of screening text or images for
5
+ # potentially harmful content. Most code obtains one through
6
+ # RubyLLM.moderate.
7
+ #
8
+ # result = RubyLLM.moderate("This is a safe message about Ruby programming")
9
+ # result.flagged? # => false
10
+ #
11
+ # RubyLLM.moderate(with: "profile.png").flagged?
12
+ #
6
13
  class Moderation
7
- attr_reader :id, :model, :results
14
+ include Support::Inspectable
15
+ include Accounting::Usage::Result
8
16
 
9
- def initialize(id:, model:, results:)
17
+ # A Result is the verdict for a single moderated input. Providers return
18
+ # results in different shapes. RubyLLM normalizes all of them into Result
19
+ # objects on Moderation#results.
20
+ #
21
+ # result = RubyLLM.moderate("Some user input").results.first
22
+ # result.flagged? # => false
23
+ # result.categories # => []
24
+ # result.category_scores["violence"] # => 0.0004
25
+ #
26
+ class Result
27
+ # The names of the categories flagged for this input, as an array of
28
+ # strings, empty when nothing was flagged.
29
+ attr_reader :categories
30
+
31
+ # The confidence scores for this input, as a hash of category name to
32
+ # a score between 0.0 and 1.0. Empty when the provider reports none.
33
+ attr_reader :category_scores
34
+
35
+ def initialize(flagged:, categories:, category_scores:) # :nodoc:
36
+ @flagged = flagged
37
+ @categories = categories
38
+ @category_scores = category_scores
39
+ end
40
+
41
+ def self.from_h(data) # :nodoc:
42
+ flagged_names = (data['categories'] || {}).select { |_category, flagged| flagged }.keys
43
+ new(
44
+ flagged: data.fetch('flagged', flagged_names.any?),
45
+ categories: flagged_names,
46
+ category_scores: data['category_scores'] || {}
47
+ )
48
+ end
49
+
50
+ # Returns +true+ if this input was flagged as potentially harmful,
51
+ # +false+ otherwise.
52
+ def flagged?
53
+ @flagged
54
+ end
55
+ end
56
+
57
+ # The provider-assigned identifier of the moderation request.
58
+ attr_reader :id
59
+
60
+ # The id of the model that performed the moderation, or +nil+ for an
61
+ # operation that does not select a model.
62
+ attr_reader :model
63
+
64
+ # The per-input verdicts, as an array of Result objects, one per
65
+ # moderated input.
66
+ attr_reader :results
67
+
68
+ # The original provider response, or an array of responses when each
69
+ # input requires a separate request.
70
+ attr_reader :raw
71
+
72
+ def initialize(id:, model:, results:, raw: nil) # :nodoc:
10
73
  @id = id
11
74
  @model = model
12
75
  @results = results
76
+ @raw = raw
13
77
  end
14
78
 
15
- def self.moderate(input,
79
+ # Screens +input+ and optional image attachments and returns a Moderation with the
80
+ # provider's verdict. Uses the configured default moderation model when
81
+ # +model+ is not given. Pass +provider:+ and <tt>assume_model_exists: true</tt>
82
+ # to use a model that is not in the registry. An explicitly selected
83
+ # provider may instead use a configured resource without a model.
84
+ #
85
+ # RubyLLM.moderate("User message")
86
+ # RubyLLM.moderate(["First comment", "Second comment"]).results
87
+ # RubyLLM.moderate("Caption", with: "screenshot.png")
88
+ #
89
+ def self.moderate(input = nil,
16
90
  model: nil,
91
+ with: nil,
17
92
  provider: nil,
18
93
  assume_model_exists: false,
19
- context: nil)
94
+ context: nil,
95
+ provider_options: {},
96
+ metadata: nil)
97
+ attachments = Attachment.wrap(with)
98
+ raise ArgumentError, 'must provide input text, image attachment, or both' if input.nil? && attachments.empty?
99
+
20
100
  config = context&.config || RubyLLM.config
21
- model ||= config.default_moderation_model || 'omni-moderation-latest'
22
- model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
23
- config: config)
24
- model_id = model.id
101
+ model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
102
+ config: config, operation: :moderate,
103
+ default_model: config.default_moderation_model)
104
+ empty_tokens = Tokens.new
105
+ payload = {
106
+ provider: provider_instance.slug,
107
+ provider_class: provider_instance.class.display_name,
108
+ model: model&.id,
109
+ model_info: model,
110
+ input: input,
111
+ attachment_count: attachments.size,
112
+ provider_options: provider_options,
113
+ metadata: metadata,
114
+ tokens: empty_tokens,
115
+ cost: Cost.new(tokens: empty_tokens, model:)
116
+ }
25
117
 
26
- provider_instance.moderate(input, model: model_id)
118
+ RubyLLM.instrument('moderation.ruby_llm', payload, config: config) do |event|
119
+ result = provider_instance.moderate(input, model:, with: attachments, provider_options:)
120
+ event[:result] = result
121
+ event[:flagged] = result.flagged?
122
+ event[:tokens] = result.tokens
123
+ event[:cost] = result.cost
124
+ result
125
+ end
27
126
  end
28
127
 
29
- # Convenience method to get content from moderation result
30
- def content
31
- results
128
+ # Returns +true+ if any input was flagged as potentially harmful,
129
+ # +false+ otherwise.
130
+ def flagged?
131
+ results.any?(&:flagged?)
32
132
  end
33
133
 
34
- # Check if any content was flagged
35
- def flagged?
36
- results.any? { |result| result['flagged'] }
134
+ # Returns provider-reported usage across every attempt. Its fields are
135
+ # +nil+ when the provider did not report any.
136
+ def tokens
137
+ ruby_llm_usage_tokens
138
+ end
139
+
140
+ # Returns the moderation cost across every provider attempt.
141
+ def cost
142
+ ruby_llm_usage_cost
37
143
  end
38
144
 
39
- # Get all flagged categories across all results
145
+ # Returns the unique names of the categories flagged across all results.
146
+ #
147
+ # result.flagged_categories # => ["harassment", "violence"]
148
+ #
40
149
  def flagged_categories
41
- results.flat_map do |result|
42
- result['categories']&.select { |_category, flagged| flagged }&.keys || []
43
- end.uniq
150
+ results.flat_map(&:categories).uniq
44
151
  end
45
152
 
46
- # Get category scores for the first result (most common case)
153
+ # Returns the confidence scores across all results, as a hash of category
154
+ # name to a score between 0.0 and 1.0. Keeps the highest score per
155
+ # category when there are multiple results.
156
+ #
157
+ # result.category_scores["violence"] # => 0.0001
158
+ #
47
159
  def category_scores
48
- results.first&.dig('category_scores') || {}
160
+ results.map(&:category_scores).reduce({}) do |merged, scores|
161
+ merged.merge(scores) { |_category, left, right| [left, right].max }
162
+ end
49
163
  end
50
164
 
51
- # Get categories for the first result (most common case)
52
- def categories
53
- results.first&.dig('categories') || {}
165
+ def inspect_attributes # :nodoc:
166
+ { id: id, model: model, flagged: flagged? }
54
167
  end
55
168
  end
56
169
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # An OCR is the text a document AI model extracted from a file.
5
+ # RubyLLM.ocr returns one. Each page carries the extracted markdown along
6
+ # with any images and tables the provider reports.
7
+ #
8
+ # ocr = RubyLLM.ocr("contract.pdf")
9
+ # ocr.markdown # => "# Contract\n\n..."
10
+ # ocr.pages.first.markdown # => "# Contract\n..."
11
+ #
12
+ class OCR
13
+ include Support::Inspectable
14
+ include Accounting::Usage::Result
15
+
16
+ # One page of an OCR result: the zero-based page +index+, the extracted
17
+ # +markdown+, and the +images+ and +tables+ the provider reports for the
18
+ # page, or +nil+ when it reports none. +raw+ holds the provider's
19
+ # unmodified page hash, including any fields beyond these.
20
+ Page = Struct.new(:index, :markdown, :images, :tables, :raw, keyword_init: true)
21
+
22
+ # The id of the model that performed the OCR.
23
+ attr_reader :model
24
+
25
+ # The provider's usage block for the request, such as the number of
26
+ # pages processed, or +nil+ when the provider does not report one.
27
+ attr_reader :usage # :nodoc:
28
+
29
+ # The provider's raw response hash.
30
+ attr_reader :raw
31
+
32
+ # Extracts the text of +file+ and returns an OCR result. Most code calls
33
+ # this through RubyLLM.ocr. The file may be a path, URL, IO object, or
34
+ # Attachment; accepted document and image formats depend on the provider.
35
+ #
36
+ # +model:+ selects the OCR model and defaults to the configured
37
+ # +default_ocr_model+. +provider:+ forces a specific provider, and
38
+ # +assume_model_exists:+ skips the registry lookup. +context:+ supplies
39
+ # a Context whose configuration replaces the global one. +metadata:+ is
40
+ # included in the instrumentation payload. +pages:+ limits the read to
41
+ # the given zero-based page indexes. +provider_options:+ merges options
42
+ # into the request in the provider's own vocabulary, such as Mistral's
43
+ # +include_image_base64:+ or +table_format:+.
44
+ #
45
+ # RubyLLM.ocr("report.pdf")
46
+ # RubyLLM.ocr("https://example.com/scan.png")
47
+ # RubyLLM.ocr("report.pdf", pages: [0, 1], provider_options: { table_format: "html" })
48
+ #
49
+ # Raises RubyLLM::ModelNotFoundError if +model:+ is not in the registry,
50
+ # and RubyLLM::Error when the provider has no OCR support.
51
+ def self.ocr(file,
52
+ model: nil,
53
+ provider: nil,
54
+ assume_model_exists: false,
55
+ context: nil,
56
+ pages: nil,
57
+ provider_options: {},
58
+ metadata: nil)
59
+ config = context&.config || RubyLLM.config
60
+ model ||= config.default_ocr_model
61
+ model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
62
+ config: config)
63
+ payload = {
64
+ provider: provider_instance.slug,
65
+ provider_class: provider_instance.class.display_name,
66
+ model: model.id,
67
+ model_info: model,
68
+ pages: pages,
69
+ provider_options: provider_options,
70
+ metadata: metadata
71
+ }
72
+
73
+ RubyLLM.instrument('ocr.ruby_llm', payload, config: config) do |event|
74
+ result = provider_instance.ocr(file, model:, pages:, provider_options:)
75
+ event[:result] = result
76
+ event[:response_model] = result.model
77
+ result
78
+ end
79
+ end
80
+
81
+ def initialize(pages:, model:, usage: nil, raw: nil) # :nodoc:
82
+ @page_hashes = Array(pages)
83
+ @model = model
84
+ @usage = usage
85
+ @raw = raw
86
+ end
87
+
88
+ # Returns the pages of the document as an array of Page structs.
89
+ def pages
90
+ @pages ||= @page_hashes.map.with_index do |page, position|
91
+ next page if page.is_a?(Page)
92
+
93
+ Page.new(
94
+ index: page['index'] || position,
95
+ markdown: page['markdown'],
96
+ images: page['images'],
97
+ tables: page['tables'],
98
+ raw: page
99
+ )
100
+ end
101
+ end
102
+
103
+ # Returns the markdown of every page, joined with blank lines.
104
+ def markdown
105
+ pages.filter_map(&:markdown).join("\n\n")
106
+ end
107
+
108
+ def inspect_attributes # :nodoc:
109
+ { model: model, pages: pages.length }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'pathname'
5
+
6
+ module RubyLLM
7
+ # Resolves prompt templates from the application and registered engine directories.
8
+ class Prompt
9
+ # The ordered directories searched for prompt files. The application's
10
+ # prompt directory is always first, so an application can override any
11
+ # prompt an engine ships by placing a file at the same relative path.
12
+ # Engines append their own directory in an initializer:
13
+ #
14
+ # initializer "my_engine.prompts" do
15
+ # RubyLLM::Prompt.roots << MyEngine::Engine.root.join("app/prompts")
16
+ # end
17
+ #
18
+ class Roots
19
+ include Enumerable
20
+
21
+ def initialize(&default) # :nodoc:
22
+ @default = default
23
+ @registered = []
24
+ end
25
+
26
+ # Appends a prompt directory and returns +self+.
27
+ def <<(path)
28
+ @registered << Pathname.new(path)
29
+ self
30
+ end
31
+ alias push <<
32
+
33
+ # Yields each prompt directory in lookup order. Returns an Enumerator without a block.
34
+ def each(&)
35
+ [@default.call, *@registered].each(&)
36
+ end
37
+ end
38
+
39
+ attr_reader :name # :nodoc:
40
+
41
+ def initialize(name) # :nodoc:
42
+ @name = name.to_s
43
+ @filename = @name.end_with?('.txt.erb') ? @name : "#{@name}.txt.erb"
44
+ end
45
+
46
+ def path # :nodoc:
47
+ @path ||= candidates.find { |candidate| File.exist?(candidate) } || candidates.first
48
+ end
49
+
50
+ def render(**locals) # :nodoc:
51
+ raise PromptNotFoundError, "Prompt file not found: #{path}" unless File.exist?(path)
52
+
53
+ ERB.new(File.read(path)).result_with_hash(locals)
54
+ end
55
+
56
+ def self.render(name, **locals) # :nodoc:
57
+ new(name).render(**locals)
58
+ end
59
+
60
+ # Returns the ordered prompt directories. Append engine directories with +<<+.
61
+ def self.roots
62
+ @roots ||= Roots.new { root }
63
+ end
64
+
65
+ def self.root # :nodoc:
66
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
67
+ Rails.root.join('app/prompts')
68
+ else
69
+ Pathname.new(Dir.pwd).join('app/prompts')
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def candidates
76
+ self.class.roots.map { |root| Pathname.new(root).join(@filename) }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Protocol
5
+ module BinaryStreaming # :nodoc: all
6
+ def stream_binary(url, payload, &)
7
+ progress = {}
8
+ fallback = Streaming::StreamState.new
9
+ on_data = binary_on_data(fallback, progress, &)
10
+
11
+ response = @connection.post(url, payload, usage: @usage_tracker) do |request|
12
+ (request.options.context ||= {})[Transport::Connection::STREAM_PROGRESS_KEY] = progress
13
+ if faraday_1?
14
+ request.options[:on_data] = on_data
15
+ else
16
+ request.options.on_data = on_data
17
+ end
18
+ end
19
+ state = response.env[:streaming_state] || fallback
20
+ validate_binary_response(response, state.buffer)
21
+ yield state.buffer.dup unless progress[:started] || state.buffer.empty?
22
+ response.env.body = state.buffer.b
23
+ response
24
+ end
25
+
26
+ private
27
+
28
+ def binary_on_data(fallback, progress)
29
+ lambda do |chunk, bytes, env = nil|
30
+ next if chunk.empty?
31
+
32
+ state = stream_state(env, fallback)
33
+ state.buffer.clear if env.nil? && bytes && bytes <= state.buffer.bytesize
34
+ if failed_http_status(env)
35
+ handle_failed_response(chunk, state.buffer, env)
36
+ else
37
+ state.buffer << chunk.b
38
+ next unless binary_response?(env)
39
+
40
+ progress[:started] = true
41
+ yield chunk.b
42
+ end
43
+ end
44
+ end
45
+
46
+ def binary_response?(env)
47
+ return false unless env&.status&.between?(200, 299)
48
+
49
+ content_type = env.response_headers&.fetch('content-type', '').to_s.split(';').first
50
+ content_type&.start_with?('audio/') || content_type == 'application/octet-stream'
51
+ end
52
+
53
+ def validate_binary_response(response, data)
54
+ return if binary_response?(response.env)
55
+
56
+ begin
57
+ raise_stream_error(data, JSON.parse(data), response.env)
58
+ rescue JSON::ParserError
59
+ nil
60
+ end
61
+ raise Error.new('Expected an audio response from the speech endpoint', response: response)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ module RubyLLM
7
+ class Protocol
8
+ class StreamAccumulator # :nodoc:
9
+ attr_reader :content, :model, :tool_calls
10
+
11
+ def initialize
12
+ @content = +''
13
+ @citations = []
14
+ @thinking_text = nil
15
+ @thinking_signature = nil
16
+ @tool_calls = {}
17
+ @input_tokens = nil
18
+ @output_tokens = nil
19
+ @cache_read_tokens = nil
20
+ @cache_write_tokens = nil
21
+ @thinking_tokens = nil
22
+ @server_tool_use = nil
23
+ @reported_cost = nil
24
+ @server_tool_calls = []
25
+ @raw_content = nil
26
+ @raw_reasoning = nil
27
+ @finish_reason = nil
28
+ @latest_tool_call_id = nil
29
+ @tool_call_ids_by_index = {}
30
+ end
31
+
32
+ def add(chunk)
33
+ RubyLLM.logger.debug { chunk.inspect } if RubyLLM.config.log_stream_debug
34
+ @model = chunk.model if @model.to_s.empty?
35
+
36
+ handle_chunk_content(chunk)
37
+ accumulate_citations(chunk.citations)
38
+ append_thinking_from_chunk(chunk)
39
+ accumulate_server_tool_calls(chunk.server_tool_calls)
40
+ @raw_content = chunk.raw_content if chunk.raw_content
41
+ @raw_reasoning = chunk.raw_reasoning if chunk.raw_reasoning
42
+ @finish_reason = chunk.finish_reason if chunk.finish_reason
43
+ count_tokens chunk
44
+ RubyLLM.logger.debug { inspect } if RubyLLM.config.log_stream_debug
45
+ end
46
+
47
+ def to_message(response)
48
+ Message.new(
49
+ role: :assistant,
50
+ content: content.empty? ? nil : content,
51
+ citations: resolved_citations,
52
+ thinking: Thinking.build(
53
+ text: @thinking_text,
54
+ signature: @thinking_signature
55
+ ),
56
+ tokens: Tokens.new(
57
+ input: @input_tokens,
58
+ output: @output_tokens,
59
+ cache_read: @cache_read_tokens,
60
+ cache_write: @cache_write_tokens,
61
+ thinking: @thinking_tokens,
62
+ server_tool_use: @server_tool_use,
63
+ reported_cost: @reported_cost
64
+ ),
65
+ server_tool_calls: @server_tool_calls,
66
+ raw_content: @raw_content,
67
+ raw_reasoning: @raw_reasoning,
68
+ finish_reason: @finish_reason,
69
+ model: model,
70
+ tool_calls: tool_calls_from_stream(response),
71
+ raw: response
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ # Providers like Perplexity repeat the full citation list on every chunk.
78
+ def accumulate_citations(new_citations)
79
+ new_citations.each do |citation|
80
+ @citations << citation unless @citations.include?(citation)
81
+ end
82
+ end
83
+
84
+ def accumulate_server_tool_calls(calls)
85
+ calls.each do |call|
86
+ index = @server_tool_calls.index do |existing|
87
+ call.id && existing.id == call.id && existing.type == call.type
88
+ end
89
+ if index
90
+ @server_tool_calls[index] = call
91
+ else
92
+ @server_tool_calls << call
93
+ end
94
+ end
95
+ end
96
+
97
+ def resolved_citations
98
+ @citations.map { |citation| resolve_citation_text(citation) }
99
+ end
100
+
101
+ def resolve_citation_text(citation)
102
+ return citation if citation.text || citation.start_index.nil? || citation.end_index.nil?
103
+
104
+ span = content[citation.start_index...citation.end_index]
105
+ span ? Citation.new(citation.to_h.merge(text: span)) : citation
106
+ end
107
+
108
+ def tool_calls_from_stream(response)
109
+ tool_calls.transform_values do |tc|
110
+ arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
111
+ parse_tool_call_arguments(tc.arguments, response)
112
+ elsif tc.arguments.is_a?(String)
113
+ {}
114
+ else
115
+ tc.arguments
116
+ end
117
+
118
+ ToolCall.new(
119
+ id: tc.id,
120
+ name: tc.name,
121
+ arguments: arguments,
122
+ thought_signature: tc.thought_signature,
123
+ remote: tc.remote?
124
+ )
125
+ end
126
+ end
127
+
128
+ def parse_tool_call_arguments(arguments, response)
129
+ JSON.parse(arguments)
130
+ rescue JSON::ParserError => e
131
+ raise ToolCallParseError.new(response: response, finish_reason: @finish_reason), cause: e
132
+ end
133
+
134
+ def accumulate_tool_calls(new_tool_calls)
135
+ RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
136
+ new_tool_calls.each do |stream_key, tool_call|
137
+ if tool_call.id
138
+ start_tool_call(stream_key, tool_call)
139
+ else
140
+ append_tool_call_fragment(stream_key, tool_call)
141
+ end
142
+ end
143
+ end
144
+
145
+ def start_tool_call(stream_key, tool_call)
146
+ tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
147
+
148
+ @tool_calls[tool_call_id] = ToolCall.new(
149
+ id: tool_call_id,
150
+ name: tool_call.name,
151
+ arguments: initial_tool_call_arguments(tool_call),
152
+ thought_signature: tool_call.thought_signature,
153
+ remote: tool_call.remote?
154
+ )
155
+ @tool_call_ids_by_index[stream_key] = tool_call_id unless stream_key.nil?
156
+ @latest_tool_call_id = tool_call_id
157
+ end
158
+
159
+ def initial_tool_call_arguments(tool_call)
160
+ arguments = tool_call.arguments
161
+ return +'' if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
162
+
163
+ arguments.dup
164
+ end
165
+
166
+ def append_tool_call_fragment(stream_key, tool_call)
167
+ existing = find_tool_call(stream_key)
168
+ return unless existing
169
+
170
+ fragment = tool_call.arguments
171
+ fragment = '' if fragment.nil?
172
+ existing.arguments << fragment
173
+ return unless tool_call.thought_signature && existing.thought_signature.nil?
174
+
175
+ existing.thought_signature = tool_call.thought_signature
176
+ end
177
+
178
+ def find_tool_call(stream_key)
179
+ return @tool_calls[@latest_tool_call_id] if stream_key.nil?
180
+
181
+ @tool_calls[@tool_call_ids_by_index[stream_key]] || @tool_calls[stream_key]
182
+ end
183
+
184
+ def count_tokens(chunk)
185
+ tokens = chunk.tokens
186
+ @input_tokens = tokens.input if tokens.input
187
+ @output_tokens = tokens.output if tokens.output
188
+ @cache_read_tokens = tokens.cache_read if tokens.cache_read
189
+ @cache_write_tokens = tokens.cache_write if tokens.cache_write
190
+ @thinking_tokens = tokens.thinking if tokens.thinking
191
+ @server_tool_use = tokens.server_tool_use if tokens.server_tool_use
192
+ @reported_cost = tokens.reported_cost if tokens.reported_cost
193
+ end
194
+
195
+ def handle_chunk_content(chunk)
196
+ accumulate_tool_calls(chunk.tool_calls) if chunk.tool_call?
197
+
198
+ content_text = chunk.content || ''
199
+ @content << (content_text.is_a?(String) ? content_text : content_text.to_s)
200
+ end
201
+
202
+ def append_thinking_from_chunk(chunk)
203
+ thinking = chunk.thinking
204
+ return unless thinking
205
+
206
+ unless thinking.text.nil?
207
+ @thinking_text ||= +''
208
+ @thinking_text << thinking.text.to_s
209
+ end
210
+ @thinking_signature ||= thinking.signature # rubocop:disable Naming/MemoizedInstanceVariableName
211
+ end
212
+ end
213
+ end
214
+ end