ruby_llm 1.16.0 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'stringio'
5
+
6
+ module RubyLLM
7
+ module Protocols
8
+ class ChatCompletions
9
+ # Image generation methods for the OpenAI API integration
10
+ module Images
11
+ module_function
12
+
13
+ def images_url(with: nil, mask: nil)
14
+ editing?(with, mask) ? 'images/edits' : 'images/generations'
15
+ end
16
+
17
+ def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
18
+ return render_edit_payload(prompt, model:, size:, count:, with:, mask:, provider_options:) if editing?(with,
19
+ mask)
20
+
21
+ {
22
+ model: model,
23
+ prompt: prompt,
24
+ n: count || 1,
25
+ size: size
26
+ }.merge(provider_options)
27
+ end
28
+
29
+ def parse_image_response(response, model:)
30
+ parse_image_responses(response, model:).first
31
+ end
32
+
33
+ def parse_image_responses(response, model:)
34
+ data = response.body
35
+ entries = Array(data['data'])
36
+
37
+ raise Error, 'Unexpected response format from OpenAI image API' if entries.empty?
38
+
39
+ entries.map.with_index do |image_data, index|
40
+ Image.new(
41
+ url: image_data['url'],
42
+ mime_type: 'image/png', # DALL-E typically returns PNGs
43
+ revised_prompt: image_data['revised_prompt'],
44
+ model: model,
45
+ data: image_data['b64_json'],
46
+ usage: index.zero? ? (data['usage'] || {}) : {}
47
+ )
48
+ end
49
+ end
50
+
51
+ def validate_paint_inputs!(with:, mask:)
52
+ return unless editing?(with, mask)
53
+
54
+ raise ArgumentError, 'with: is required when mask: is provided' if mask && !attachments?(with)
55
+ end
56
+
57
+ def render_edit_payload(prompt, model:, size:, with:, mask:, provider_options:, count: nil)
58
+ payload = { model: model, prompt: prompt, n: count || 1 }
59
+ if json_image_references?(model)
60
+ payload[:images] = build_image_references(with)
61
+ payload[:mask] = build_image_reference(mask) if mask
62
+ payload[:size] = size if size
63
+ else
64
+ payload[:image] = single_upload_part(build_upload_parts(with))
65
+ payload[:mask] = build_upload_part(mask) if mask
66
+ end
67
+ payload.merge(provider_options)
68
+ end
69
+
70
+ def json_image_references?(model)
71
+ model.match?(/\A(gpt-image|chatgpt-image)/)
72
+ end
73
+
74
+ def build_image_references(sources)
75
+ Array(sources).filter_map do |source|
76
+ next if blank_attachment?(source)
77
+
78
+ build_image_reference(source)
79
+ end
80
+ end
81
+
82
+ def build_image_reference(source)
83
+ attachment = Attachment.new(source, config: @config)
84
+ return { file_id: attachment.provider_file_id } if attachment.provider_file?
85
+ return { image_url: attachment.source.to_s } if attachment.url?
86
+
87
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
88
+
89
+ { image_url: attachment.for_llm }
90
+ end
91
+
92
+ def build_upload_parts(sources)
93
+ Array(sources).filter_map do |source|
94
+ next if blank_attachment?(source)
95
+
96
+ build_upload_part(source)
97
+ end
98
+ end
99
+
100
+ # Retries only rewind upload parts at the top level of the payload,
101
+ # so a lone image rides there instead of inside an array.
102
+ def single_upload_part(parts)
103
+ parts.one? ? parts.first : parts
104
+ end
105
+
106
+ def build_upload_part(source)
107
+ attachment = Attachment.new(source, config: @config)
108
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
109
+
110
+ Faraday::UploadIO.new(StringIO.new(attachment.content), attachment.mime_type, attachment.filename)
111
+ end
112
+
113
+ def editing?(with, mask)
114
+ attachments?(with) || !mask.nil?
115
+ end
116
+
117
+ def attachments?(value)
118
+ Array(value).any? { |item| !blank_attachment?(item) }
119
+ end
120
+
121
+ def blank_attachment?(value)
122
+ value.nil? || (value.is_a?(String) && value.strip.empty?)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,36 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- module Providers
5
- class OpenAI
4
+ module Protocols
5
+ class ChatCompletions
6
6
  # Handles formatting of media content (images, audio) for OpenAI APIs
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content, document_attachments: :pdf, image_attachments: true, audio_attachments: true)
11
- if content.is_a?(RubyLLM::Content::Raw)
12
- value = content.value
13
- return value.is_a?(Hash) ? value.to_json : value
14
- end
15
- return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
16
- return content unless content.is_a?(Content)
17
-
18
- parts = []
19
- parts << format_text(content.text) if content.text
20
-
21
- content.attachments.each do |attachment|
22
- parts << format_attachment(
10
+ def format_content(content, attachments = [], document_attachments: :pdf, image_attachments: true,
11
+ audio_attachments: true)
12
+ format_parts(content, attachments) do |attachment|
13
+ format_attachment(
23
14
  attachment,
24
15
  document_attachments:,
25
16
  image_attachments:,
26
17
  audio_attachments:
27
18
  )
28
19
  end
20
+ end
21
+
22
+ # Shared preamble and attachment loop for OpenAI-compatible providers.
23
+ # The block formats a single attachment in the provider's dialect.
24
+ def format_parts(content, attachments = [])
25
+ return content if attachments.empty?
26
+
27
+ parts = []
28
+ parts << format_text(content) if content
29
+
30
+ attachments.each do |attachment|
31
+ parts << yield(attachment)
32
+ end
29
33
 
30
34
  parts
31
35
  end
32
36
 
33
37
  def format_attachment(attachment, document_attachments:, image_attachments:, audio_attachments:)
38
+ return format_provider_file(attachment, document_attachments:) if attachment.provider_file?
39
+
34
40
  case attachment.type
35
41
  when :image
36
42
  raise UnsupportedAttachmentError, attachment.mime_type unless image_attachments
@@ -68,8 +74,15 @@ module RubyLLM
68
74
  }
