ruby_llm 1.16.0 → 2.0.0.rc4

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 (480) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +108 -44
  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/legacy_content_sql.rb +34 -0
  91. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  92. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  93. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  94. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -0
  96. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  97. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +247 -0
  98. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
  99. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
  100. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  101. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
  102. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
  103. data/lib/ruby_llm/accounting/usage.rb +254 -0
  104. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  105. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  106. data/lib/ruby_llm/active_record/batch.rb +97 -0
  107. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  108. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  109. data/lib/ruby_llm/active_record/model.rb +135 -0
  110. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  111. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  112. data/lib/ruby_llm/active_record/usage.rb +61 -0
  113. data/lib/ruby_llm/agent.rb +1066 -151
  114. data/lib/ruby_llm/aliases.json +291 -101
  115. data/lib/ruby_llm/attachment.rb +192 -48
  116. data/lib/ruby_llm/batch.rb +432 -0
  117. data/lib/ruby_llm/cached_content.rb +112 -0
  118. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  119. data/lib/ruby_llm/chat.rb +1131 -198
  120. data/lib/ruby_llm/chunk.rb +10 -0
  121. data/lib/ruby_llm/citation.rb +105 -0
  122. data/lib/ruby_llm/configuration.rb +261 -24
  123. data/lib/ruby_llm/context.rb +128 -6
  124. data/lib/ruby_llm/cost.rb +217 -80
  125. data/lib/ruby_llm/downloaded_file.rb +33 -0
  126. data/lib/ruby_llm/embedding.rb +121 -17
  127. data/lib/ruby_llm/embedding_request.rb +53 -0
  128. data/lib/ruby_llm/error.rb +159 -23
  129. data/lib/ruby_llm/fallback.rb +133 -0
  130. data/lib/ruby_llm/files/mime_type.rb +97 -0
  131. data/lib/ruby_llm/image.rb +153 -32
  132. data/lib/ruby_llm/message.rb +242 -54
  133. data/lib/ruby_llm/model/modalities.rb +17 -4
  134. data/lib/ruby_llm/model/pricing.rb +24 -5
  135. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  136. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  137. data/lib/ruby_llm/model.rb +244 -2
  138. data/lib/ruby_llm/models/aliases.rb +41 -0
  139. data/lib/ruby_llm/models/registry.rb +165 -0
  140. data/lib/ruby_llm/models/schema.rb +99 -0
  141. data/lib/ruby_llm/models.json +76038 -34173
  142. data/lib/ruby_llm/models.rb +477 -215
  143. data/lib/ruby_llm/moderation.rb +139 -26
  144. data/lib/ruby_llm/ocr.rb +112 -0
  145. data/lib/ruby_llm/prompt.rb +79 -0
  146. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  147. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  148. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  149. data/lib/ruby_llm/protocol.rb +662 -0
  150. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  151. data/lib/ruby_llm/protocols/anthropic/chat.rb +543 -0
  152. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  153. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  154. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  155. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  156. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  157. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  158. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  159. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  160. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +110 -0
  161. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  162. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  163. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  164. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  165. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  166. data/lib/ruby_llm/protocols/chat_completions/chat.rb +484 -0
  167. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  168. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  169. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  170. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  171. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  172. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  173. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +63 -0
  174. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  175. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  176. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  177. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  178. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  179. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  180. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  181. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  182. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  183. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  184. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  185. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  186. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  187. data/lib/ruby_llm/protocols/cohere/rerank.rb +59 -0
  188. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  189. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  190. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  191. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  192. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  193. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  194. data/lib/ruby_llm/protocols/converse/chat.rb +694 -0
  195. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  196. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  197. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  198. data/lib/ruby_llm/protocols/converse.rb +54 -0
  199. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  200. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  201. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  202. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  203. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  204. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  209. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  210. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  211. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  212. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  213. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  214. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  215. data/lib/ruby_llm/protocols/files.rb +119 -0
  216. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  217. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  218. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  219. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +91 -0
  220. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  221. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  222. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  223. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  224. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  226. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  227. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  228. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  229. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  230. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  231. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  232. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  233. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  234. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  235. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  236. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  237. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  238. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  239. data/lib/ruby_llm/protocols/interactions/tools.rb +53 -0
  240. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  241. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  242. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  243. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  244. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  245. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  246. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  247. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  248. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  249. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  250. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  251. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  252. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  253. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  254. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  255. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  256. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  257. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  258. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  259. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  260. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  261. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  262. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  263. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  264. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  265. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  266. data/lib/ruby_llm/protocols/responses/chat.rb +466 -0
  267. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  268. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  269. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  270. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  271. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  272. data/lib/ruby_llm/protocols/responses.rb +35 -0
  273. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  274. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  275. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  276. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  277. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  278. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  279. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  280. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  281. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  282. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  283. data/lib/ruby_llm/provider.rb +560 -128
  284. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  285. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  286. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  287. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  288. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  289. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  290. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  291. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  292. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  293. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  294. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  295. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  296. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  297. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  298. data/lib/ruby_llm/providers/azure.rb +77 -78
  299. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  300. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  301. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  302. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  303. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  304. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  305. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  306. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  307. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  308. data/lib/ruby_llm/providers/cohere.rb +31 -0
  309. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  310. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  311. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  312. data/lib/ruby_llm/providers/deepseek/responses.rb +67 -0
  313. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  314. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  315. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  316. data/lib/ruby_llm/providers/gemini.rb +15 -8
  317. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  318. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  319. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  320. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  321. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  322. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  323. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  324. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  325. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  326. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  327. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  328. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  329. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  330. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  331. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  332. data/lib/ruby_llm/providers/mistral/ocr.rb +51 -0
  333. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  334. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  335. data/lib/ruby_llm/providers/mistral.rb +16 -4
  336. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  337. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  338. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  339. data/lib/ruby_llm/providers/ollama.rb +9 -8
  340. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  341. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  342. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  343. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  344. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  345. data/lib/ruby_llm/providers/openai.rb +92 -11
  346. data/lib/ruby_llm/providers/openrouter/chat.rb +126 -108
  347. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  348. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  349. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  350. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  351. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  352. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  353. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  354. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  355. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  356. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  357. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  358. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  359. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  360. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  361. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  362. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  363. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  364. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  365. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  366. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  367. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  368. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  369. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  370. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  371. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  372. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  373. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  374. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  375. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  376. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  377. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  378. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  379. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  380. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  381. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  382. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  383. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  384. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  385. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  386. data/lib/ruby_llm/providers/xai.rb +15 -5
  387. data/lib/ruby_llm/railtie.rb +7 -16
  388. data/lib/ruby_llm/rerank.rb +105 -0
  389. data/lib/ruby_llm/research_job.rb +241 -0
  390. data/lib/ruby_llm/search_results.rb +68 -0
  391. data/lib/ruby_llm/server_tool_call.rb +73 -0
  392. data/lib/ruby_llm/speech.rb +159 -0
  393. data/lib/ruby_llm/speech_chunk.rb +33 -0
  394. data/lib/ruby_llm/support/deprecator.rb +22 -0
  395. data/lib/ruby_llm/support/inspectable.rb +49 -0
  396. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  397. data/lib/ruby_llm/support/utils.rb +147 -0
  398. data/lib/ruby_llm/thinking.rb +127 -20
  399. data/lib/ruby_llm/tokenization.rb +59 -0
  400. data/lib/ruby_llm/tokens.rb +103 -33
  401. data/lib/ruby_llm/tool.rb +266 -91
  402. data/lib/ruby_llm/tool_call.rb +36 -3
  403. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  404. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  405. data/lib/ruby_llm/transcription.rb +138 -13
  406. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  407. data/lib/ruby_llm/transport/connection.rb +193 -0
  408. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  409. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  410. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  411. data/lib/ruby_llm/uploaded_file.rb +144 -0
  412. data/lib/ruby_llm/version.rb +2 -1
  413. data/lib/ruby_llm/video.rb +136 -0
  414. data/lib/ruby_llm/video_job.rb +150 -0
  415. data/lib/ruby_llm/workflow.rb +91 -0
  416. data/lib/ruby_llm.rb +380 -6
  417. data/lib/tasks/ruby_llm.rake +21 -16
  418. data/skills/rubyllm/SKILL.md +81 -0
  419. data/skills/rubyllm/agents/openai.yaml +4 -0
  420. metadata +344 -97
  421. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  422. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  423. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  424. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  425. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  426. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  427. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  428. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  429. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  430. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  431. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  432. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  433. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  434. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  435. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  436. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  437. data/lib/ruby_llm/aliases.rb +0 -41
  438. data/lib/ruby_llm/connection.rb +0 -159
  439. data/lib/ruby_llm/content.rb +0 -91
  440. data/lib/ruby_llm/deprecator.rb +0 -24
  441. data/lib/ruby_llm/error_middleware.rb +0 -81
  442. data/lib/ruby_llm/instrumentation.rb +0 -36
  443. data/lib/ruby_llm/mime_type.rb +0 -96
  444. data/lib/ruby_llm/model/info.rb +0 -164
  445. data/lib/ruby_llm/model_registry.rb +0 -39
  446. data/lib/ruby_llm/models_schema.json +0 -171
  447. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  448. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  449. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  450. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  451. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  452. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  453. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  454. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  455. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  456. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  457. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  458. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  459. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  460. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  461. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  462. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  463. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  464. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  465. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  466. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  467. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  468. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  469. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  470. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  471. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  472. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  473. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  474. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  475. data/lib/ruby_llm/streaming.rb +0 -179
  476. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  477. data/lib/ruby_llm/utils.rb +0 -130
  478. data/lib/tasks/models.rake +0 -593
  479. data/lib/tasks/release.rake +0 -94
  480. data/lib/tasks/vcr.rake +0 -124
