ruby_llm 2.0.0 → 2.1.0

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 (284) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -8
  3. data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
  5. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
  6. data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
  7. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
  8. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
  9. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
  10. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
  11. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
  13. data/lib/ruby_llm/accounting/usage.rb +86 -49
  14. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  15. data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
  16. data/lib/ruby_llm/active_record/batch.rb +1 -1
  17. data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
  18. data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
  19. data/lib/ruby_llm/active_record/message_methods.rb +32 -7
  20. data/lib/ruby_llm/active_record/model.rb +1 -1
  21. data/lib/ruby_llm/active_record/provider_file.rb +64 -0
  22. data/lib/ruby_llm/active_record/record.rb +19 -0
  23. data/lib/ruby_llm/active_record/tool_call.rb +1 -1
  24. data/lib/ruby_llm/active_record/usage.rb +53 -3
  25. data/lib/ruby_llm/agent.rb +198 -33
  26. data/lib/ruby_llm/aliases.json +66 -8
  27. data/lib/ruby_llm/attachment.rb +54 -10
  28. data/lib/ruby_llm/batch.rb +26 -3
  29. data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
  30. data/lib/ruby_llm/chat/tool_search.rb +26 -0
  31. data/lib/ruby_llm/chat.rb +488 -82
  32. data/lib/ruby_llm/choice.rb +38 -0
  33. data/lib/ruby_llm/configuration.rb +36 -11
  34. data/lib/ruby_llm/context.rb +45 -32
  35. data/lib/ruby_llm/cost.rb +42 -1
  36. data/lib/ruby_llm/embedding.rb +9 -2
  37. data/lib/ruby_llm/error.rb +34 -2
  38. data/lib/ruby_llm/evaluation/assertions.rb +40 -0
  39. data/lib/ruby_llm/evaluation/case.rb +52 -0
  40. data/lib/ruby_llm/evaluation/dataset.rb +65 -0
  41. data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
  42. data/lib/ruby_llm/evaluation/evidence.rb +125 -0
  43. data/lib/ruby_llm/evaluation/minitest.rb +26 -0
  44. data/lib/ruby_llm/evaluation/report.rb +103 -0
  45. data/lib/ruby_llm/evaluation/result.rb +75 -0
  46. data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
  47. data/lib/ruby_llm/evaluation/rspec.rb +23 -0
  48. data/lib/ruby_llm/evaluation/runner.rb +93 -0
  49. data/lib/ruby_llm/evaluation/trial.rb +101 -0
  50. data/lib/ruby_llm/evaluation.rb +289 -0
  51. data/lib/ruby_llm/image.rb +12 -4
  52. data/lib/ruby_llm/judge/builder.rb +51 -0
  53. data/lib/ruby_llm/judge/data.rb +43 -0
  54. data/lib/ruby_llm/judge/question.rb +101 -0
  55. data/lib/ruby_llm/judge.rb +179 -0
  56. data/lib/ruby_llm/judgment.rb +125 -0
  57. data/lib/ruby_llm/mcp/apps.rb +25 -0
  58. data/lib/ruby_llm/mcp/client.rb +213 -0
  59. data/lib/ruby_llm/mcp/collection.rb +56 -0
  60. data/lib/ruby_llm/mcp/content.rb +44 -0
  61. data/lib/ruby_llm/mcp/error.rb +27 -0
  62. data/lib/ruby_llm/mcp/http.rb +379 -0
  63. data/lib/ruby_llm/mcp/input_request.rb +118 -0
  64. data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
  65. data/lib/ruby_llm/mcp/listener.rb +168 -0
  66. data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
  67. data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
  68. data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
  69. data/lib/ruby_llm/mcp/oauth.rb +653 -0
  70. data/lib/ruby_llm/mcp/param_headers.rb +83 -0
  71. data/lib/ruby_llm/mcp/prompt.rb +71 -0
  72. data/lib/ruby_llm/mcp/resource.rb +84 -0
  73. data/lib/ruby_llm/mcp/resource_template.rb +99 -0
  74. data/lib/ruby_llm/mcp/result.rb +76 -0
  75. data/lib/ruby_llm/mcp/session_expired.rb +10 -0
  76. data/lib/ruby_llm/mcp/stdio.rb +250 -0
  77. data/lib/ruby_llm/mcp/task.rb +151 -0
  78. data/lib/ruby_llm/mcp/tool.rb +130 -0
  79. data/lib/ruby_llm/mcp.rb +1101 -0
  80. data/lib/ruby_llm/message.rb +41 -6
  81. data/lib/ruby_llm/model.rb +2 -1
  82. data/lib/ruby_llm/models/aliases.rb +5 -2
  83. data/lib/ruby_llm/models/lookup.rb +80 -0
  84. data/lib/ruby_llm/models/registry.rb +2 -2
  85. data/lib/ruby_llm/models/schema.rb +1 -1
  86. data/lib/ruby_llm/models.json +23082 -10158
  87. data/lib/ruby_llm/models.rb +20 -69
  88. data/lib/ruby_llm/moderation.rb +7 -2
  89. data/lib/ruby_llm/ocr.rb +7 -2
  90. data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
  91. data/lib/ruby_llm/open_telemetry.rb +82 -0
  92. data/lib/ruby_llm/probability.rb +30 -0
  93. data/lib/ruby_llm/progress.rb +44 -0
  94. data/lib/ruby_llm/prompt/context.rb +55 -0
  95. data/lib/ruby_llm/prompt.rb +1 -1
  96. data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
  97. data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
  98. data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
  99. data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
  100. data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
  101. data/lib/ruby_llm/protocol/streaming.rb +38 -31
  102. data/lib/ruby_llm/protocol.rb +192 -40
  103. data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
  104. data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
  105. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
  106. data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
  107. data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
  108. data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
  109. data/lib/ruby_llm/protocols/anthropic.rb +20 -1
  110. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
  111. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
  112. data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
  113. data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
  114. data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
  115. data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
  116. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
  117. data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
  118. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
  119. data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
  120. data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
  121. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
  122. data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
  123. data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
  124. data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
  125. data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
  126. data/lib/ruby_llm/protocols/cohere.rb +1 -0
  127. data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
  128. data/lib/ruby_llm/protocols/converse/media.rb +16 -2
  129. data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
  130. data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
  131. data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
  132. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
  133. data/lib/ruby_llm/protocols/converse.rb +3 -5
  134. data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
  135. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
  136. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
  137. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
  138. data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
  139. data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
  140. data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
  141. data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
  142. data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
  143. data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
  144. data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
  145. data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
  146. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
  147. data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
  148. data/lib/ruby_llm/protocols/gemini.rb +1 -0
  149. data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
  150. data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
  151. data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
  152. data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
  153. data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
  154. data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
  155. data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
  156. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  157. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
  158. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
  159. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
  160. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
  161. data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
  162. data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
  163. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
  164. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
  165. data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
  166. data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
  167. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
  168. data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
  169. data/lib/ruby_llm/protocols/openai/files.rb +0 -7
  170. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
  171. data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
  172. data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
  173. data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
  174. data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
  175. data/lib/ruby_llm/protocols/responses/media.rb +11 -7
  176. data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
  177. data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
  178. data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
  179. data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
  180. data/lib/ruby_llm/protocols/responses.rb +1 -0
  181. data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
  182. data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
  183. data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
  184. data/lib/ruby_llm/protocols/system_one.rb +27 -0
  185. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
  186. data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
  187. data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
  188. data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
  189. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
  190. data/lib/ruby_llm/provider.rb +87 -15
  191. data/lib/ruby_llm/providers/anthropic.rb +23 -0
  192. data/lib/ruby_llm/providers/azure/chat.rb +4 -0
  193. data/lib/ruby_llm/providers/azure/images.rb +2 -2
  194. data/lib/ruby_llm/providers/azure/media.rb +8 -3
  195. data/lib/ruby_llm/providers/azure/videos.rb +7 -2
  196. data/lib/ruby_llm/providers/azure.rb +24 -4
  197. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
  198. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
  199. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
  200. data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
  201. data/lib/ruby_llm/providers/bedrock.rb +38 -6
  202. data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
  203. data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
  204. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  205. data/lib/ruby_llm/providers/gemini.rb +4 -0
  206. data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
  207. data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
  208. data/lib/ruby_llm/providers/hetzner.rb +41 -0
  209. data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
  210. data/lib/ruby_llm/providers/mistral/media.rb +8 -4
  211. data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
  212. data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
  213. data/lib/ruby_llm/providers/ollama/media.rb +6 -2
  214. data/lib/ruby_llm/providers/ollama/models.rb +2 -0
  215. data/lib/ruby_llm/providers/ollama.rb +11 -0
  216. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
  217. data/lib/ruby_llm/providers/openai.rb +14 -0
  218. data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
  219. data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
  220. data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
  221. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
  222. data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
  223. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  224. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  225. data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
  226. data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
  227. data/lib/ruby_llm/providers/perplexity.rb +11 -0
  228. data/lib/ruby_llm/providers/typesafe.rb +32 -0
  229. data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
  230. data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
  231. data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
  232. data/lib/ruby_llm/providers/vertexai.rb +27 -28
  233. data/lib/ruby_llm/providers/xai/chat.rb +6 -0
  234. data/lib/ruby_llm/providers/xai/images.rb +9 -10
  235. data/lib/ruby_llm/providers/xai/responses.rb +10 -8
  236. data/lib/ruby_llm/providers/xai/videos.rb +11 -9
  237. data/lib/ruby_llm/providers/xai.rb +4 -0
  238. data/lib/ruby_llm/railtie.rb +11 -0
  239. data/lib/ruby_llm/request_shape/part.rb +117 -0
  240. data/lib/ruby_llm/request_shape/problem.rb +63 -0
  241. data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
  242. data/lib/ruby_llm/request_shape/turn.rb +54 -0
  243. data/lib/ruby_llm/request_shape.rb +160 -0
  244. data/lib/ruby_llm/rerank.rb +9 -2
  245. data/lib/ruby_llm/research_job.rb +21 -5
  246. data/lib/ruby_llm/score.rb +42 -0
  247. data/lib/ruby_llm/search_results.rb +2 -2
  248. data/lib/ruby_llm/server_tool_call.rb +10 -2
  249. data/lib/ruby_llm/speech.rb +8 -1
  250. data/lib/ruby_llm/support/cancellation.rb +39 -0
  251. data/lib/ruby_llm/support/inspectable.rb +3 -2
  252. data/lib/ruby_llm/support/instrumentation.rb +38 -1
  253. data/lib/ruby_llm/support/process_cache.rb +60 -0
  254. data/lib/ruby_llm/support/progress_reporter.rb +33 -0
  255. data/lib/ruby_llm/thinking.rb +2 -1
  256. data/lib/ruby_llm/tokens.rb +57 -14
  257. data/lib/ruby_llm/tool.rb +50 -3
  258. data/lib/ruby_llm/tool_call.rb +17 -2
  259. data/lib/ruby_llm/transcription.rb +10 -3
  260. data/lib/ruby_llm/transport/connection.rb +127 -44
  261. data/lib/ruby_llm/transport/error_middleware.rb +90 -24
  262. data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
  263. data/lib/ruby_llm/transport/json_response.rb +33 -0
  264. data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
  265. data/lib/ruby_llm/version.rb +1 -1
  266. data/lib/ruby_llm/video.rb +5 -3
  267. data/lib/ruby_llm/video_job.rb +48 -5
  268. data/lib/ruby_llm.rb +106 -5
  269. data/lib/tasks/ruby_llm/evaluations.rb +43 -0
  270. data/lib/tasks/ruby_llm.rake +6 -9
  271. data/skills/rubyllm/SKILL.md +33 -12
  272. metadata +95 -37
  273. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
  274. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
  275. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
  276. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
  277. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
  278. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
  279. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
  280. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
  281. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
  282. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
  283. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
  284. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