69
75
  end
70
76
 
71
- def format_pdf(pdf)
72
- format_document(pdf)
77
+ def format_provider_file(file, document_attachments:)
78
+ raise UnsupportedAttachmentError, file.mime_type if document_attachments == :none
79
+
80
+ {
81
+ type: 'file',
82
+ file: {
83
+ file_id: file.provider_file_id
84
+ }
85
+ }
73
86
  end
74
87
 
75
88
  def format_text_file(text_file)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # Models methods of the OpenAI API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug)
15
+ Array(response.body['data']).map do |model_data|
16
+ model_id = model_data['id']
17
+
18
+ Model.new(
19
+ id: model_id,
20
+ name: model_id,
21
+ provider: slug,
22
+ created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
23
+ metadata: model_metadata(model_data)
24
+ )
25
+ end
26
+ end
27
+
28
+ def model_metadata(model_data)
29
+ metadata = {
30
+ object: model_data['object'],
31
+ owned_by: model_data['owned_by']
32
+ }
33
+ metadata[:shutdown_date] = model_data['shutdown_date'] if model_data['shutdown_date']
34
+ metadata
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # Moderation methods of the OpenAI API integration
7
+ module Moderation
8
+ module_function
9
+
10
+ def moderation_url
11
+ 'moderations'
12
+ end
13
+
14
+ def render_moderation_payload(input, model:, with: [], provider_options: {})
15
+ attachments = Attachment.wrap(with)
16
+
17
+ {
18
+ model: model,
19
+ input: moderation_input(input, attachments)
20
+ }.merge(provider_options)
21
+ end
22
+
23
+ def parse_moderation_response(response, model:)
24
+ data = response.body
25
+ raise Error.new(data.dig('error', 'message'), response:) if data.dig('error', 'message')
26
+
27
+ RubyLLM::Moderation.new(
28
+ id: data['id'],
29
+ model: model,
30
+ results: Array(data['results']).map { |result| RubyLLM::Moderation::Result.from_h(result) },
31
+ raw: data
32
+ )
33
+ end
34
+
35
+ def moderation_input(input, attachments)
36
+ return input if attachments.empty?
37
+
38
+ parts = []
39
+ parts << Media.format_text(input) if input
40
+ parts.concat(attachments.map { |attachment| format_moderation_attachment(attachment) })
41
+ parts
42
+ end
43
+
44
+ def format_moderation_attachment(attachment)
45
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
46
+
47
+ Media.format_image(attachment)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # The Jina-style rerank endpoint several OpenAI-compatible providers
7
+ # serve: OpenRouter at /api/v1/rerank and GPUStack at /v1/rerank,
8
+ # with identical request and response shapes.
9
+ module Rerank
10
+ module_function
11
+
12
+ def rerank_url
13
+ 'rerank'
14
+ end
15
+
16
+ def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
17
+ {
18
+ model: model,
19
+ query: query,
20
+ documents: documents,
21
+ top_n: top_n
22
+ }.compact.merge(provider_options)
23
+ end
24
+
25
+ def parse_rerank_response(response, model:, documents: [])
26
+ data = response.body
27
+ usage = data['usage'] || {}
28
+
29
+ RubyLLM::Rerank.new(
30
+ results: parse_rerank_results(data, documents),
31
+ model: data['model'] || model,
32
+ raw: data,
33
+ input_tokens: usage['total_tokens'],
34
+ reported_cost: usage['cost']
35
+ )
36
+ end
37
+
38
+ def parse_rerank_results(data, documents = [])
39
+ Array(data['results']).map do |result|
40
+ index = result['index']
41
+
42
+ RubyLLM::Rerank::Result.new(
43
+ index: index,
44
+ document: rerank_document(result['document']) || (index && documents[index]),
45
+ score: result['relevance_score']
46
+ )
47
+ end
48
+ end
49
+
50
+ def rerank_document(document)
51
+ document.is_a?(Hash) ? document['text'] : document
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # Speech generation methods for the OpenAI API integration
7
+ module Speech
8
+ module_function
9
+
10
+ def speech_url(model:) # rubocop:disable Lint/UnusedMethodArgument
11
+ 'audio/speech'
12
+ end
13
+
14
+ def stream_speech(payload, model:, voice:, format:, &)
15
+ stream_speech_response(speech_url(model:), payload, model:, voice:, format:, &)
16
+ end
17
+
18
+ def render_speech_payload(input, model:, voice:, format:, provider_options: {})
19
+ {
20
+ model: model,
21
+ input: input,
22
+ voice: voice || 'alloy',
23
+ response_format: format
24
+ }.compact.merge(provider_options)
25
+ end
26
+
27
+ def parse_speech_response(response, model:, voice:, format:)
28
+ resolved_format = (format || 'mp3').to_s
29
+
30
+ RubyLLM::Speech.new(
31
+ data: response.body,
32
+ model: model,
33
+ voice: voice || 'alloy',
34
+ format: resolved_format
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class ChatCompletions
8
+ # Streaming methods of the OpenAI API integration
9
+ module Streaming
10
+ module_function
11
+
12
+ def stream_url
13
+ completion_url
14
+ end
15
+
16
+ def build_chunk(data)
17
+ usage = data['usage'] || {}
18
+ delta = data.dig('choices', 0, 'delta') || {}
19
+ content_source = delta['content'] || data.dig('choices', 0, 'message', 'content')
20
+ content, thinking_from_blocks = extract_content_and_thinking(content_source)
21
+
22
+ Chunk.new(
23
+ role: :assistant,
24
+ model: data['model'],
25
+ content: content,
26
+ citations: extract_chunk_citations(delta, data),
27
+ thinking: Thinking.build(
28
+ text: thinking_from_blocks || delta['reasoning_content'] || delta['reasoning'],
29
+ signature: delta['reasoning_signature']
30
+ ),
31
+ tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false, stream_keys: true),
32
+ input_tokens: input_tokens(usage),
33
+ output_tokens: output_tokens(usage),
34
+ cache_read_tokens: cache_read_tokens(usage),
35
+ cache_write_tokens: cache_write_tokens(usage),
36
+ thinking_tokens: thinking_tokens(usage),
37
+ server_tool_use: server_tool_use(usage),
38
+ reported_cost: reported_cost(usage),
39
+ finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason'))
40
+ )
41
+ end
42
+
43
+ def extract_chunk_citations(delta, data)
44
+ annotations = parse_annotations(delta['annotations'], nil)
45
+ return annotations if annotations.any?
46
+
47
+ parse_root_citations(data)
48
+ end
49
+
50
+ def parse_streaming_error(data)
51
+ error_data = JSON.parse(data)
52
+ return [nil, error_data.to_s] unless error_data.is_a?(Hash)
53
+
54
+ error = error_data['error']
55
+ return [nil, error.to_s] unless error.is_a?(Hash)
56
+
57
+ case error['type']
58
+ when 'server_error'
59
+ [500, error['message']]
60
+ when 'rate_limit_exceeded', 'insufficient_quota'
61
+ [429, error['message']]
62
+ else
63
+ [400, error['message']]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -3,8 +3,8 @@
3
3
  require 'json'
