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,48 +1,184 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Custom error class that wraps API errors from different providers
5
- # into a consistent format with helpful error messages.
4
+ # Error is the base class for provider-operation errors raised by
5
+ # RubyLLM, including API, network, capability, and provider response-shape
6
+ # failures. When an HTTP response is available, it wraps that response and
7
+ # normalizes the message across providers. Subclasses map common HTTP
8
+ # status codes: BadRequestError (400), UnauthorizedError (401),
9
+ # PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
10
+ # ServerError (500), ServiceUnavailableError (502 to 504), and
11
+ # OverloadedError (529).
12
+ #
13
+ # begin
14
+ # RubyLLM.chat.ask "Translate 'hello' to French."
15
+ # rescue RubyLLM::RateLimitError
16
+ # puts "Rate limit hit. Please wait a moment."
17
+ # rescue RubyLLM::Error => e
18
+ # puts "API error: #{e.message}"
19
+ # puts e.response&.status
20
+ # end
21
+ #
22
+ # Local setup and programming errors, such as ConfigurationError and
23
+ # ModelNotFoundError, inherit from StandardError directly and are not
24
+ # caught by rescuing Error.
6
25
  class Error < StandardError
26
+ # The HTTP response that caused the error, or +nil+ when none is
27
+ # available. Its +status+ and +body+ carry the provider's reply.
7
28
  attr_reader :response
8
29
 
9
- def initialize(response = nil, message = nil)
10
- if response.is_a?(String)
11
- message = response
12
- response = nil
13
- end
30
+ def self.default_message # :nodoc:
31
+ nil
32
+ end
14
33
 
34
+ # Creates an error with +message+. Pass +response:+ to attach the HTTP
35
+ # response; its body supplies the message when +message+ is +nil+.
36
+ def initialize(message = nil, response: nil)
15
37
  @response = response
16
- super(message || response&.body)
38
+ super(message || response&.body || self.class.default_message)
17
39
  end
18
40
  end
19
41
 
20
- # Error classes for non-HTTP errors
42
+ # Raised when a deprecated API is used and
43
+ # Configuration#deprecation_behavior is +:raise+. With the default
44
+ # +:warn+, deprecations are logged instead.
45
+ #
46
+ # RubyLLM.configure do |config|
47
+ # config.deprecation_behavior = :raise
48
+ # end
49
+ class DeprecationError < StandardError; end
50
+
51
+ # Raised when required configuration, such as a provider API key, is
52
+ # missing.
21
53
  class ConfigurationError < StandardError; end
54
+
55
+ # Raised when RubyLLM.render_prompt cannot find the named prompt file.
22
56
  class PromptNotFoundError < StandardError; end
57
+
58
+ # Raised when a message role outside +:system+, +:user+, +:assistant+, or
59
+ # +:tool+ is used.
23
60
  class InvalidRoleError < StandardError; end
61
+
62
+ # Raised when the +choice:+ option of Chat#with_tool_options is neither a
63
+ # known mode nor the name of a registered tool.
24
64
  class InvalidToolChoiceError < StandardError; end
65
+
66
+ # Raised when a user message is staged while the last response still has
67
+ # unanswered tool calls. Providers reject such a transcript, so the chat
68
+ # refuses it up front: finish the round with #complete, recording
69
+ # #approve or #deny decisions for calls that require approval.
70
+ class PendingToolCallsError < StandardError; end
71
+
72
+ # Raised when a requested model id is not in the model registry.
25
73
  class ModelNotFoundError < StandardError; end
26
74
 
27
- # Raised when RubyLLM cannot format an attachment for the selected provider.
28
- class UnsupportedAttachmentError < StandardError
29
- GUIDANCE = 'Consider using a model that supports this attachment type.'
75
+ # Raised when a model registry cannot be fetched, parsed, or persisted.
76
+ class ModelRegistryError < StandardError; end
77
+
78
+ # Raised when an in-flight chat operation is cancelled with Chat#cancel.
79
+ class CancelledError < StandardError
80
+ # Creates an error for a cancelled chat operation.
81
+ def initialize(message = 'Chat generation cancelled')
82
+ super
83
+ end
84
+ end
85
+
86
+ # Raised when Chat#with_server_tools is used with a provider RubyLLM has
87
+ # no server-tool support for, or with an alias the provider's protocol
88
+ # does not define.
89
+ class UnsupportedServerToolError < Error; end
30
90
 