@@ -31,8 +31,8 @@ module RubyLLM
31
31
  "#{@provider.model_path(@model.id)}:countTokens"
32
32
  end
33
33
 
34
- def render_count_tokens_payload(messages, **options)
35
- count_tokens_request(messages, **options)
34
+ def render_count_tokens_payload(messages, **)
35
+ count_tokens_request(messages, **)
36
36
  end
37
37
 
38
38
  def caches_url
@@ -35,6 +35,7 @@ module RubyLLM
35
35
  parts = data.dig('candidates', 0, 'content', 'parts') || []
36
36
  segments = parts.filter_map { |part| parse_transcription_segment(part) }
37
37
  text = parts.filter_map { |part| part['text'] || part.dig('audioTranscription', 'text') }.join
38
+ raise_if_transcription_blocked(data, text, response)
38
39
  words = segments.flat_map { |segment| segment['words'] }
39
40
  RubyLLM::Transcription.new(text:, model:, segments: segments.empty? ? nil : segments,
40
41
  words: words.empty? ? nil : words, **extract_usage(data))
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'stringio'
4
-
5
3
  module RubyLLM
6
4
  module Providers
7
5
  # Google Vertex AI implementation
@@ -28,6 +26,18 @@ module RubyLLM
28
26
  VertexAI::Capabilities