@@ -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,63 @@
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
+ unless valid_rerank_index?(index, documents)
42
+ raise Error, 'Rerank endpoint returned an invalid document index'
43
+ end
44
+
45
+ RubyLLM::Rerank::Result.new(
46
+ index: index,
47
+ document: rerank_document(result['document']) || documents[index],
48
+ score: result['relevance_score']
49
+ )
50
+ end
51
+ end
52
+
53
+ def rerank_document(document)
54
+ document.is_a?(Hash) ? document['text'] : document
55
+ end
56
+
57
+ def valid_rerank_index?(index, documents)
58
+ index.is_a?(Integer) && index.between?(0, documents.length - 1)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ # The OpenAI Chat Completions API — the lingua franca of LLM APIs.
6
+ class ChatCompletions < Protocol
7
+ include ChatCompletions::Chat
8
+ include ChatCompletions::Embeddings
9
+ include ChatCompletions::Models
10
+ include ChatCompletions::Moderation
11
+ include ChatCompletions::Streaming
12
+ include ChatCompletions::Tools
13
+ include ChatCompletions::Images
14
+ include ChatCompletions::Media
15
+ include ChatCompletions::Speech
16
+ include ChatCompletions::Transcription
17
+
18
+ public :render_transcription_options
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Protocols
5
+ class Cohere
6
+ module BatchRequests # :nodoc: all
7
+ CHAT_FIELDS = %w[
8
+ messages tools temperature p frequency_penalty presence_penalty reasoning thinking_budget
9
+ return_prompt logprobs max_tokens max_input_tokens k seed
10
+ ].freeze
11
+ EMBEDDING_FIELDS = %w[texts images input_type inputs max_tokens output_dimension embedding_types
12
+ truncate].freeze
13
+ private_constant :CHAT_FIELDS, :EMBEDDING_FIELDS
14
+
15
+ def batch_dataset_type(requests)
16
+ types = requests.map do |request|
17
+ body = request.fetch(:payload)
18
+ body.key?(:messages) || body.key?('messages') ? 'batch-chat-v2-input' : 'batch-embed-v2-input'
19
+ end.uniq
20
+ raise ArgumentError, 'Cohere batches cannot mix chat and embeddings' unless types.one?
21
+
22
+ types.first
23
+ end
24
+
25
+ def render_batch_request(request, type:)
26
+ body = JSON.parse(JSON.generate(batch_payload(request, except: :model)))
27
+ if type == 'batch-embed-v2-input' && body['output_dimension']
28
+ raise ArgumentError,
29
+ 'Cohere batch datasets currently reject dimensions; omit dimensions to use the model default'
30
+ end
31
+
32
+ render_batch_chat(body) if type == 'batch-chat-v2-input'
33
+ allowed = type == 'batch-chat-v2-input' ? CHAT_FIELDS : EMBEDDING_FIELDS
34
+ unsupported = body.keys - allowed
35
+ unless unsupported.empty?
36
+ raise ArgumentError, "Cohere batches do not support these request options: #{unsupported.join(', ')}"
37
+ end
38
+
39
+ custom_id = request.fetch(:custom_id)
40
+ custom_id = "#{custom_id}:array" if request[:text].is_a?(Array)
41
+ { custom_id:, body: }
42
+ end
43
+
44
+ def render_batch_chat(body)
45
+ if (thinking = body.delete('thinking'))
46
+ body['reasoning'] = thinking['type'] != 'disabled'
47
+ body['thinking_budget'] = thinking['token_budget'] if thinking['token_budget']
48
+ end
49
+ Array(body['messages']).each { |message| render_batch_message(message) }
50
+ Array(body['tools']).each do |tool|
51
+ function = tool.fetch('function')
52
+ parameters = function['parameters']
53
+ function['parameters'] = JSON.generate(parameters) unless parameters.is_a?(String)
54
+ end
55
+ end
56
+
57
+ def render_batch_message(message)
58
+ content = message['content']
59
+ content = [{ 'type' => 'text', 'text' => content }] if content.is_a?(String)
60
+ message['content'] = content&.map { |part| render_batch_content(part) }
61
+ end
62
+
63
+ def render_batch_content(part)
64
+ unless %w[text thinking image_url].include?(part['type'])
65
+ raise ArgumentError, "Cohere batches do not support #{part['type']} content"
66
+ end
67
+
68
+ part = part.dup
69
+ part['image_url'] = part['image_url'].fetch('url') if part['image_url'].is_a?(Hash)
70
+ part
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end