31
- def initialize(type = nil)
91
+ # Raised when an attachment cannot be formatted for the selected provider,
92
+ # for example an audio file sent to a model without audio input.
93
+ class UnsupportedAttachmentError < Error
94
+ GUIDANCE = 'Consider using a model that supports this attachment type.' # :nodoc:
95
+
96
+ def initialize(type = nil) # :nodoc:
32
97
  message = 'Unsupported attachment type'
33
98
  message = "#{message}: #{type}" if type
34
99
  super("#{message}. #{GUIDANCE}")
35
100
  end
36
101
  end
37
102
 
38
- # Error classes for different HTTP status codes
39
- class BadRequestError < Error; end
40
- class ForbiddenError < Error; end
41
- class ContextLengthExceededError < Error; end
42
- class OverloadedError < Error; end
43
- class PaymentRequiredError < Error; end
44
- class RateLimitError < Error; end
45
- class ServerError < Error; end
46
- class ServiceUnavailableError < Error; end
47
- class UnauthorizedError < Error; end
103
+ # Raised for HTTP 400 responses when the request is invalid.
104
+ class BadRequestError < Error
105
+ def self.default_message # :nodoc:
106
+ 'Invalid request - please check your input'
107
+ end
108
+ end
109
+
110
+ # Raised for HTTP 403 responses when the API key lacks permission for the
111
+ # requested resource.
112
+ class ForbiddenError < Error
113
+ def self.default_message # :nodoc:
114
+ 'Forbidden - you do not have permission to access this resource'
115
+ end
116
+ end
117
+
118
+ # Raised when the request exceeds the model's context window or token
119
+ # limits.
120
+ class ContextLengthExceededError < Error
121
+ def self.default_message # :nodoc:
122
+ 'Context length exceeded'
123
+ end
124
+ end
125
+
126
+ # Raised when a provider returns tool-call arguments that are not valid
127
+ # JSON, often because the response was truncated mid-tool-call.
128
+ class ToolCallParseError < Error
129
+ # Returns the normalized reason generation stopped, or +nil+.
130
+ attr_reader :finish_reason
131
+
132
+ def initialize(message = nil, response: nil, finish_reason: nil) # :nodoc:
133
+ @finish_reason = finish_reason
134
+ message ||= 'Provider returned malformed tool call arguments'
135
+ message = "#{message} (finish_reason: #{finish_reason})" if finish_reason
136
+ super(message, response: response)
137
+ end
138
+ end
139
+
140
+ # Raised for HTTP 529 responses when the provider is temporarily
141
+ # overloaded.
142
+ class OverloadedError < Error
143
+ def self.default_message # :nodoc:
144
+ 'Service overloaded - please try again later'
145
+ end
146
+ end
147
+
148
+ # Raised for HTTP 402 responses when the provider account has a billing
149
+ # or quota problem.
150
+ class PaymentRequiredError < Error
151
+ def self.default_message # :nodoc:
152
+ 'Payment required - please top up your account'
153
+ end
154
+ end
155
+
156
+ # Raised for HTTP 429 responses when the provider rate limit is exceeded.
157
+ class RateLimitError < Error
158
+ def self.default_message # :nodoc:
159
+ 'Rate limit exceeded - please wait a moment'
160
+ end
161
+ end
162
+
163
+ # Raised for HTTP 500 responses when the provider reports a server error.
164
+ class ServerError < Error
165
+ def self.default_message # :nodoc:
166
+ 'API server error - please try again'
167
+ end
168
+ end
169
+
170
+ # Raised for HTTP 502, 503, and 504 responses when the provider is
171
+ # temporarily unavailable.
172
+ class ServiceUnavailableError < Error
173
+ def self.default_message # :nodoc:
174
+ 'API server unavailable - please try again later'
175
+ end
176
+ end
177
+
178
+ # Raised for HTTP 401 responses when the API key is missing or invalid.
179
+ class UnauthorizedError < Error
180
+ def self.default_message # :nodoc:
181
+ 'Invalid API key - check your credentials'
182
+ end
183
+ end
48
184
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A Fallback is a fallback model target configured with Chat#with_fallbacks.
5
+ # When the active model fails with a matching error, the chat retries the
6
+ # request with each fallback in order. Instances are yielded to
7
+ # Chat#before_fallback and Chat#after_fallback callbacks, enriched with the
8
+ # details of that attempt.
9
+ #
10
+ # chat = RubyLLM.chat(model: "gpt-4.1")
11
+ # .with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5")
12
+ #
13
+ # chat.before_fallback do |fallback|
14
+ # puts "Falling back from #{fallback.from.id} to #{fallback.to.id}"
15
+ # end
16
+ #
17
+ # chat.after_fallback do |fallback|
18
+ # puts "Fallback #{fallback.succeeded? ? 'succeeded' : 'failed'}"
19
+ # end
20
+ class Fallback
21
+ include Support::Inspectable
22
+
23
+ # The error classes that trigger a fallback when Chat#with_fallbacks is
24
+ # called without +on:+.
25
+ DEFAULT_ERRORS = [
26
+ RateLimitError,
27
+ ServerError,
28
+ ServiceUnavailableError,
29
+ OverloadedError,
30
+ Faraday::TimeoutError,
31
+ Faraday::ConnectionFailed
32
+ ].freeze
33
+
34
+ # The model id of the fallback target.
35
+ attr_reader :id
36
+
37
+ # The provider slug of the fallback target as a String, or +nil+.
38
+ attr_reader :provider
39
+
40
+ # The Model object the fallback was configured with, or +nil+ when it was
41
+ # configured with a model id.
42
+ attr_reader :model
43
+
44
+ # The Chat performing the fallback attempt.
45
+ attr_reader :chat
46
+
47
+ # The error that triggered the fallback.
48
+ attr_reader :error
49
+
50
+ # The Model the chat is falling back from.
51
+ attr_reader :from
52
+
53
+ # The Model the chat is falling back to.
54
+ attr_reader :to
55
+
56
+ # The fallback attempt number, starting at 1.
57
+ attr_reader :attempt
58
+
59
+ # The response Message from a successful fallback attempt, or +nil+.
60
+ attr_reader :response
61
+
62
+ # The error raised by the fallback attempt itself, or +nil+ if it
63
+ # succeeded.
64
+ attr_reader :fallback_error
65
+
66
+ attr_reader :streaming # :nodoc:
67
+
68
+ def self.build(value) # :nodoc:
69
+ case value
70
+ when self
71
+ value
72
+ when RubyLLM::Model
73
+ new(model: value)
74
+ when String, Symbol
75
+ new(id: value.to_s)
76
+ else
77
+ raise ArgumentError, 'Expected a model id or RubyLLM::Model'
78
+ end
79
+ end
80
+
81
+ def inspect_attributes # :nodoc:
82
+ { id: id, provider: provider, attempt: attempt }
83
+ end
84
+
85
+ def initialize(id: nil, provider: nil, model: nil, **attributes) # :nodoc:
86
+ @id = id || model&.id
87
+ @provider = (provider || model&.provider)&.to_s
88
+ @model = model
89
+ @chat = attributes[:chat]
90
+ @error = attributes[:error]
91
+ @from = attributes[:from]
92
+ @to = attributes[:to]
93
+ @attempt = attributes[:attempt]
94
+ @streaming = attributes.fetch(:streaming, false)
95
+ @chunks_yielded = attributes.fetch(:chunks_yielded, false)
96
+ @response = attributes[:response]
97
+ @fallback_error = attributes[:fallback_error]
98
+ end
99
+
100
+ def with_attempt(**attributes) # :nodoc:
101
+ self.class.new(id: id, provider: provider, model: model, **attributes)
102
+ end
103
+
104
+ def finish(response: nil, fallback_error: nil) # :nodoc:
105
+ @response = response
106
+ @fallback_error = fallback_error
107
+ self
108
+ end
109
+
110
+ # Returns whether the fallback happened during a streaming request.
111
+ def streaming?
112
+ streaming
113
+ end
114
+
115
+ # Returns whether the failed model already yielded stream chunks before
116
+ # the fallback.
117
+ def chunks_yielded?
118
+ @chunks_yielded
119
+ end
120
+
121
+ # Returns +true+ if the fallback attempt raised an error, +false+
122
+ # otherwise.
123
+ def failed?
124
+ !fallback_error.nil?
125
+ end
126
+
127
+ # Returns +true+ if the fallback attempt produced a response without
128
+ # error, +false+ otherwise.
129
+ def succeeded?
130
+ !response.nil? && !failed?
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'marcel'
4
+
5
+ module RubyLLM
6
+ module Files # :nodoc:
7
+ module MimeType # :nodoc: all
8
+ module_function
9
+
10
+ def for(...)
11
+ Marcel::MimeType.for(...)
12
+ end
13
+
14
+ def image?(type)
15
+ type.start_with?('image/')
16
+ end
17
+
18
+ def video?(type)
19
+ type.start_with?('video/')
20
+ end
21
+
22
+ def audio?(type)
23
+ type.start_with?('audio/')
24
+ end
25
+
26
+ def pdf?(type)
27
+ type == 'application/pdf'
28
+ end
29
+
30
+ def document?(type)
31
+ return false if pdf?(type) || text?(type)
32
+
33
+ DOCUMENT_MIME_TYPES.include?(type) ||
34
+ DOCUMENT_MIME_PREFIXES.any? { |prefix| type.start_with?(prefix) }
35
+ end
36
+
37
+ def text?(type)
38
+ type.start_with?('text/') ||
39
+ TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
40
+ NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
41
+ end
42
+
43
+ # Structured-syntax suffixes (+json, +xml, ...) that mark a non-text/ type as text.
44
+ TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
45
+
46
+ # MIME types that don't have a text/ prefix but should be treated as text
47
+ NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
48
+ 'application/json', # Base type, even if specific ones end with +json
49
+ 'application/xml', # Base type, even if specific ones end with +xml
50
+ 'application/javascript',
51
+ 'application/ecmascript',
52
+ 'application/rtf',
53
+ 'application/sql',
54
+ 'application/x-sh',
55
+ 'application/x-csh',
56
+ 'application/x-httpd-php',
57
+ 'application/sdp',
58
+ 'application/sparql-query',
59
+ 'application/graphql',
60
+ 'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
61
+ 'application/mbox', # Mailbox format
62
+ 'application/x-tex',
63
+ 'application/x-latex',
64
+ 'application/x-perl',
65
+ 'application/x-python',
66
+ 'application/x-tcl',
67
+ 'application/pgp-signature', # Often ASCII armored
68
+ 'application/pgp-keys', # Often ASCII armored
69
+ 'application/vnd.coffeescript',
70
+ 'application/vnd.dart',
71
+ 'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
72
+ 'application/vnd.zul', # ZK User Interface Language (can be XML-like)
73
+ 'application/x-yaml', # Common non-standard for YAML
74
+ 'application/yaml', # Standard for YAML
75
+ 'application/toml' # TOML configuration files
76
+ ].freeze
77
+
78
+ DOCUMENT_MIME_TYPES = [
79
+ 'application/msword',
80
+ 'application/rtf',
81
+ 'application/vnd.apple.keynote',
82
+ 'application/vnd.apple.numbers',
83
+ 'application/vnd.apple.pages',
84
+ 'application/vnd.google-apps.document',
85
+ 'application/vnd.google-apps.presentation',
86
+ 'application/vnd.google-apps.spreadsheet',
87
+ 'application/vnd.ms-excel',
88
+ 'application/vnd.ms-powerpoint'
89
+ ].freeze
90
+
91
+ DOCUMENT_MIME_PREFIXES = [
92
+ 'application/vnd.openxmlformats-officedocument.',
93
+ 'application/vnd.oasis.opendocument.'
94
+ ].freeze
95
+ end
96
+ end
97
+ end
@@ -3,82 +3,203 @@
3
3
  require 'base64'