29
27
  end
30
28
 
29
+ def between_tools_off?(model_id) # :nodoc:
30
+ Providers::Anthropic.between_tools_off?(model_id)
31
+ end
32
+
33
+ def thinking_off_control(model_id) # :nodoc:
34
+ { enabled: false } if between_tools_off?(model_id)
35
+ end
36
+
37
+ def cache_write_input_multiplier(model_id, ttl) # :nodoc:
38
+ Providers::Anthropic.cache_write_input_multiplier(model_id, ttl) if model_id.to_s.start_with?('claude')
39
+ end
40
+
31
41
  def models_dev_alias(...)
32
42
  VertexAI::Models.models_dev_alias(...)
33
43
  end
@@ -68,11 +78,6 @@ module RubyLLM
68
78
  "#{location_path}/publishers/#{publisher}/models/#{model}"
69
79
  end
70
80
 
71
- def initialize(config)
72
- super
73
- @authorizer = nil
74
- end
75
-
76
81
  def batch_protocol
77
82
  batch_protocol_for_name(:gemini)
78
83
  end
@@ -111,6 +116,13 @@ module RubyLLM
111
116
  api_base_for(@config.vertexai_location)
112
117
  end
113
118
 
119
+ # Uploads are objects in the configured bucket, readable with any
120
+ # credentials the bucket admits.
121
+ def account_identity
122
+ bucket = @config.vertexai_batch_gcs_uri
123
+ account_digest(bucket) if bucket
124
+ end
125
+
114
126
  def api_base_for(location)