4
4
 
5
5
  module RubyLLM
6
- module Providers
7
- class OpenAI
6
+ module Protocols
7
+ class ChatCompletions
8
8
  # Tools methods of the OpenAI API integration
9
9
  module Tools
10
10
  module_function
@@ -18,8 +18,8 @@ module RubyLLM
18
18
  }.freeze
19
19
 
20
20
  def parameters_schema_for(tool)
21
- tool.params_schema ||
22
- schema_from_parameters(tool.parameters)
21
+ tool.parameters_schema ||
22
+ schema_from_parameters(tool.declared_parameters)
23
23
  end
24
24
 
25
25
  def schema_from_parameters(parameters)
@@ -39,16 +39,9 @@ module RubyLLM
39
39
  }
40
40
  }
41
41
 
42
- return definition if tool.provider_params.empty?
42
+ return definition if tool.provider_options.empty?
43
43
 
44
- RubyLLM::Utils.deep_merge(definition, tool.provider_params)
45
- end
46
-
47
- def param_schema(param)
48
- {
49
- type: param.type,
50
- description: param.description
51
- }.compact
44
+ RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
52
45
  end
53
46
 
54
47
  def format_tool_calls(tool_calls)
@@ -72,7 +65,7 @@ module RubyLLM
72
65
  end
