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
@@ -67,6 +67,11 @@ module RubyLLM
67
67
  # Chat#with_provider_tools and the model used one.
68
68
  attr_reader :server_tool_calls
69
69
 
70
+ # The MCP::Result behind a tool result message, when the MCP tool that
71
+ # returned it has a UI and did not fail, so your app can render the UI
72
+ # again with the result. +nil+ otherwise.
73
+ attr_reader :mcp_result
74
+
70
75
  # The provider-shaped content blocks of this assistant message, kept
71
76
  # verbatim when the response used server tools so later requests can
72
77
  # replay the turn exactly. +nil+ otherwise.
@@ -95,6 +100,7 @@ module RubyLLM
95
100
  output: options[:output_tokens],
96
101
  cache_read: options[:cache_read_tokens],
97
102
  cache_write: options[:cache_write_tokens],
103
+ cache_write_by_ttl: options[:cache_write_tokens_by_ttl],
98
104
  thinking: options[:thinking_tokens],
99
105
  server_tool_use: options[:server_tool_use],
100
106
  reported_cost: options[:reported_cost]
@@ -105,9 +111,11 @@ module RubyLLM
105
111
  @server_tool_calls = Array(options[:server_tool_calls]).map { |call| coerce_value(call, ServerToolCall) }
106
112
  @raw_content = options[:raw_content]
107
113
  @raw_reasoning = options[:raw_reasoning]
114
+ @mcp_result = coerce_mcp_result(options[:mcp_result])
108
115
  @finish_reason = options[:finish_reason]&.to_sym
109
116
  self.ruby_llm_usage_entries = options[:usage_entries] if options[:usage_entries]
110
117
  @cache_until_here = options.fetch(:cache_until_here, false)
118
+ @cache_ttl = options[:cache_ttl] if @cache_until_here
111
119
 
112
120
  ensure_valid_role
113
121
  end
@@ -129,10 +137,11 @@ module RubyLLM
129
137
  dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
130
138
  end
131
139
 
132
- def without_thinking # :nodoc:
140
+ def without_native_content(raw_content: nil) # :nodoc:
133
141
  dup.tap do |message|
134
142
  message.instance_variable_set(:@thinking, nil)
135
143
  message.instance_variable_set(:@raw_reasoning, nil)
144
+ message.instance_variable_set(:@raw_content, raw_content)
136
145
  message.instance_variable_set(:@tool_calls, tool_calls_without_thought_signatures)
137
146
  end
138
147
  end
@@ -209,15 +218,32 @@ module RubyLLM
209
218
 
210
219
  # Marks this message as an explicit prompt cache boundary. Providers
211
220
  # with boundary controls use the conversation up to and including this
212
- # message as the cacheable prefix. Returns +self+.
221
+ # message as the cacheable prefix. Pass +ttl:+ to give this boundary its
222
+ # own cache lifetime instead of the +ttl:+ from Chat#with_caching, on
223
+ # providers that set a lifetime per boundary. Returns +self+.
213
224
  #
214
225
  # chat.add_message(role: :user, content: long_context).cache_until_here
226
+ # chat.add_message(role: :user, content: handbook).cache_until_here(ttl: "1h")
215
227
  #
216
- def cache_until_here
228
+ def cache_until_here(ttl: nil)
217
229
  @cache_until_here = true
230
+ @cache_ttl = ttl
218
231
  self
219
232
  end
220
233
 
234
+ # Returns the cache lifetime given to this message's boundary, such as
235
+ # <tt>"1h"</tt>, or +nil+ when the chat's caching policy applies.
236
+ attr_reader :cache_ttl
237
+
238
+ def self.cache_boundary_options(value) # :nodoc:
239
+ return {} if value == true
240
+
241
+ options = value.transform_keys(&:to_sym) if value.is_a?(Hash)
242
+ return options if options && (options.keys - %i[ttl]).empty?
243
+
244
+ raise ArgumentError, "cache_until_here accepts true, false, or ttl:, got #{value.inspect}"
245
+ end
246
+
221
247
  # Returns +true+ if the message carries an explicit prompt cache
222
248
  # boundary, +false+ otherwise.
223
249
  def cache_until_here?
@@ -227,7 +253,8 @@ module RubyLLM
227
253
  # Returns a Hash of the message's attributes, with token counts merged
228
254
  # in as +:input_tokens+, +:output_tokens+, and related keys. Omits