115
127
  return @config.vertexai_api_base if @config.vertexai_api_base
116
128
 
@@ -128,23 +140,26 @@ module RubyLLM
128
140
 
129
141
  def ranking_connection # :nodoc:
130
142
  base = @config.vertexai_ranking_api_base || 'https://discoveryengine.googleapis.com/v1'
131
- @ranking_connection ||= Transport::Connection.new(self, @config, api_base: base).tap do |connection|
132
- connection.connection.headers['X-Goog-User-Project'] = @config.vertexai_project_id
133
- end
143
+ @ranking_connection ||= Transport::Connection.new(
144
+ self, @config, api_base: base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
145
+ )
134
146
  end
135
147
 
136
148
  # The rescue can't name Google::Auth::AuthorizationError directly:
137
149
  # when googleauth is missing, evaluating the constant would replace
138
150
  # the helpful install error with a NameError.
139
151
  def headers
140
- initialize_authorizer unless @authorizer
141
- @authorizer.apply({})
152
+ Credentials.for(@config).headers
142
153
  rescue StandardError => e
143
154
  raise unless defined?(Google::Auth::AuthorizationError) && e.is_a?(Google::Auth::AuthorizationError)
144
155
 
145
156
  raise UnauthorizedError, "Invalid Google Cloud credentials for Vertex AI: #{e.message}"
146
157
  end
147
158
 
159
+ def google_credentials # :nodoc:
160
+ Credentials.for(@config).authorizer
161
+ end
162
+
148
163
  class << self
149
164
  def configuration_options