4
4
 
5
5
  module RubyLLM
6
- # Represents a generated image from an AI model.
6
+ # An Image is a generated or edited image. Save it to a file with #save
7
+ # or read its bytes with #to_blob. Both handle hosted URLs and inline data.
8
+ #
9
+ # image = RubyLLM.paint("a sunset over mountains in watercolor style")
10
+ # image.save("sunset.png")
11
+ #
7
12
  class Image
8
- attr_reader :url, :data, :mime_type, :revised_prompt, :model_id, :usage
13
+ include Support::Inspectable
14
+ include Accounting::Usage::Result
9
15
 
10
- def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil, usage: {}) # rubocop:disable Metrics/ParameterLists
16
+ # The URL of the hosted image, for providers that return one, or +nil+.
17
+ attr_reader :url
18
+
19
+ # The Base64-encoded image data, for providers that return the image
20
+ # inline, or +nil+.
21
+ attr_reader :data
22
+
23
+ # The MIME type of the image data, such as <tt>"image/png"</tt>.
24
+ attr_reader :mime_type
25
+
26
+ # The provider's rewritten version of the prompt, when reported.
27
+ attr_reader :revised_prompt
28
+
29
+ # The id of the model that generated the image.
30
+ attr_reader :model
31
+
32
+ # Generates an image from +prompt+ and returns an Image. Most code
33
+ # calls this through RubyLLM.paint.
34
+ #
35
+ # +model:+ selects the image model and defaults to the configured
36
+ # +default_image_model+. +provider:+ forces a specific provider, and
37
+ # +assume_model_exists:+ skips the registry lookup, which is useful
38
+ # for custom endpoints. +size:+ requests dimensions on models that
39
+ # support it. +count:+ asks for several images in one request, returning
40
+ # an Array of Images instead of one. +with:+ passes one or more source
41
+ # images for editing, and +mask:+ constrains which parts of the image
42
+ # may change. +provider_options:+ takes options in the provider's
43
+ # request vocabulary and merges them into the request as-is.
44
+ # +context:+ supplies a Context whose configuration replaces the
45
+ # global one. +metadata:+ is included in the instrumentation payload.
46
+ #
47
+ # image = RubyLLM.paint("A small watercolor robot", model: "gpt-image-2")
48
+ #
49
+ # images = RubyLLM.paint("A small watercolor robot", count: 4)
50
+ # images.each_with_index { |image, i| image.save("robot-#{i}.png") }
51
+ #
52
+ # RubyLLM.paint(
53
+ # "Turn the logo green and keep the background transparent",
54
+ # model: "gpt-image-2",
55
+ # with: "logo.png"
56
+ # )
57
+ #
58
+ # Providers that cannot generate several images in one request ignore
59
+ # +count:+ and return a single Image.
60
+ def self.paint(prompt,
61
+ model: nil,
62
+ provider: nil,
63
+ assume_model_exists: false,
64
+ size: nil,
65
+ count: nil,
66
+ context: nil,
67
+ with: nil,
68
+ mask: nil,
69
+ provider_options: {},
70
+ metadata: nil)
71
+ config = context&.config || RubyLLM.config
72
+ model ||= config.default_image_model
73
+ model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
74
+ config: config)
75
+ empty_tokens = Tokens.new
76
+ payload = {
77
+ provider: provider_instance.slug,
78
+ provider_class: provider_instance.class.display_name,
79
+ model: model.id,
80
+ model_info: model,
81
+ prompt: prompt,
82
+ size: size,
83
+ count: count,
84
+ provider_options: provider_options,
85
+ metadata: metadata,
86
+ tokens: empty_tokens,
87
+ cost: Cost.new(tokens: empty_tokens, model:, category: :images)
88
+ }
89
+
90
+ RubyLLM.instrument('image.ruby_llm', payload, config: config) do |event|
91
+ result = provider_instance.paint(prompt, model:, size:, count:, with:, mask:, provider_options:)
92
+ images = Support::Utils.to_safe_array(result)
93
+ event[:result] = result
94
+ event[:response_model] = images.first&.model
95
+ event[:tokens] = Tokens.aggregate(images.map(&:tokens))
96
+ event[:cost] = Cost.aggregate(images.map(&:cost))
97
+ result
98
+ end
99
+ end
100
+
101
+ # :stopdoc:
102
+
103
+ # Set by the protocol that generated the image, so a Context's
104
+ # connection settings reach #to_blob.
105
+ attr_writer :config
106
+
107
+ def config
108
+ @config || RubyLLM.config
109
+ end
110
+
111
+ def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model: nil, usage: {})
11
112
  @url = url