73
66
  end
74
67
 
75
- def parse_tool_call_arguments(tool_call)
68
+ def parse_tool_call_arguments(tool_call, response: nil, finish_reason: nil)
76
69
  arguments = tool_call.dig('function', 'arguments')
77
70
 
78
71
  if arguments.nil? || arguments.empty?
@@ -80,19 +73,24 @@ module RubyLLM
80
73
  else
81
74
  JSON.parse(arguments)
82
75
  end
76
+ rescue JSON::ParserError => e
77
+ raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
83
78
  end
84
79
 
85
- def parse_tool_calls(tool_calls, parse_arguments: true)
80
+ # Streaming deltas key by index so id-less argument fragments find
81
+ # their call even when a delta batches or interleaves several calls;
82
+ # complete responses key by id, which tool results join on.
83
+ def parse_tool_calls(tool_calls, parse_arguments: true, response: nil, finish_reason: nil, stream_keys: false)
86
84
  return nil unless tool_calls&.any?
87
85
 
88
86
  tool_calls.to_h do |tc|
89
87
  [
90
- tc['id'],
88
+ stream_keys ? tc['index'] || tc['id'] : tc['id'],
91
89
  ToolCall.new(
92
90
  id: tc['id'],
93
91
  name: tc.dig('function', 'name'),
94
92
  arguments: if parse_arguments
95
- parse_tool_call_arguments(tc)
93
+ parse_tool_call_arguments(tc, response: response, finish_reason: finish_reason)
96
94
  else
97
95
  tc.dig('function', 'arguments')
98
96
  end,
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class ChatCompletions
6
+ # Audio transcription methods for the OpenAI API integration
7
+ module Transcription
8
+ module_function
9
+
10
+ def transcription_url
11
+ 'audio/transcriptions'
12
+ end
13
+
14
+ def render_transcription_options(timestamps:, format:, streaming:)
15
+ return {} if timestamps.nil?
16
+
17
+ values = Array(timestamps).map(&:to_s)
18
+ unless values.any? && (values - %w[word segment]).empty?
19
+ raise ArgumentError, 'Transcription timestamps must be word or segment'
20
+ end
21
+ if streaming || (format && format != 'verbose_json')
22
+ raise ArgumentError, 'Transcription timestamps require a non-streaming verbose_json response'
23
+ end
24
+
25
+ { response_format: 'verbose_json', timestamp_granularities: values }
26
+ end
27
+
28
+ def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
29
+ speaker_references: nil, provider_options: {}, prompt: nil,
30
+ temperature: nil)
31
+ {
32
+ model: model,
33
+ file: file_part,
34
+ language: language,
35
+ response_format: format || default_response_format(model),
36
+ prompt: prompt,
37
+ temperature: temperature,
38
+ known_speaker_names: speaker_names,
39
+ known_speaker_references: encode_speaker_references(speaker_references)
40
+ }.compact.merge(provider_options)
41
+ end
42
+
43
+ def encode_speaker_references(references)
44
+ return nil unless references
45
+
46
+ references.map do |ref|
47
+ Attachment.new(ref, config: @config).for_llm
48
+ end
49
+ end
50
+
51
+ def reported_cost(_usage)
52
+ nil
53
+ end
54
+
55
+ # Diarization models return plain text with no segments unless the
56
+ # response format asks for them.
57
+ def default_response_format(model)
58
+ 'diarized_json' if model.include?('diarize')
59
+ end
60
+
61
+ # OpenAI streams transcriptions as server-sent events carrying text
62
+ # deltas, completed segments on diarization models, and a final
63
+ # event with the whole transcript and its usage.
64
+ def stream_transcription(payload, model:, &block)
65
+ chunks = []
66
+
67
+ stream_events(transcription_url, payload.merge(stream: 'true')) do |data|
68
+ chunk = build_transcription_chunk(data)
69
+ chunks << chunk
70
+ block.call chunk
71
+ end
72
+
73
+ build_streamed_transcription(chunks, model: model)
74
+ end
75
+
76
+ def build_transcription_chunk(data)
77
+ type = data['type']
78
+
79
+ RubyLLM::TranscriptionChunk.new(
80
+ type: type,
81
+ delta: data['delta'],
82
+ text: (data['text'] if type == RubyLLM::TranscriptionChunk::DONE),
83
+ segment: (data.except('type') if type == RubyLLM::TranscriptionChunk::SEGMENT),
84
+ raw: data
85
+ )
86
+ end
87
+
88
+ def build_streamed_transcription(chunks, model:)
89
+ final = chunks.reverse.find(&:done?)
90
+ data = final&.raw || {}
91
+ usage = data['usage'] || {}
92
+
93
+ RubyLLM::Transcription.new(
94
+ text: final&.text || streamed_transcript_text(chunks),
95
+ model: model,
96
+ language: data['language'],
97
+ duration: transcription_duration(usage),
98
+ segments: streamed_transcription_segments(chunks, data),
99
+ reported_cost: reported_cost(usage),
100
+ **transcription_tokens(usage)
101
+ )
102
+ end
103
+
104
+ # Diarization models stream segments instead of deltas, so the
105
+ # transcript is rebuilt from whichever the provider sent.
106
+ def streamed_transcript_text(chunks)
107
+ deltas = chunks.filter_map(&:delta)
108
+ return deltas.join if deltas.any?
109
+
110
+ chunks.filter_map { |chunk| chunk.segment&.fetch('text', nil) }.join(' ')
111
+ end
112
+
113
+ def streamed_transcription_segments(chunks, data)
114
+ segments = data['segments'] || chunks.filter_map(&:segment)
115
+ segments.empty? ? nil : segments
116
+ end
117
+
118
+ def parse_transcription_response(response, model:)
119
+ data = response.body
120
+
121
+ return RubyLLM::Transcription.new(text: data, model: model) if data.is_a?(String)
122
+
123
+ usage = data['usage'] || {}
124
+
125
+ RubyLLM::Transcription.new(
126
+ text: data['text'],
127
+ model: model,
128
+ language: data['language'],
129
+ duration: data['duration'] || transcription_duration(usage),
130
+ segments: data['segments'],
131
+ words: data['words'],
132
+ reported_cost: reported_cost(usage),
133
+ **transcription_tokens(usage)
134
+ )
135
+ end
136
+
137
+ def transcription_tokens(usage)
138
+ {
139
+ input_tokens: usage['input_tokens'] || usage['prompt_tokens'],
140
+ output_tokens: usage['output_tokens'] || usage['completion_tokens']
141
+ }
142
+ end
143
+
144
+ def transcription_duration(usage)
145
+ usage['seconds']
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end