150
165
  %i[
@@ -172,22 +187,6 @@ module RubyLLM
172
187
  end
173
188
  end
174
189
 
175
- def initialize_authorizer
176
- require 'googleauth'
177
- @authorizer =
178
- if @config.vertexai_service_account_key
179
- ::Google::Auth::ServiceAccountCredentials.make_creds(
180
- json_key_io: StringIO.new(@config.vertexai_service_account_key),
181
- scope: SCOPES
182
- )
183
- else
184
- ::Google::Auth.get_application_default(SCOPES)
185
- end
186
- rescue LoadError
187
- raise Error,
188
- 'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
189
- end
190
-
191
190
  def batch_protocol_name_for(model)
192
191
  case model
193
192
  when %r{/} then :chat_completions
@@ -10,6 +10,12 @@ module RubyLLM
10
10
  role.to_s
11
11
  end
12
12
 
13
+ def supported_attachment?(attachment)
14
+ Protocols::ChatCompletions::Media.supported_attachment?(
15
+ attachment, document_attachments: :none, audio_attachments: false
16
+ )
17
+ end
18
+
13
19
  def format_content(content, attachments = [])
14
20
  Protocols::ChatCompletions::Media.format_content(
15
21
  content,
@@ -3,10 +3,9 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class XAI
6
- # Image generation and editing for the xAI API. Generation rejects the
7
- # size parameter. Editing takes JSON image references (URL, data URI,
8
- # or file id) instead of multipart uploads, up to three per request,
9
- # and has no mask support.
6
+ # Image generation and editing for the xAI API. Editing takes JSON
7
+ # image references (URL, data URI, or file id) instead of multipart
8
+ # uploads, up to three per request, and has no mask support.
10
9
  module Images
11
10
  module_function
12
11
 
@@ -15,21 +14,21 @@ module RubyLLM
15
14
  end
16
15
 
17
16
  def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
18
- return render_edit_payload(prompt, model:, count:, with:, provider_options:) if editing?(with, mask)
17
+ return render_edit_payload(prompt, model:, size:, count:, with:, provider_options:) if editing?(with, mask)
19
18
 
20
- RubyLLM.logger.debug { "Ignoring size #{size}. xAI image generation does not support a size parameter." }
21
- payload = { model: model, prompt: prompt }
19
+ payload = { model: model, prompt: prompt, size: size }.compact
22
20
  payload[:n] = count if count
23
21
 
24
22
  payload.merge(provider_options)
25
23
  end
26
24
 
27
- def render_edit_payload(prompt, model:, with:, provider_options:, count: nil)
25
+ def render_edit_payload(prompt, model:, with:, provider_options:, size: nil, count: nil)
28
26
  payload = {
29
27
  model: model,
30
28
  prompt: prompt,
31
- images: image_references(with)
32
- }
29
+ images: image_references(with),
30
+ size: size
31
+ }.compact
33
32
  payload[:n] = count if count
34
33
 
35
34
  payload.merge(provider_options)
@@ -27,24 +27,26 @@ module RubyLLM
27
27
  mcp: Protocols::Responses::SERVER_TOOL_ALIASES.fetch(:mcp)
28
28
  }.freeze
29
29
 
30
- SERVER_TOOL_USAGE_COUNTERS = %w[num_sources_used num_server_side_tools_used].freeze
30
+ SERVER_TOOL_USAGE_NAMES = { 'code_interpreter' => 'code_execution' }.freeze
31
31
 
32
32
  def server_tool_aliases
33
33
  SERVER_TOOL_ALIASES
34
34
  end
35
35
 
36
36
  def parse_usage(usage)
37
- super.merge(
38
- server_tool_use: server_side_tool_usage(usage),
39
- reported_cost: reported_cost(usage)
40
- )
37
+ super.merge(reported_cost: reported_cost(usage))
41
38
  end
42
39
 
43
40
  private
44
41
 
45
- def server_side_tool_usage(usage)
46
- counters = usage.slice(*SERVER_TOOL_USAGE_COUNTERS).reject { |_, count| count.to_i.zero? }
47
- counters.empty? ? nil : counters
42
+ # Each *_calls detail counts one tool; the totals and the X Search
43
+ # fetch counts beside them do not.
44
+ def parse_server_tool_use(response)
45
+ details = response.dig('usage', 'server_side_tool_usage_details').to_h
46
+ details.filter_map do |counter, count|
47
+ tool = counter[/\A(.+)_calls\z/, 1]
48
+ ["#{SERVER_TOOL_USAGE_NAMES.fetch(tool, tool)}_requests", count] if tool
49
+ end.to_h
48
50
  end
49
51
  end
50
52
  end
@@ -5,6 +5,8 @@ module RubyLLM
5
5
  class XAI
6
6
  # Grok Imagine generation, editing, and extension jobs.
7
7
  module Videos
8
+ include XAI::ReportedCost
9
+
8
10
  def video_url
9
11
  'videos/generations'
10
12
  end
@@ -32,11 +34,11 @@ module RubyLLM
32
34
  { model: model, prompt: prompt, video: video_reference(video) }.merge(provider_options)
33
35
  end
34
36
 
35
- def parse_video_job(response, model:)
37
+ def parse_video_job(response, model:, **request)
36
38
  id = response.body['request_id']
37
39
  raise Error.new('xAI did not return a video request id', response:) unless id
38
40
 
39
- VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
41
+ VideoJob.new(id: id, protocol: self, model: model, raw: response.body, **request)
40
42
  end
41
43
 
42
44
  def video_job_url(job)
@@ -45,11 +47,12 @@ module RubyLLM
45
47
 
46
48
  def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
47
49
  body = response.body