229
255
  # +nil+ values and empty attachment and citation lists. Includes +:cost+
230
- # only when supplied explicitly, preserving unknown costs on round-trip.
256
+ # only when supplied explicitly, preserving unknown costs on round-trip,
257
+ # and +:reported_cost+ when the provider reported its own price.
231
258
  def to_h
232
259
  {
233
260
  role: role,
@@ -235,6 +262,7 @@ module RubyLLM
235
262
  attachments: list_to_h(attachments),
236
263
  model: model,
237
264
  cost: @supplied_cost && cost.to_h,
265
+ reported_cost: tokens.reported_cost,
238
266
  tool_calls: tool_calls&.transform_values(&:to_h),
239
267
  tool_call_id: tool_call_id,
240
268
  thinking: thinking&.text,
@@ -243,8 +271,10 @@ module RubyLLM
243
271
  server_tool_calls: list_to_h(server_tool_calls),
244
272
  raw_content: raw_content,
245
273
  raw_reasoning: raw_reasoning,
274
+ mcp_result: mcp_result&.dump,
246
275
  finish_reason: finish_reason,
247
- cache_until_here: cache_until_here? || nil
276
+ cache_until_here: cache_until_here? || nil,
277
+ cache_ttl: cache_ttl
248
278
  }.merge(tokens.to_h).compact
249
279
  end
250
280
 
@@ -292,7 +322,8 @@ module RubyLLM
292
322
 
293
323
  def coerce_thinking(thinking, signature)
294
324
  case thinking
295
- when nil, Thinking then thinking
325
+ when Thinking then thinking
326
+ when nil then Thinking.build(signature: signature)
296
327
  when Hash then Thinking.build(**thinking.transform_keys(&:to_sym).slice(:text, :signature))
297
328
  else Thinking.build(text: thinking.to_s, signature: signature)
298
329
  end
@@ -302,6 +333,10 @@ module RubyLLM
302
333
  value.is_a?(Hash) ? klass.from_h(value) : value
303
334
  end
304
335
 
336
+ def coerce_mcp_result(value)
337
+ value.is_a?(Hash) ? MCP::Result.load(value.transform_keys(&:to_s)) : value
338
+ end
339
+
305
340
  def normalize_content(content)
306
341
  return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
307
342
  return content if content.nil? || content.is_a?(String)
@@ -171,7 +171,7 @@ module RubyLLM
171
171
 
172
172
  # Returns the model's primary function, inferred from its output
173
173
  # modalities: +:chat+, +:embedding+, +:moderation+, +:image+, +:audio+,
174
- # +:video+, or +:rerank+.
174
+ # +:video+, +:rerank+, or +:judgment+.
175
175
  def type
176
176
  output = modalities.output
177
177
  return :embedding if output.include?('embeddings')
@@ -180,6 +180,7 @@ module RubyLLM
180
180
  return :audio if output.include?('audio')
181
181
  return :video if output.include?('video')
182
182
  return :rerank if output.include?('rerank')
183
+ return :judgment if output.include?('judgment')
183
184
 
184
185
  :chat
185
186
  end
@@ -5,6 +5,9 @@ require 'json'
5
5
  module RubyLLM
6
6
  class Models
7
7
  class Aliases # :nodoc:
8
+ LOAD_LOCK = Mutex.new
9
+ private_constant :LOAD_LOCK
10
+
8
11
  class << self
9
12
  def resolve(model_id, provider = nil)
10
13
  return model_id unless aliases[model_id]
@@ -17,7 +20,7 @@ module RubyLLM
17
20
  end
18
21
 
19
22
  def aliases
20
- @aliases ||= load_aliases
23
+ @aliases || LOAD_LOCK.synchronize { @aliases ||= load_aliases }
21
24
  end
22
25
 
23
26
  def aliases_file
@@ -26,7 +29,7 @@ module RubyLLM
26
29
 
27
30
  def load_aliases
28
31
  if File.exist?(aliases_file)
29
- JSON.parse(File.read(aliases_file))
32
+ JSON.parse(File.read(aliases_file, encoding: Encoding::UTF_8))
30
33
  else
31
34
  {}
