ruby_llm 1.15.0 → 2.0.0.rc1

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 (470) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +87 -33
  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 -6
  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 +25 -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 +100 -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 -136
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +119 -75
  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 -150
  109. data/lib/ruby_llm/aliases.json +338 -167
  110. data/lib/ruby_llm/attachment.rb +217 -61
  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 +1208 -150
  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 +274 -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 +141 -7
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +161 -89
  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 +155 -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 +43 -14
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +66 -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 +70380 -33380
  137. data/lib/ruby_llm/models.rb +528 -201
  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 +540 -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 +166 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
  153. data/lib/ruby_llm/protocols/anthropic.rb +100 -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/protocols/chat_completions/media.rb +121 -0
  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 +17 -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 +685 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
  192. data/lib/ruby_llm/protocols/converse.rb +54 -0
  193. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  194. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  195. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  196. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  197. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  198. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  199. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  208. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  209. data/lib/ruby_llm/protocols/files.rb +119 -0
  210. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  211. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  212. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  213. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  214. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  215. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  216. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  217. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  218. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  219. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
  220. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  221. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  222. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  223. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  224. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  225. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  226. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  227. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  228. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  229. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  230. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  231. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  232. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  233. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  234. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  235. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  236. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  237. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  238. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  239. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  240. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  242. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  243. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  244. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  248. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  249. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  250. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  251. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  252. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  253. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  254. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  255. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  256. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  257. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  258. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  259. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  261. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  262. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  263. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  264. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  265. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  266. data/lib/ruby_llm/protocols/responses.rb +35 -0
  267. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  268. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  271. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  272. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  273. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  274. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  275. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  276. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  277. data/lib/ruby_llm/provider.rb +565 -124
  278. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  279. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  280. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  281. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  282. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  283. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  284. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  285. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  286. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  287. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  288. data/lib/ruby_llm/providers/azure/media.rb +6 -15
  289. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  290. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  291. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  292. data/lib/ruby_llm/providers/azure.rb +77 -78
  293. data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
  294. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  295. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  300. data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
  301. data/lib/ruby_llm/providers/bedrock.rb +217 -46
  302. data/lib/ruby_llm/providers/cohere.rb +31 -0
  303. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  304. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
  305. data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
  306. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  307. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  308. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  309. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  310. data/lib/ruby_llm/providers/gemini.rb +15 -8
  311. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
  312. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  313. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  314. data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
  315. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  316. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  317. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  318. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  319. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  320. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  321. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  322. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  323. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  324. data/lib/ruby_llm/providers/mistral/media.rb +43 -0
  325. data/lib/ruby_llm/providers/mistral/models.rb +57 -21
  326. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  327. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  328. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  329. data/lib/ruby_llm/providers/mistral.rb +18 -6
  330. data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
  331. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  332. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  333. data/lib/ruby_llm/providers/ollama.rb +9 -8
  334. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  335. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  336. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  337. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  338. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  339. data/lib/ruby_llm/providers/openai.rb +92 -11
  340. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
  341. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  342. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  343. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  344. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  345. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  346. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  347. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  348. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  349. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  350. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  351. data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
  352. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  353. data/lib/ruby_llm/providers/perplexity.rb +29 -21
  354. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  355. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  356. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  357. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  359. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  360. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  361. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  362. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  363. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  364. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  365. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  366. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  367. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  368. data/lib/ruby_llm/providers/vertexai.rb +164 -17
  369. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  370. data/lib/ruby_llm/providers/xai/chat.rb +10 -0
  371. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  372. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  373. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  374. data/lib/ruby_llm/providers/xai/models.rb +32 -48
  375. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  377. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  378. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  379. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  380. data/lib/ruby_llm/providers/xai.rb +17 -7
  381. data/lib/ruby_llm/railtie.rb +11 -16
  382. data/lib/ruby_llm/rerank.rb +105 -0
  383. data/lib/ruby_llm/research_job.rb +241 -0
  384. data/lib/ruby_llm/search_results.rb +68 -0
  385. data/lib/ruby_llm/server_tool_call.rb +73 -0
  386. data/lib/ruby_llm/speech.rb +159 -0
  387. data/lib/ruby_llm/speech_chunk.rb +33 -0
  388. data/lib/ruby_llm/support/deprecator.rb +22 -0
  389. data/lib/ruby_llm/support/inspectable.rb +49 -0
  390. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  391. data/lib/ruby_llm/support/utils.rb +147 -0
  392. data/lib/ruby_llm/thinking.rb +127 -20
  393. data/lib/ruby_llm/tokenization.rb +59 -0
  394. data/lib/ruby_llm/tokens.rb +103 -33
  395. data/lib/ruby_llm/tool.rb +266 -91
  396. data/lib/ruby_llm/tool_call.rb +36 -3
  397. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  398. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  399. data/lib/ruby_llm/transcription.rb +139 -13
  400. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  401. data/lib/ruby_llm/transport/connection.rb +193 -0
  402. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  403. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  404. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  405. data/lib/ruby_llm/uploaded_file.rb +144 -0
  406. data/lib/ruby_llm/version.rb +2 -1
  407. data/lib/ruby_llm/video.rb +136 -0
  408. data/lib/ruby_llm/video_job.rb +150 -0
  409. data/lib/ruby_llm/workflow.rb +91 -0
  410. data/lib/ruby_llm.rb +385 -4
  411. data/lib/tasks/ruby_llm.rake +21 -16
  412. data/skills/rubyllm/SKILL.md +81 -0
  413. data/skills/rubyllm/agents/openai.yaml +4 -0
  414. metadata +340 -92
  415. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  416. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  417. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  418. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  419. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  422. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  424. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  426. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  428. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
  429. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  430. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  431. data/lib/ruby_llm/aliases.rb +0 -38
  432. data/lib/ruby_llm/connection.rb +0 -130
  433. data/lib/ruby_llm/content.rb +0 -77
  434. data/lib/ruby_llm/mime_type.rb +0 -71
  435. data/lib/ruby_llm/model/info.rb +0 -130
  436. data/lib/ruby_llm/models_schema.json +0 -171
  437. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
  438. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  439. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  440. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  441. data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
  442. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
  443. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
  444. data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
  445. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
  446. data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
  447. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  448. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  449. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  450. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
  451. data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
  452. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  453. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  454. data/lib/ruby_llm/providers/openai/chat.rb +0 -221
  455. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  456. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  457. data/lib/ruby_llm/providers/openai/media.rb +0 -84
  458. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  459. data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
  460. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  461. data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
  462. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  463. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  464. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  465. data/lib/ruby_llm/stream_accumulator.rb +0 -203
  466. data/lib/ruby_llm/streaming.rb +0 -175
  467. data/lib/ruby_llm/utils.rb +0 -91
  468. data/lib/tasks/models.rake +0 -565
  469. data/lib/tasks/release.rake +0 -67
  470. data/lib/tasks/vcr.rake +0 -124