48
- case body['status']
49
- when 'done' then { status: :completed, raw: body }
50
- when 'failed', 'expired' then { status: :failed, raw: body, error: body['error'] || body['status'] }
51
- else { status: :pending, raw: body }
52
- end
50
+ state = case body['status']
51
+ when 'done' then { status: :completed }
52
+ when 'failed', 'expired' then { status: :failed, error: body['error'] || body['status'] }
53
+ else { status: :pending }
54
+ end
55
+ state.merge({ raw: body, reported_cost: reported_cost(body['usage'] || {}) }.compact)
53
56
  end
54
57
 
55
58
  def download_video(job)
@@ -77,9 +80,8 @@ module RubyLLM
77
80
 
78
81
  def video_reference(attachment)
79
82
  return { file_id: attachment.provider_file_id } if attachment.provider_file?
80
- return { url: attachment.source.to_s } if attachment.url?
81
83
 
82
- { url: attachment.for_llm }
84
+ { url: attachment.url_or_data_uri }
83
85
  end
84
86
  end
85
87
  end
@@ -18,6 +18,10 @@ module RubyLLM
18
18
  }
19
19
  end
20
20
 
21
+ def account_identity
22
+ account_digest(api_base, @config.xai_api_key)
23
+ end
24
+
21
25
  class << self
22
26
  def capabilities
23
27
  XAI::Capabilities
@@ -15,16 +15,27 @@ if defined?(Rails::Railtie)
15
15
 
16
16
  initializer 'ruby_llm.active_record' do
17
17
  ActiveSupport.on_load :active_record do
18
+ require 'ruby_llm/active_record/record'
18
19
  require 'ruby_llm/active_record/payload_helpers'
19
20
  require 'ruby_llm/active_record/model'
20
21
  require 'ruby_llm/active_record/tool_call'
21
22
  require 'ruby_llm/active_record/usage'
22
23
  require 'ruby_llm/active_record/batch'
24
+ require 'ruby_llm/active_record/mcp_credential'
25
+ require 'ruby_llm/active_record/provider_file'
23
26
  require 'ruby_llm/active_record/chat_methods'
24
27
  require 'ruby_llm/active_record/message_methods'
25
28
 
26
29
  require 'ruby_llm/active_record/acts_as'
27
30
  ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
31
+ RubyLLM.config.mcp_credential_store ||= RubyLLM::ActiveRecord::MCPCredential
32
+ RubyLLM::Accounting::Usage.ledger ||= RubyLLM::ActiveRecord::Usage
33
+ end
34
+ end
35
+
36
+ initializer 'ruby_llm.active_storage' do
37
+ ActiveSupport.on_load :active_storage_blob do
38
+ after_destroy { RubyLLM::ActiveRecord::ProviderFile.forget_blob(self) }
28
39
  end
29
40
  end
