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,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'open3'
5
+ require 'stringio'
6
+ require 'generators/ruby_llm/provider/scaffold'
7
+
8
+ module RubyLLM
9
+ module Generators
10
+ module Provider # :nodoc: all
11
+ # Command-line interface for public provider gem generation.
12
+ class CLI # :nodoc: all
13
+ ACTION_LABELS = { written: 'create', updated: 'update', skipped: 'skip' }.freeze
14
+
15
+ class HelpRequested < StandardError; end
16
+
17
+ def self.run(argv, out: $stdout, err: $stderr)
18
+ new(argv, out:, err:).run
19
+ end
20
+
21
+ def self.parse_options(argv, mode:)
22
+ new(argv, out: StringIO.new, err: StringIO.new).parse_provider_options(mode:)
23
+ end
24
+
25
+ def initialize(argv, out:, err:)
26
+ @argv = argv.dup
27
+ @out = out
28
+ @err = err
29
+ end
30
+
31
+ def run
32
+ command = @argv.shift
33
+
34
+ case command
35
+ when 'provider-gem'
36
+ generate_provider_gem
37
+ when nil, 'help', '-h', '--help'
38
+ @out.puts help
39
+ 0
40
+ else
41
+ @err.puts "Unknown command: #{command}"
42
+ @err.puts
43
+ @err.puts help
44
+ 1
45
+ end
46
+ rescue HelpRequested => e
47
+ @out.puts e.message
48
+ 0
49
+ rescue ArgumentError, OptionParser::ParseError => e
50
+ @err.puts e.message
51
+ 1
52
+ end
53
+
54
+ def parse_provider_options(mode:)
55
+ options = { mode: mode }
56
+ options[:destination] = Dir.pwd if mode == 'core'
57
+ parser = OptionParser.new
58
+ parser.banner = "Usage: #{command_for(mode)} [options]"
59
+ parser.on('--destination PATH', 'Exact directory to write into') { |value| options[:destination] = value }
60
+ parser.on('--api-base URL', 'Provider API base URL') { |value| options[:api_base] = value }
61
+ parser.on('--api-key-env NAME', 'Environment variable for the provider API key') do |value|
62
+ options[:api_key_env] = value
63
+ end
64
+ parser.on('--api-base-env NAME', 'Environment variable for the provider API base') do |value|
65
+ options[:api_base_env] = value
66
+ end
67
+ parser.on('--dialect NAME', "Wire dialect: #{Scaffold::SUPPORTED_DIALECTS.join(', ')}") do |value|
68
+ options[:dialect] = value
69
+ end
70
+ if mode == 'core'
71
+ parser.on('--models-dev-provider NAME', 'models.dev provider key for first-party core providers') do |value|
72
+ options[:models_dev_provider] = value
73
+ end
74
+ end
75
+ parser.on('--[no-]dynamic-models', 'Allow model ids that are not in the registry') do |value|
76
+ options[:dynamic_models] = value
77
+ end
78
+ if mode == 'gem'
79
+ parser.on('--gem-name NAME', 'RubyGems package name') { |value| options[:gem_name] = value }
80
+ parser.on('--github-owner OWNER', 'GitHub owner or organization for generated metadata') do |value|
81
+ options[:github_owner] = value
82
+ end
83
+ parser.on('--skip-bundle', 'Do not run bundle install') { options[:skip_bundle] = true }
84
+ end
85
+ parser.on('--force', 'Overwrite existing generated files') { options[:force] = true }
86
+ parser.on('-h', '--help', 'Print help') { raise HelpRequested, parser.to_s }
87
+
88
+ parser.parse!(@argv)
89
+ name = @argv.shift
90
+ raise ArgumentError, parser.to_s unless name
91
+ raise ArgumentError, "Unexpected arguments: #{@argv.join(' ')}" if @argv.any?
92
+
93
+ [name, options]
94
+ end
95
+
96
+ private
97
+
98
+ def generate_provider_gem
99
+ name, options = parse_provider_options(mode: 'gem')
100
+ skip_bundle = options.delete(:skip_bundle)
101
+ scaffold = Scaffold.new(name, **options)
102
+ destination_existed = Dir.exist?(scaffold.destination)
103
+ result = scaffold.generate
104
+
105
+ report_file_actions(result, scaffold.destination, destination_existed:)
106
+ return 1 unless initialize_git_repository(scaffold.destination)
107
+ return 1 unless skip_bundle || install_dependencies(scaffold.destination)
108
+
109
+ 0
110
+ end
111
+
112
+ def report_file_actions(result, destination, destination_existed:)
113
+ report_action(destination_existed ? 'exist' : 'create', destination)
114
+ result.actions.each do |action, path|
115
+ report_action(ACTION_LABELS.fetch(action), path)
116
+ end
117
+ end
118
+
119
+ def report_action(action, path)
120
+ @out.puts format('%<action>12s %<path>s', action:, path:)
121
+ end
122
+
123
+ def initialize_git_repository(destination)
124
+ command = git_init_command
125
+ report_action('run', command.join(' '))
126
+ stdout, stderr, status = Open3.capture3(*command, chdir: destination)
127
+ @out.print(stdout)
128
+ @err.print(stderr)
129
+ @err.puts 'Git repository initialization failed.' unless status.success?
130
+ status.success?
131
+ rescue Errno::ENOENT => e
132
+ @err.puts "Git repository initialization failed: #{e.message}"
133
+ false
134
+ end
135
+
136
+ def install_dependencies(destination)
137
+ command = %w[bundle install]
138
+ report_action('run', command.join(' '))
139
+ stdout, stderr, status = Open3.capture3(*command, chdir: destination)
140
+ @out.print(stdout)
141
+ @err.print(stderr)
142
+ @err.puts 'Bundle install failed.' unless status.success?
143
+ status.success?
144
+ rescue Errno::ENOENT => e
145
+ @err.puts "Bundle install failed: #{e.message}"
146
+ false
147
+ end
148
+
149
+ def git_init_command
150
+ _output, status = Open3.capture2e('git', 'config', '--get', 'init.defaultBranch')
151
+ status.success? ? %w[git init] : %w[git init -b main]
152
+ rescue Errno::ENOENT
153
+ %w[git init]
154
+ end
155
+
156
+ def help
157
+ <<~HELP
158
+ Usage:
159
+ ruby_llm provider-gem NAME [options]
160
+
161
+ Generates a standalone RubyLLM provider gem with provider code, specs,
162
+ CI, release workflow, ArchSpec, Flay, RuboCop, and live VCR specs.
163
+ Creates ruby_llm-providers-NAME in the current directory by default;
164
+ --destination selects an exact directory. Initializes the target as a
165
+ Git repository and runs bundle install.
166
+ HELP
167
+ end
168
+
169
+ def command_for(mode)
170
+ mode == 'core' ? 'script/generate-provider NAME' : 'ruby_llm provider-gem NAME'
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'ruby_llm/support/utils'
7
+
8
+ module RubyLLM
9
+ module Generators
10
+ module Provider # :nodoc: all
11
+ # Generates first-party providers and standalone provider gems.
12
+ class Scaffold # :nodoc: all
13
+ SUPPORTED_MODES = %w[core gem].freeze
14
+ SUPPORTED_DIALECTS = %w[chat_completions responses anthropic gemini converse ollama].freeze
15
+ TEMPLATE_ROOT = File.expand_path('templates', __dir__)
16
+
17
+ Result = Struct.new(:written, :updated, :skipped, :actions, keyword_init: true) do
18
+ def changed
19
+ written + updated
20
+ end
21
+ end
22
+
23
+ attr_reader :name, :mode, :destination, :api_base, :api_key_env, :api_base_env,
24
+ :dialect, :models_dev_provider, :gem_name, :github_owner
25
+
26
+ def initialize(name, **options)
27
+ @name = normalize_provider_name(name)
28
+ @mode = normalize_option(options.fetch(:mode, :core), SUPPORTED_MODES, 'mode')
29
+ @dialect = normalize_option(options.fetch(:dialect, :chat_completions), SUPPORTED_DIALECTS, 'dialect')
30
+ @api_base = options[:api_base] || 'https://api.example.com/v1'
31
+ @api_key_env = options[:api_key_env] || "#{env_prefix}_API_KEY"
32
+ @api_base_env = options[:api_base_env] || "#{env_prefix}_API_BASE"
33
+ @models_dev_provider = blank_to_nil(options[:models_dev_provider])
34
+ @dynamic_models = dynamic_models_option?(options)
35
+ @gem_name = normalize_gem_name(options[:gem_name] || "ruby_llm-providers-#{slug.tr('_', '-')}")
36
+ @destination = File.expand_path(options[:destination] || default_destination)
37
+ @github_owner = options[:github_owner] || 'your-github-org'
38
+ @force = options.fetch(:force, false)
39
+ @result = Result.new(written: [], updated: [], skipped: [], actions: [])
40
+ end
41
+
42
+ def generate
43
+ mode == 'core' ? generate_core_provider : generate_provider_gem
44
+ @result
45
+ end
46
+
47
+ def class_name
48
+ @class_name ||= classify(name)
49
+ end
50
+
51
+ def slug
52
+ @slug ||= RubyLLM::Support::Utils.underscore(class_name).tr('-', '_')
53
+ end
54
+
55
+ def env_prefix
56
+ slug.upcase
57
+ end
58
+
59
+ def api_key_config
60
+ "#{slug}_api_key"
61
+ end
62
+
63
+ def api_base_config
64
+ "#{slug}_api_base"
65
+ end
66
+
67
+ def dynamic_models?
68
+ !!@dynamic_models
69
+ end
70
+
71
+ def protocol_name
72
+ dialect == 'ollama' ? 'chat_completions' : dialect
73
+ end
74
+
75
+ def protocol_class_name
76
+ {
77
+ 'chat_completions' => 'ChatCompletions',
78
+ 'responses' => 'Responses',
79
+ 'anthropic' => 'Anthropic',
80
+ 'gemini' => 'Gemini',
81
+ 'converse' => 'Converse',
82
+ 'ollama' => 'ChatCompletions'
83
+ }.fetch(dialect)
84
+ end
85
+
86
+ def protocol_superclass
87
+ {
88
+ 'chat_completions' => 'Protocols::ChatCompletions',
89
+ 'responses' => 'Protocols::Responses',
90
+ 'anthropic' => 'Protocols::Anthropic',
91
+ 'gemini' => 'Protocols::Gemini',
92
+ 'converse' => 'Protocols::Converse',
93
+ 'ollama' => 'Ollama::ChatCompletions'
94
+ }.fetch(dialect)
95
+ end
96
+
97
+ def provider_description
98
+ "#{class_name} API integration."
99
+ end
100
+
101
+ def require_path
102
+ "ruby_llm/providers/#{slug}"
103
+ end
104
+
105
+ private
106
+
107
+ def generate_core_provider
108
+ write_template 'core/provider.rb.erb', "lib/ruby_llm/providers/#{slug}.rb"
109
+ write_template 'core/provider_spec.rb.erb', "spec/ruby_llm/providers/#{slug}_spec.rb"
110
+
111
+ update_core_registration
112
+ update_core_env_example
113
+ update_core_spec_configuration
114
+ update_core_vcr_configuration
115
+ update_core_models_dev_map if models_dev_provider
116
+ end
117
+
118
+ def generate_provider_gem
119
+ write_template 'gem/gitignore.erb', '.gitignore'
120
+ write_template 'gem/rspec.erb', '.rspec'
121
+ write_template 'gem/rubocop.yml.erb', '.rubocop.yml'
122
+ write_template 'gem/overcommit.yml.erb', '.overcommit.yml'
123
+ write_template 'gem/flayignore.erb', '.flayignore'
124
+ write_template 'gem/env.erb', '.env'
125
+ write_template 'gem/gemfile.erb', 'Gemfile'
126
+ write_template 'gem/rakefile.erb', 'Rakefile'
127
+ write_template 'gem/archspec.rb.erb', 'Archspec.rb'
128
+ write_template 'gem/readme.md.erb', 'README.md'
129
+ write_template 'gem/license.erb', 'LICENSE'
130
+ write_template 'gem/gemspec.erb', "#{gem_name}.gemspec"
131
+ write_template 'gem/ci.yml.erb', '.github/workflows/ci.yml'
132
+ write_template 'gem/release.yml.erb', '.github/workflows/release.yml'
133
+ write_template 'gem/gitleaks.yml.erb', '.github/workflows/gitleaks.yml'
134
+ write_template 'gem/bin/setup.erb', 'bin/setup', executable: true
135
+ write_template 'gem/bin/console.erb', 'bin/console', executable: true
136
+ write_template 'gem/provider.rb.erb', "lib/ruby_llm/providers/#{slug}.rb"
137
+ write_template 'gem/spec_helper.rb.erb', 'spec/spec_helper.rb'
138
+ write_template 'gem/rubyllm_configuration.rb.erb', 'spec/support/rubyllm_configuration.rb'
139
+ write_template 'gem/vcr_configuration.rb.erb', 'spec/support/vcr_configuration.rb'
140
+ write_template 'gem/models.rb.erb', 'spec/support/models.rb'
141
+ write_template 'gem/provider_spec.rb.erb', "spec/ruby_llm/providers/#{slug}_spec.rb"
142
+ write_template 'gem/chat_spec.rb.erb', 'spec/ruby_llm/chat_spec.rb'
143
+ write_template 'gem/chat_streaming_spec.rb.erb', 'spec/ruby_llm/chat_streaming_spec.rb'
144
+ write_template 'gem/chat_tools_spec.rb.erb', 'spec/ruby_llm/chat_tools_spec.rb'
145
+ write_template 'gem/chat_schema_spec.rb.erb', 'spec/ruby_llm/chat_schema_spec.rb'
146
+ write_template 'gem/embedding_spec.rb.erb', 'spec/ruby_llm/embedding_spec.rb'
147
+ write_template 'gem/image_spec.rb.erb', 'spec/ruby_llm/image_spec.rb'
148
+ write_template 'gem/speech_spec.rb.erb', 'spec/ruby_llm/speech_spec.rb'
149
+ write_template 'gem/video_spec.rb.erb', 'spec/ruby_llm/video_spec.rb'
150
+ write_template 'gem/moderation_spec.rb.erb', 'spec/ruby_llm/moderation_spec.rb'
151
+ write_template 'gem/rerank_spec.rb.erb', 'spec/ruby_llm/rerank_spec.rb'
152
+ write_template 'gem/models_spec.rb.erb', 'spec/ruby_llm/models_spec.rb'
153
+ write_template 'gem/fixtures_gitkeep.erb', 'spec/fixtures/vcr_cassettes/.gitkeep'
154
+ end
155
+
156
+ def update_core_registration
157
+ path = file_path('lib/ruby_llm.rb')
158
+ return unless File.exist?(path)
159
+
160
+ insert_sorted_line(path, " '#{slug}' => '#{class_name}',", /^\s+'[^']+' => '[^']+',?$/)
161
+ insert_sorted_line(
162
+ path,
163
+ "RubyLLM::Provider.register :#{slug}, RubyLLM::Providers::#{class_name}",
164
+ /^RubyLLM::Provider\.register /
165
+ )
166
+ end
167
+
168
+ def update_core_env_example
169
+ path = file_path('.env.example')
170
+ return unless File.exist?(path)
171
+
172
+ append_unique_line(path, "#{api_key_env}=$(op read \"op://RubyLLM/#{class_name}/credential\")")
173
+ append_unique_line(path, "#{api_base_env}=#{api_base}")
174
+ end
175
+
176
+ def update_core_spec_configuration
177
+ path = file_path('spec/support/rubyllm_configuration.rb')
178
+ return unless File.exist?(path)
179
+
180
+ insert_sorted_line(path, " config.#{api_base_config} = ENV.fetch('#{api_base_env}', '#{api_base}')",
181
+ /^\s+config\.[a-z0-9_]+_api_base =/)
182
+ insert_sorted_line(path, " config.#{api_key_config} = ENV.fetch('#{api_key_env}', 'test')",
183
+ /^\s+config\.[a-z0-9_]+_api_key =/)
184
+ end
185
+
186
+ def update_core_vcr_configuration
187
+ path = file_path('spec/support/vcr_configuration.rb')
188
+ return unless File.exist?(path)
189
+
190
+ line = " config.filter_sensitive_data('<#{api_key_env}>') { " \
191
+ "ENV.fetch('#{api_key_env}', nil) }"
192
+ insert_sorted_line(path, line, /^\s+config\.filter_sensitive_data\('<[A-Z0-9_]+>'\)/)
193
+ end
194
+
195
+ def update_core_models_dev_map
196
+ path = file_path('lib/ruby_llm/models.rb')
197
+ return unless File.exist?(path)
198
+
199
+ insert_before_first(path, " '#{models_dev_provider}' => '#{slug}',", /^ \}\.freeze$/)
200
+ end
201
+
202
+ def write_template(template, relative_path, executable: false)
203
+ content = ERB.new(File.read(template_path(template)), trim_mode: '-').result(binding)
204
+ write_file(relative_path, content, executable:)
205
+ end
206
+
207
+ def write_file(relative_path, content, executable: false)
208
+ path = file_path(relative_path)
209
+ existed = File.exist?(path)
210
+ if existed && !@force
211
+ record(:skipped, relative_path)
212
+ return
213
+ end
214
+
215
+ FileUtils.mkdir_p(File.dirname(path))
216
+ File.write(path, content)
217
+ FileUtils.chmod('+x', path) if executable
218
+ record(existed ? :updated : :written, relative_path)
219
+ end
220
+
221
+ def append_unique_line(path, line)
222
+ content = File.exist?(path) ? File.read(path) : ''
223
+ return record(:skipped, relative_path(path)) if content.include?(line)
224
+
225
+ FileUtils.mkdir_p(File.dirname(path))
226
+ File.write(path, "#{content.chomp}\n#{line}\n")
227
+ record(:updated, relative_path(path))
228
+ end
229
+
230
+ def insert_sorted_line(path, line, matcher)
231
+ content = File.read(path)
232
+ return record(:skipped, relative_path(path)) if content.include?(line)
233
+
234
+ lines = content.lines
235
+ indexes = lines.each_index.select { |index| lines[index].match?(matcher) }
236
+ insert_at = indexes.find { |index| line < lines[index].chomp } || indexes.last&.+(1) || lines.length
237
+ lines.insert(insert_at, "#{line}\n")
238
+ File.write(path, lines.join)
239
+ record(:updated, relative_path(path))
240
+ end
241
+
242
+ def insert_before_first(path, line, matcher)
243
+ content = File.read(path)
244
+ return record(:skipped, relative_path(path)) if content.include?(line)
245
+
246
+ lines = content.lines
247
+ insert_at = lines.index { |candidate| candidate.match?(matcher) } || lines.length
248
+ lines.insert(insert_at, "#{line}\n")
249
+ File.write(path, lines.join)
250
+ record(:updated, relative_path(path))
251
+ end
252
+
253
+ def record(action, path)
254
+ @result.public_send(action) << path
255
+ @result.actions << [action, path]
256
+ path
257
+ end
258
+
259
+ def file_path(relative_path)
260
+ File.join(destination, relative_path)
261
+ end
262
+
263
+ def relative_path(path)
264
+ Pathname.new(path).relative_path_from(Pathname.new(destination)).to_s
265
+ end
266
+
267
+ def template_path(relative_path)
268
+ File.join(TEMPLATE_ROOT, relative_path)
269
+ end
270
+
271
+ def default_destination
272
+ mode == 'gem' ? File.join(Dir.pwd, gem_name) : Dir.pwd
273
+ end
274
+
275
+ def normalize_option(value, allowed, name)
276
+ normalized = value.to_s.tr('-', '_')
277
+ return normalized if allowed.include?(normalized)
278
+
279
+ raise ArgumentError, "unsupported #{name}: #{value.inspect}. Expected one of: #{allowed.join(', ')}"
280
+ end
281
+
282
+ def normalize_gem_name(value)
283
+ value = value.to_s
284
+ return value if value.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]*\z/)
285
+
286
+ raise ArgumentError, 'gem name must contain only letters, numbers, ., - or _'
287
+ end
288
+
289
+ def normalize_provider_name(value)
290
+ value = value.to_s.strip
291
+ return value if value.match?(/\A[A-Za-z][A-Za-z0-9_-]*\z/)
292
+
293
+ raise ArgumentError, 'provider name must start with a letter and contain only letters, numbers, - or _'
294
+ end
295
+
296
+ def classify(value)
297
+ words = value.to_s.tr('-', '_').split('_').reject(&:empty?)
298
+ return value if value.match?(/\A[A-Z][A-Za-z0-9]*\z/)
299
+
300
+ words.map { |word| word[0].upcase + word[1..] }.join
301
+ end
302
+
303
+ def blank_to_nil(value)
304
+ value = value.to_s.strip
305
+ value.empty? ? nil : value
306
+ end
307
+
308
+ def truthy?(value)
309
+ case value
310
+ when true, 'true', '1', 1, 'yes', 'on' then true
311
+ else false
312
+ end
313
+ end
314
+
315
+ def dynamic_models_option?(options)
316
+ return truthy?(options[:dynamic_models]) if options.key?(:dynamic_models)
317
+
318
+ false
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # <%= provider_description %>
6
+ class <%= class_name %> < Provider
7
+ class <%= protocol_class_name %> < <%= protocol_superclass %>
8
+ end
9
+
10
+ protocol :<%= protocol_name %>, <%= protocol_class_name %>
11
+
12
+ def api_base
13
+ @config.<%= api_base_config %> || '<%= api_base %>'
14
+ end
15
+
16
+ def headers
17
+ { 'Authorization' => "Bearer #{@config.<%= api_key_config %>}" }
18
+ end
19
+
20
+ class << self
21
+ def configuration_options
22
+ %i[<%= api_key_config %> <%= api_base_config %>]
23
+ end
24
+
25
+ def configuration_requirements
26
+ %i[<%= api_key_config %>]
27
+ end
28
+ <% if dynamic_models? -%>
29
+
30
+ def assume_models_exist?
31
+ true
32
+ end
33
+ <% end -%>
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyLLM::Providers::<%= class_name %> do
6
+ subject(:provider) { described_class.new(config) }
7
+
8
+ let(:config) do
9
+ RubyLLM::Configuration.new.tap do |provider_config|
10
+ provider_config.<%= api_key_config %> = 'test-key'
11
+ provider_config.<%= api_base_config %> = 'https://example.test/v1'
12
+ end
13
+ end
14
+
15
+ it 'registers a default protocol' do
16
+ expect(described_class.protocols).to include(<%= "#{protocol_name}:" %> described_class::<%= protocol_class_name %>)
17
+ end
18
+
19
+ it 'declares provider configuration' do
20
+ expect(described_class.configuration_options).to eq(%i[<%= api_key_config %> <%= api_base_config %>])
21
+ expect(described_class.configuration_requirements).to eq(%i[<%= api_key_config %>])
22
+ end
23
+
24
+ it 'uses configured API base and bearer token' do
25
+ expect(provider.api_base).to eq('https://example.test/v1')
26
+ expect(provider.headers).to eq('Authorization' => 'Bearer test-key')
27
+ end
28
+ <% if dynamic_models? -%>
29
+
30
+ it 'allows provider model ids missing from the registry' do
31
+ expect(described_class.assume_models_exist?).to be(true)
32
+ end
33
+ <% end -%>
34
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'lib/**/*.rb'
4
+
5
+ component :provider,
6
+ in: %w[
7
+ lib/ruby_llm/providers/<%= slug %>.rb
8
+ lib/ruby_llm/providers/<%= slug %>/**/*.rb
9
+ ],
10
+ namespace: 'RubyLLM::Providers::<%= class_name %>'
11
+
12
+ provider.cannot_reference_constants 'RSpec', 'WebMock', 'VCR'
13
+
14
+ preset :ruby_conventions
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'dotenv/load'
6
+ require '<%= require_path %>'
7
+
8
+ require 'irb'
9
+
10
+ RubyLLM.configure do |config|
11
+ config.<%= api_key_config %> = ENV.fetch('<%= api_key_env %>', nil)
12
+ config.<%= api_base_config %> = ENV.fetch('<%= api_base_env %>', '<%= api_base %>')
13
+ end
14
+
15
+ IRB.start(__FILE__)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyLLM::Chat, :live do
6
+ include_context 'with configured RubyLLM'
7
+
8
+ let(:person_schema) do
9
+ {
10
+ type: 'object',
11
+ properties: {
12
+ name: { type: 'string' },
13
+ age: { type: 'integer' }
14
+ },
15
+ required: %w[name age],
16
+ additionalProperties: false
17
+ }
18
+ end
19
+
20
+ each_model(STRUCTURED_OUTPUT_MODELS) do |provider, model|
21
+ it "#{provider}/#{model} returns structured output" do
22
+ response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
23
+ .with_schema(person_schema)
24
+ .ask('Generate a person named John who is 30 years old')
25
+
26
+ expect(response.content).to be_a(String)
27
+ expect(response.parsed).to include('name' => 'John', 'age' => 30)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RubyLLM::Chat, :live do
6
+ include_context 'with configured RubyLLM'
7
+
8
+ each_model(CHAT_MODELS) do |provider, model|
9
+ it "#{provider}/#{model} can have a basic conversation" do
10
+ response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true).ask("What's 2 + 2?")
11
+
12
+ expect(response.content).to include('4')
13
+ expect(response.role).to eq(:assistant)
14
+ expect(response.tokens.input.to_i).to be_positive
15
+ expect(response.tokens.output.to_i).to be_positive
16
+ end
17
+
18
+ it "#{provider}/#{model} returns the raw response" do
19
+ response = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
20
+ .ask('What is the capital of France?')
21
+
22
+ expect(response.raw.status).to eq(200)
23
+ expect(response.raw.headers).not_to be_empty
24
+ expect(response.raw.body).not_to be_empty
25
+ expect(response.raw.env.request_body).not_to be_empty
26
+ end
27
+
28
+ it "#{provider}/#{model} can handle a multi-turn conversation" do
29
+ chat = RubyLLM.chat(model: model, provider: provider, assume_model_exists: true)
30
+
31
+ expect(chat.ask('Who created the programming language Ruby?').content).to match(/Matz|Matsumoto/i)
32
+ expect(chat.ask('What year was Ruby first released?').content).to include('199')
33
+ end
34
+ end
35
+ end