@@ -1,112 +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
- class UnsupportedAttachmentError < StandardError; end
27
-
28
- # Error classes for different HTTP status codes
29
- class BadRequestError < Error; end
30
- class ForbiddenError < Error; end
31
- class ContextLengthExceededError < Error; end
32
- class OverloadedError < Error; end
33
- class PaymentRequiredError < Error; end
34
- class RateLimitError < Error; end
35
- class ServerError < Error; end
36
- class ServiceUnavailableError < Error; end
37
- class UnauthorizedError < Error; end
38
-
39
- # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
40
- class ErrorMiddleware < Faraday::Middleware
41
- def initialize(app, options = {})
42
- super(app)
43
- @provider = options[:provider]
74
+
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
90
+
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:
97
+ message = 'Unsupported attachment type'
98
+ message = "#{message}: #{type}" if type
99
+ super("#{message}. #{GUIDANCE}")
100
+ end
101
+ end
102
+
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'
44
107
  end
108
+ end
45
109
 
46
- def call(env)
47
- @app.call(env).on_complete do |response|
48
- self.class.parse_error(provider: @provider, response: response)
49
- end
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'
50
115
  end
116
+ end
51
117
 
52
- class << self
53
- CONTEXT_LENGTH_PATTERNS = [
54
- /context length/i,
55
- /context window/i,
56
- /maximum context/i,
57
- /request too large/i,
58
- /too many tokens/i,
59
- /token count exceeds/i,
60
- /input[_\s-]?token/i,
61
- /input or output tokens? must be reduced/i,
62
- /reduce the length of messages/i
63
- ].freeze
64
-
65
- def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
66
- message = provider&.parse_error(response)
67
-
68
- case response.status
69
- when 200..399
70
- message
71
- when 400
72
- if context_length_exceeded?(message)
73
- raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
74
- end
75
-
76
- raise BadRequestError.new(response, message || 'Invalid request - please check your input')
77
- when 401
78
- raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
79
- when 402
80
- raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
81
- when 403
82
- raise ForbiddenError.new(response,
83
- message || 'Forbidden - you do not have permission to access this resource')
84
- when 429
85
- if context_length_exceeded?(message)
86
- raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
87
- end
88
-
89
- raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
90
- when 500
91
- raise ServerError.new(response, message || 'API server error - please try again')
92
- when 502..504
93
- raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
94
- when 529
95
- raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
96
- else
97
- raise Error.new(response, message || 'An unknown error occurred')
98
- end
99
- end
100
-
101
- private
102
-
103
- def context_length_exceeded?(message)
104
- return false if message.to_s.empty?
105
-
106
- CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
107
- end
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'
108
175
  end
109
176
  end
110
- end
111
177
 
112
- Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
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
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