12
113
  @data = data
13
114
  @mime_type = mime_type
14
115
  @revised_prompt = revised_prompt
15
- @model_id = model_id
16
- @usage = usage
116
+ @model = model
117
+ @raw_usage = usage
17
118
  end
119
+ # :startdoc:
18
120
 
121
+ # Returns +true+ if the image holds inline Base64 data, +false+ otherwise.
19
122
  def base64?
20
123
  !@data.nil?
21
124
  end
22
125
 
126
+ # Returns the raw binary image bytes, decoding #data when present or
127
+ # downloading from #url otherwise.
128
+ #
129
+ # image_bytes = image.to_blob
130
+ #
23
131
  def to_blob
24
132
  if base64?
25
133
  Base64.decode64 @data
26
134
  else
27
- response = Connection.basic.get @url
135
+ response = Transport::Connection.basic(config).get @url
28
136
  response.body
29
137
  end
30
138
  end
31
139
 
140
+ # Writes the binary image to +path+, expanding it first. Returns
141
+ # +path+ as given.
142
+ #
143
+ # image.save("steampunk_owl.png")
144
+ #
32
145
  def save(path)
33
146
  File.binwrite(File.expand_path(path), to_blob)
34
147
  path
35
148
  end
36
149
 
37
- def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
38
- model: nil,
39
- provider: nil,
40
- assume_model_exists: false,
41
- size: '1024x1024',
42
- context: nil,
43
- with: nil,
44
- mask: nil,
45
- params: {})
46
- config = context&.config || RubyLLM.config
47
- model ||= config.default_image_model
48
- model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
49
- config: config)
50
- model_id = model.id
51
-
52
- provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
53
- end
54
-
150
+ # Returns a Tokens with usage across every provider attempt.
151
+ # Its fields are +nil+ when none were reported.
152
+ #
153
+ # image.tokens.input
154
+ # image.tokens.output
155
+ #
55
156
  def tokens