30
41
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class RequestShape
5
+ # A Part describes one piece of a turn by its kind and size, never by
6
+ # its content.
7
+ #
8
+ # part.kind # => :tool_call
9
+ # part.name # => "find_tasks"
10
+ # part.size # => 42
11
+ # part.unit # => :chars
12
+ # part.signed? # => true
13
+ # part.to_s # => "call find_tasks (args 42 chars), signed"
14
+ #
15
+ class Part
16
+ include Support::Inspectable
17
+
18
+ TEXT_KINDS = %i[text thinking].freeze
19
+ private_constant :TEXT_KINDS
20
+
21
+ # The kind of piece: +:text+, +:thinking+, +:image+, +:audio+,
22
+ # +:video+, +:document+, +:tool_call+, +:tool_result+, or +:other+
23
+ # for a piece RubyLLM does not classify, such as a provider tool step.
24
+ attr_reader :kind
25
+
26
+ # The size of the piece as the request sends it, in #unit: the
27
+ # characters of text, thinking, tool call arguments, and a tool
28
+ # result's text, or of its JSON when it is structured data, and the
29
+ # bytes of inline media. Media inside a tool result follows it as
30
+ # parts of its own. Returns +nil+ when there is nothing to measure,
31
+ # such as media sent by URL or thinking the provider encrypted.
32
+ attr_reader :size
33
+
34
+ # The unit of #size: +:chars+ or +:bytes+, or +nil+ without a size.
35
+ attr_reader :unit
36
+
37
+ # The tool's name for a tool call, and for a tool result the name of
38
+ # the call it answers, which is +nil+ when no call in the request
39
+ # matches it. For an +:other+ piece, the provider's name for it.
40
+ attr_reader :name
41
+
42
+ # Where media comes from: +:inline+ for bytes in the request, +:url+
43
+ # for a link the provider fetches, or +:file+ for a file stored with
44
+ # the provider. Returns +nil+ for pieces that are not media.
45
+ attr_reader :source
46
+
47
+ # The MIME type of media, as the request gives it or as its format
48
+ # implies, or +nil+.
49
+ attr_reader :mime_type
50
+
51
+ # Creates a part. Every attribute but +kind+ is optional. A +size+
52
+ # counts characters unless +unit+ says +:bytes+; pass +signed: true+
53
+ # when the piece carries a thinking signature.
54
+ def initialize(kind:, size: nil, unit: nil, name: nil, source: nil, mime_type: nil, signed: false)
55
+ @kind = kind
56
+ @size = size
57
+ @unit = size && (unit || :chars)
58
+ @name = name
59
+ @source = source
60
+ @mime_type = mime_type
61
+ @signed = signed
62
+ end
63
+
64
+ # Returns whether the piece carries a thinking signature, the token a
65
+ # provider issues to verify replayed thinking or tool calls.
66
+ def signed?
67
+ @signed
68
+ end
69
+
70
+ # Returns the part in words, with its size and unit:
71
+ #
72
+ # text (13 chars)
73
+ # image/png (34512 bytes)
74
+ # document (url)
75
+ # result find_tasks (6859 chars)
76
+ # thinking (no text), signed
77
+ #
78
+ def to_s
79
+ signed? ? "#{description}, signed" : description
80
+ end
81
+
82
+ # Returns the part as a Hash, with its size under its unit, omitting
83
+ # attributes it does not have.
84
+ def to_h
85
+ hash = { kind: kind, name: name }
86
+ hash[unit] = size if size
87
+ hash.merge(source: source, mime_type: mime_type, signed: signed? || nil).compact
88
+ end
89
+
90
+ def inspect_attributes # :nodoc:
91
+ to_h
92
+ end
93
+
94
+ private
95
+
96
+ def description
97
+ case kind
98
+ when :tool_call then ['call', name, size && "(args #{measured})"].compact.join(' ')
99
+ when :tool_result then ['result', name, size && "(#{measured})"].compact.join(' ')
100
+ when :other then name || 'part of no known kind'
101
+ else "#{mime_type || kind} (#{size ? measured : missing})"
102
+ end
103
+ end
104
+
105
+ def measured
106
+ noun = { chars: 'char', bytes: 'byte' }.fetch(unit, unit.to_s)
107
+ "#{size} #{noun}#{'s' unless size == 1}"
108
+ end
109
+
110
+ def missing
111
+ return source.to_s if source && source != :inline
112
+
113
+ TEXT_KINDS.include?(kind) ? 'no text' : 'no data'
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class RequestShape
5
+ # A Problem is something in a request that providers are known to
6
+ # reject, found by the protocol that rendered it: a part with no data, a
7
+ # tool round whose calls and results do not pair up, a step that lacks
8
+ # the signature the provider requires, or roles out of order.
9
+ #
10
+ # problem.kind # => :part_without_data
11
+ # problem.turn # => 3
12
+ # problem.part # => 0
13
+ # problem.to_s # => "problem at #3, part 0: thinking part carries no data"
14
+ #
15
+ class Problem
16
+ include Support::Inspectable
17
+
18
+ # What is wrong: +:part_without_data+, +:empty_turn+,
19
+ # +:unpaired_round+, +:unmatched_result+, +:unsigned_call+,
20
+ # +:unsigned_thinking+, or +:role_order+.
21
+ attr_reader :kind
22
+
23
+ # The index of the turn the problem is in, or +nil+.
24
+ attr_reader :turn
25
+
26
+ # The position of the part in the turn's parts, or +nil+ when the
27
+ # problem concerns the whole turn.
28
+ attr_reader :part
29
+
30
+ # The id of the call an unmatched tool result names, the only id a
31
+ # shape shows, or +nil+.
32
+ attr_reader :call_id
33
+
34
+ # The problem in words.
35
+ attr_reader :message
36
+
37
+ # Creates a problem of +kind+ described by +message+.
38
+ def initialize(kind:, message:, turn: nil, part: nil, call_id: nil)
39
+ @kind = kind
40
+ @message = message
41
+ @turn = turn
42
+ @part = part
43
+ @call_id = call_id
44
+ end
45
+
46
+ # Returns the problem as one line, with where it is.
47
+ def to_s
48
+ location = ["##{turn}", part && "part #{part}"].compact.join(', ') if turn
49
+ line = location ? "problem at #{location}: #{message}" : "problem: #{message}"
50
+ call_id ? "#{line} (call id #{call_id})" : line
51
+ end
52
+
53
+ # Returns the problem as a Hash, omitting attributes it does not have.
54
+ def to_h
55
+ { kind: kind, turn: turn, part: part, call_id: call_id, message: message }.compact
56
+ end
57
+
58
+ def inspect_attributes # :nodoc:
59
+ to_h
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class RequestShape
5
+ # A ToolRound describes a model turn that called tools and the results
6
+ # the request sends back before the next model turn: how many of each,
7
+ # and whether they pair up the way the provider matches them.
8
+ #
9
+ # round.turn # => 1
10
+ # round.calls # => 2
11
+ # round.results # => 1
12
+ # round.paired? # => false
13
+ # round.to_s # => "tool round at #1: 2 calls, 1 result, not paired"
14
+ #
15
+ class ToolRound
16
+ include Support::Inspectable
17
+
18
+ # The index of the turn that made the calls.
19
+ attr_reader :turn
20
+
21
+ # The number of tool calls in the round.
22
+ attr_reader :calls
23
+
24
+ # The number of tool results sent back for the round.
25
+ attr_reader :results
26
+
27
+ # Creates a round at +turn+ with the number of +calls+ and +results+,
28
+ # and whether they are +paired+.
29
+ def initialize(turn:, calls:, results:, paired:)
30
+ @turn = turn
31
+ @calls = calls
32
+ @results = results
33
+ @paired = paired
34
+ end
35
+
36
+ # Returns whether every call has its result, matched the way the
37
+ # provider matches them: by call id, or by position and tool name.
38
+ def paired?
39
+ @paired
40
+ end
41
+
42
+ # Returns the round in words.
43
+ def to_s
44
+ "tool round at ##{turn}: #{counted(calls, 'call')}, #{counted(results, 'result')}, " \
45
+ "#{paired? ? 'paired' : 'not paired'}"
46
+ end
47
+
48
+ # Returns the round as a Hash with its turn, counts, and pairing.
49
+ def to_h
50
+ { turn: turn, calls: calls, results: results, paired: paired? }
51
+ end
52
+
53
+ def inspect_attributes # :nodoc:
54
+ to_h
55
+ end
56
+
57
+ private
58
+
59
+ def counted(count, noun)
60
+ "#{count} #{noun}#{'s' unless count == 1}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class RequestShape
5
+ # A Turn describes one entry of a request's conversation: a message, or
6
+ # one item where the provider sends a model's reasoning and tool calls as
7
+ # items of their own.
8
+ #
9
+ # turn = error.request_shape.turns.last
10
+ # turn.index # => 4
11
+ # turn.role # => "user"
12
+ # turn.to_s # => "#4 user: text (31 chars)"
13
+ #
14
+ class Turn
15
+ include Support::Inspectable
16
+
17
+ # The turn's 0-indexed position in the request, the position a
18
+ # provider's error message counts from.
19
+ attr_reader :index
20
+
21
+ # The turn's role as the payload names it, such as <tt>"model"</tt>,
22
+ # or for an entry that names no role, its type, such as
23
+ # <tt>"function_call"</tt>. Returns +nil+ when the entry names neither.
24
+ attr_reader :role
25
+
26
+ # The Part objects of the turn, in order.
27
+ attr_reader :parts
28
+
29
+ # Creates a turn at +index+ with +role+ and +parts+.
30
+ def initialize(index:, role:, parts: [])
31
+ @index = index
32
+ @role = role
33
+ @parts = parts
34
+ end
35
+
36
+ # Returns the turn as one line: its index, its role, and its parts.
37
+ #
38
+ # turn.to_s # => "#3 model: thinking (11 chars), text (13 chars), signed"
39
+ #
40
+ def to_s
41
+ "##{[index, role].compact.join(' ')}: #{parts.empty? ? 'no parts' : parts.join(', ')}"
42
+ end
43
+
44
+ # Returns the turn as a Hash with its index, role, and parts.
45
+ def to_h
46
+ { index: index, role: role, parts: parts.map(&:to_h) }
47
+ end
48
+
49
+ def inspect_attributes # :nodoc:
50
+ { index: index, role: role, parts: parts.join(', ') }
51
+ end
52
+ end
53
+ end
54
+ end