32
35
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Models
5
+ # Finds registry entries by model id, resolving aliases, provider
6
+ # registry ids, and deployments, and preferring first-party providers.
7
+ module Lookup # :nodoc:
8
+ private
9
+
10
+ def models_by_id
11
+ models = @models
12
+ index = @model_index
13
+ index = @model_index = [models, models.group_by(&:id)] unless index && index.first.equal?(models)
14
+ index.last
15
+ end
16
+
17
+ def find_with_provider(model_id, provider, config = nil)
18
+ deployed_id = Provider.resolve(provider)&.deployed_model_id(model_id, config || RubyLLM.config)
19
+ return find_registered(model_id, provider, config) unless deployed_id
20
+
21
+ Model.new(find_deployed_model(model_id, deployed_id, provider, config).to_h.merge(id: model_id))
22
+ end
23
+
24
+ def find_deployed_model(deployment, model_id, provider, config)
25
+ find_registered(model_id, provider, config)
26
+ rescue ModelNotFoundError
27
+ raise ConfigurationError, "Deployment #{deployment.inspect} points to unknown model #{model_id.inspect} " \
28
+ "for provider: #{provider.inspect}. #{refresh_registry_guidance}"
29
+ end
30
+
31
+ def find_registered(model_id, provider, config)
32
+ resolved_id = Aliases.resolve(model_id, provider)
33
+ resolved_id = resolve_provider_registry_id(resolved_id, provider, config)
34
+ index = models_by_id
35
+ Array(index[resolved_id]).find { |m| m.provider == provider.to_s } ||
36
+ Array(index[model_id]).find { |m| m.provider == provider.to_s } ||
37
+ raise_model_not_found(model_id, provider: provider)
38
+ end
39
+
40
+ def resolve_provider_registry_id(model_id, provider, config = nil)
41
+ provider_class = Provider.resolve(provider)
42
+ return model_id unless provider_class
43
+
44
+ provider_class.resolve_registry_id(model_id, self, config || RubyLLM.config)
45
+ end
46
+
47
+ # A name can be one provider's exact id and another's alias:
48
+ # claude-opus-4 is exact on vertexai, an alias on anthropic.
49
+ # Provider preference settles it, not the kind of match.
50
+ def find_without_provider(model_id)
51
+ resolved_id = Aliases.resolve(model_id)
52
+ index = models_by_id
53
+ matches = Array(index[model_id])
54
+ matches += Array(index[resolved_id]) unless resolved_id == model_id
55
+
56
+ preferred_match(matches) || raise_model_not_found(model_id)
57
+ end
58
+
59
+ def raise_model_not_found(model_id, provider: nil)
60
+ message = "Unknown model: #{model_id.inspect}"
61
+ message = "#{message} for provider: #{provider.inspect}" if provider
62
+
63
+ raise ModelNotFoundError, "#{message}. #{refresh_registry_guidance}"
64
+ end
65
+
66
+ def refresh_registry_guidance
67
+ 'If the model exists at the provider, refresh the registry with `RubyLLM.models.refresh`.'
68
+ end
69
+
70
+ def preferred_match(candidates)
71
+ return candidates.first if candidates.size == 1
72
+
73
+ candidates.min_by do |model|
74
+ index = PROVIDER_PREFERENCE.index(model.provider)
75
+ [model.unlisted? ? 1 : 0, index || PROVIDER_PREFERENCE.length]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -94,7 +94,7 @@ module RubyLLM
94
94
 
95
95
  def fetch(etag: nil)
96
96
  connection = RubyLLM::Transport::Connection.basic do |faraday|
97
- faraday.response :json, parser_options: { symbolize_names: true }
97
+ faraday.use Transport::JsonResponse, parser_options: { symbolize_names: true }
98
98
  end
99
99
  response = connection.get(url) do |request|
100
100
  request.headers['If-None-Match'] = etag if etag
@@ -116,7 +116,7 @@ module RubyLLM
116
116
  module_function
117
117
 
118
118
  def read(file)
119
- data = JSON.parse(File.read(file), symbolize_names: true)
119
+ data = JSON.parse(File.read(file, encoding: Encoding::UTF_8), symbolize_names: true)
120
120
  models_from_data(data, source: file)
121
121
  rescue Errno::ENOENT
122
122
  nil
@@ -13,7 +13,7 @@ module RubyLLM
13
13
  structured_output predicted_outputs
14
14
  distillation fine_tuning batch realtime image_generation
15
15
  speech_generation transcription translation citations reasoning
16
- caching moderation json_mode vision video ocr
16
+ caching moderation json_mode vision video ocr judgment
17
17
  ].freeze
18
18
 
19
19
  description 'A model entry in the RubyLLM model registry'