56
- @tokens ||= Tokens.build(
57
- input: usage_value('input_tokens'),
58
- output: usage_value('output_tokens')
157
+ return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
158
+
159
+ @tokens ||= Tokens.new(
160
+ input: raw_usage['input_tokens'],
161
+ output: raw_usage['output_tokens'],
162
+ reported_cost: raw_usage['cost']
59
163
  )
60
164
  end
61
165
 
166
+ # Returns a Cost across every provider attempt, using reported prices
167
+ # when available and registry pricing otherwise.
168
+ #
169
+ # image.cost.total
170
+ #
62
171
  def cost
172
+ return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
173
+
63
174
  Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
64
175
  end
65
176
 
177
+ # Returns the registry Model for #model, or +nil+ if the model id
178
+ # is missing or not in the registry.
66
179
  def model_info
67
- return unless model_id
180
+ return unless model
68
181
 
69
- @model_info ||= RubyLLM.models.find(model_id)
182
+ @model_info ||= RubyLLM.models.find(model)
70
183
  rescue ModelNotFoundError
71
184
  nil
72
185
  end
73
186
 
74
187
  private
75
188
 
189
+ attr_reader :raw_usage
190
+
76
191
  def input_tokens_details
77
- usage_value('input_tokens_details')
192
+ raw_usage['input_tokens_details']
78
193
  end
79
194
 
80
- def usage_value(key)
81
- usage[key] || usage[key.to_sym]
195
+ def inspect_attributes # :nodoc:
196
+ {
197
+ model: model,
198
+ mime_type: mime_type,
199
+ url: url,
200
+ data: data && "#{data.bytesize} bytes",
201
+ revised_prompt: revised_prompt
202
+ }
82
203
  end
83
204
  end
84
205
  end