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
@@ -185,7 +185,8 @@ module RubyLLM
185
185
  # +append:+ is true. Pass <tt>persist: false</tt> to apply the
186
186
  # instructions only to the in-memory chat for this record instance.
187
187
  # With <tt>cache_until_here: true</tt> the instruction becomes an
188
- # explicit prompt cache boundary. Returns +self+.
188
+ # explicit prompt cache boundary, and <tt>cache_until_here: { ttl: "1h" }</tt>
189
+ # gives that boundary its own lifetime. Returns +self+.
189
190
  #
190
191
  # chat.with_instructions "You are a Ruby expert."
191
192
  # chat.with_instructions "Use short bullet points.", append: true
@@ -212,20 +213,40 @@ module RubyLLM
212
213
  # RubyLLM::Chat. Each behaves exactly as documented on RubyLLM::Chat,
213
214
  # then returns the record so calls chain.
214
215
  CHAINABLE_CHAT_DELEGATES = %i[
215
- with_tools with_tool_options with_provider_tools with_fallbacks with_temperature
216
+ with_tools with_mcp with_tool_options with_provider_tools with_fallbacks with_temperature answer decline
216
217
  with_max_output_tokens with_thinking with_citations with_caching
217
218
  with_end_user with_compaction
218
219
  with_provider_options with_headers with_schema
219
- before_request before_message after_message before_tool_call after_tool_result
220
+ convert_unsupported_attachments
221
+ before_request before_message after_message before_tool_call after_tool_result after_tool_progress
220
222
  before_fallback after_fallback
221
223
  ].freeze
222
224
 
223
225
  ##
224
226
  # :method: with_tools
225
- # :call-seq: with_tools(*tools)
227
+ # :call-seq: with_tools(*tools, defer: nil)
226
228
  #
227
229
  # Applies Chat#with_tools and returns this record.
228
230
 
231
+ ##
232
+ # :method: with_mcp
233
+ # :call-seq: with_mcp(*servers, defer: nil)
234
+ #
235
+ # Applies Chat#with_mcp and returns this record.
236
+
237
+ ##
238
+ # :method: answer
239
+ # :call-seq: answer(request, **values)
240
+ #
241
+ # Applies Chat#answer and returns this record. The answer persists
242
+ # on the tool call, so any process can resume the call.
243
+
244
+ ##
245
+ # :method: decline
246
+ # :call-seq: decline(request)
247
+ #
248
+ # Applies Chat#decline and returns this record.
249
+
229
250
  ##
230
251
  # :method: with_tool_options
231
252
  # :call-seq: with_tool_options(**options)
@@ -304,6 +325,12 @@ module RubyLLM
304
325
  #
305
326
  # Applies Chat#with_schema and returns this record.
306
327
 
328
+ ##
329
+ # :method: convert_unsupported_attachments
330
+ # :call-seq: convert_unsupported_attachments(&block)
331
+ #
332
+ # Applies Chat#convert_unsupported_attachments and returns this record.
333
+
307
334
  ##
308
335
  # :method: before_request
309
336
  # :call-seq: before_request(&block)
@@ -334,6 +361,12 @@ module RubyLLM
334
361
  #
335
362
  # Applies Chat#after_tool_result and returns this record.
336
363
 
364
+ ##
365
+ # :method: after_tool_progress
366
+ # :call-seq: after_tool_progress(&block)
367
+ #
368
+ # Applies Chat#after_tool_progress and returns this record.
369
+
337
370
  ##
338
371
  # :method: before_fallback
339
372
  # :call-seq: before_fallback(&block)
@@ -436,6 +469,51 @@ module RubyLLM
436
469
  #
437
470
  # Delegates to Chat#tools. See that method for arguments and return values.
438
471
 
472
+ ##
473
+ # :method: deferred_tools
474
+ # :call-seq: deferred_tools
475
+ #
476
+ # Delegates to Chat#deferred_tools. See that method for arguments and return values.
477
+
478
+ ##
479
+ # :method: mcp
480
+ # :call-seq: mcp
481
+ #
482
+ # Delegates to Chat#mcp. See that method for arguments and return values.
483
+
484
+ ##
485
+ # :method: waiting?
486
+ # :call-seq: waiting?
487
+ #
488
+ # Delegates to Chat#waiting?. Approvals, answers, and tasks persist on
489
+ # their tool calls, so any process sees what the chat waits on.
490
+
491
+ ##
492
+ # :method: awaiting_input?
493
+ # :call-seq: awaiting_input?
494
+ #
495
+ # Delegates to Chat#awaiting_input?. See that method for arguments and return values.
496
+
497
+ ##
498
+ # :method: pending_inputs
499
+ # :call-seq: pending_inputs
500
+ #
501
+ # Delegates to Chat#pending_inputs. Requests persist on their tool
502
+ # calls, so they survive restarts.
503
+
504
+ ##
505
+ # :method: awaiting_tasks?
506
+ # :call-seq: awaiting_tasks?
507
+ #
508
+ # Delegates to Chat#awaiting_tasks?. See that method for arguments and return values.
509
+
510
+ ##
511
+ # :method: pending_tasks
512
+ # :call-seq: pending_tasks
513
+ #
514
+ # Delegates to Chat#pending_tasks. Tasks persist on their tool calls,
515
+ # so any process can check on them and resume the chat.
516
+
439
517
  CHAINABLE_CHAT_DELEGATES.each do |name|
440
518
  define_method(name) do |*args, **kwargs, &block|
441
519
  to_llm.public_send(name, *args, **kwargs, &block)
@@ -449,8 +527,8 @@ module RubyLLM
449
527
 
450
528
  PASSTHROUGH_CHAT_DELEGATES = %i[
451
529
  caching citations compaction concurrency end_user fallbacks headers max_output_tokens provider_options
452
- schema provider_tools temperature thinking tool_options tools
453
- add_completion count_tokens each render
530
+ schema provider_tools temperature thinking tool_options tools deferred_tools mcp waiting? awaiting_input?
531
+ pending_inputs awaiting_tasks? pending_tasks add_completion count_tokens each render
454
532
  ].freeze
455
533
 
456
534
  ##
@@ -526,17 +604,19 @@ module RubyLLM
526
604
  end
527
605
 
528
606
  # Marks the latest persisted message as a prompt cache boundary, or the
529
- # latest in-memory message when none is persisted yet. Returns +self+.
607
+ # latest in-memory message when none is persisted yet. Pass +ttl:+ to
608
+ # give the boundary its own cache lifetime. Returns +self+.
530
609
  #
531
610
  # chat.with_instructions('Reusable analysis prompt').cache_until_here
611
+ # chat.with_instructions('Reusable analysis prompt').cache_until_here(ttl: "1h")
532
612
  #
533
613
  # Raises ArgumentError if the chat has no messages.
534
- def cache_until_here
614
+ def cache_until_here(ttl: nil)
535
615
  message_record = messages_association.order(:id).last
536
616
  if message_record
537
- message_record.cache_until_here
617
+ message_record.cache_until_here(ttl:)
538
618
  elsif @chat&.messages&.any?
539
- @chat.cache_until_here
619
+ @chat.cache_until_here(ttl:)
540
620
  else
541
621
  raise ArgumentError, 'No messages to cache'
542
622
  end
@@ -585,7 +665,11 @@ module RubyLLM
585
665
  #
586
666
  def ask_later(message = nil, with: nil)
587
667
  to_llm.raise_if_pending_tool_calls!
588
- add_message(role: :user, content: message, attachments: with)
668
+ if message.is_a?(RubyLLM::MCP::Prompt)
669
+ message.messages.each { |prompt_message| add_message(prompt_message) }
670
+ else
671
+ add_message(role: :user, content: message, attachments: with)
672
+ end
589
673
  self
590
674
  end
591
675
 
@@ -617,9 +701,9 @@ module RubyLLM
617
701
 
618
702
  # Advances the conversation by one move: runs the pending tool calls if
619
703
  # there are any, otherwise generates a response. Returns +nil+ once the
620
- # chat is complete or waiting for approval. See RubyLLM::Chat#step.
704
+ # chat is complete or #waiting?. See RubyLLM::Chat#step.
621
705
  #
622
- # chat.step until chat.complete? || chat.awaiting_approval?
706
+ # chat.step until chat.complete? || chat.waiting?
623
707
  #
624
708
  def step(...)
625
709
  to_llm.step(...)
@@ -649,27 +733,11 @@ module RubyLLM
649
733
  private
650
734
 
651
735
  def persist_usage_entry(entry)
652
- tokens = entry.tokens
653
- cost = entry.cost
654
- attributes = {
655
- operation: entry.operation,
656
- provider: entry.provider,
657
- model: entry.model,
658
- status: entry.status,
659
- input_tokens: tokens.input,
660
- output_tokens: tokens.output,
661
- cache_read_tokens: tokens.cache_read,
662
- cache_write_tokens: tokens.cache_write,
663
- thinking_tokens: tokens.thinking,
664
- input_cost: cost.input,
665
- output_cost: cost.output,
666
- cache_read_cost: cost.cache_read,
667
- cache_write_cost: cost.cache_write,
668
- thinking_cost: cost.thinking,
669
- total_cost: cost.total
670
- }
671
- record = ruby_llm_usages.create!(attributes)
672
- usage_records_by_entry[entry] = record
736
+ usage_records_by_entry[entry] = Usage.transaction(requires_new: true) do
737
+ ruby_llm_usages.create!(Usage.attributes_for(entry))
738
+ end
739
+ rescue StandardError => e
740
+ RubyLLM.logger.warn("RubyLLM could not record #{entry.operation} usage: #{e.class}: #{e.message}")
673
741
  end
674
742
 
675
743
  def link_usage_entries(message)
@@ -800,6 +868,8 @@ module RubyLLM
800
868
  sync_messages(chat)
801
869
  chat.cancellation_checker = proc { consume_persisted_cancellation_request }
802
870
  chat.approval_checker = proc { |tool_call| persisted_tool_call_approval(tool_call) }
871
+ chat.input_checker = proc { |tool_call| persisted_tool_call_input(tool_call) }
872
+ chat.input_recorder = proc { |tool_call, input| persist_tool_call_input(tool_call, input) }
803
873
  install_persistence_callbacks(chat)
804
874
  end
805
875
 
@@ -813,6 +883,16 @@ module RubyLLM
813
883
  self
814
884
  end
815
885
 
886
+ def persisted_tool_call_input(tool_call)
887
+ record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
888
+ record.mcp_state if record&.has_attribute?(:mcp_state)
889
+ end
890
+
891
+ def persist_tool_call_input(tool_call, input)
892
+ record = find_tool_call(tool_call.id)
893
+ record.update!(mcp_state: input) if record&.has_attribute?(:mcp_state)
894
+ end
895
+
816
896
  def persisted_tool_call_approval(tool_call)
817
897
  record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
818
898
  return unless record&.has_attribute?(:approval)
@@ -827,7 +907,7 @@ module RubyLLM
827
907
  message_records = eager_load_messages
828
908
  chat.messages = message_records
829
909
  linked_entry_pairs = message_records.zip(chat.messages).flat_map do |record, message|
830
- record.ruby_llm_usages.zip(message.ruby_llm_usage_entries)
910
+ record.ruby_llm_usage_records.zip(message.ruby_llm_usage_entries)
831
911
  end
832
912
  linked_entries = linked_entry_pairs.to_h { |record, entry| [record.id, entry] }
833
913
  chat.usage_entries = ruby_llm_usages.map do |record|
@@ -856,15 +936,17 @@ module RubyLLM
856
936
  update_persisted_system_instruction(existing.first, instructions, cache_until_here:)
857
937
  else
858
938
  clear_persisted_system_instructions
859
- messages_association.create!(role: :system, content: instructions, cache_until_here: cache_until_here)
939
+ messages_association.create!(role: :system, content: instructions,
940
+ **cache_boundary_attributes(cache_until_here))
860
941
  end
861
942
  end
862
943
 
863
944
  # Rewriting the same instructions every turn would move the system row
864
945
  # behind the user messages and rebroadcast it each time.
865
946
  def update_persisted_system_instruction(record, instructions, cache_until_here:)
866
- attributes = { content: instructions }
867
- attributes[:cache_until_here] = cache_until_here if record.has_attribute?(:cache_until_here)
947
+ boundary = cache_boundary_attributes(cache_until_here)
948
+ boundary.delete(:cache_until_here) unless record.has_attribute?(:cache_until_here)
949
+ attributes = { content: instructions, **boundary }
868
950
  changed = attributes.any? { |column, value| record[column] != value }
869
951
  record.update!(attributes) if changed
870
952
  messages_association.reset
@@ -877,7 +959,7 @@ module RubyLLM
877
959
  messages_association.create!(
878
960
  role: :system,
879
961
  content: instructions,
880
- cache_until_here: cache_until_here
962
+ **cache_boundary_attributes(cache_until_here)
881
963
  )
882
964
  else
883
965
  replace_persisted_system_instructions(instructions, cache_until_here:)
@@ -885,6 +967,13 @@ module RubyLLM
885
967
  end
886
968
  end
887
969
 
970
+ def cache_boundary_attributes(cache_until_here)
971
+ ttl = RubyLLM::Message.cache_boundary_options(cache_until_here)[:ttl] if cache_until_here
972
+ attributes = { cache_until_here: cache_until_here ? true : false }
973
+ attributes[:cache_ttl] = ttl if ttl || messages_association.klass.column_names.include?('cache_ttl')
974
+ attributes
975
+ end
976
+
888
977
  def unpersisted_instructions
889
978
  @unpersisted_instructions ||= []
890
979
  end
@@ -932,7 +1021,7 @@ module RubyLLM
932
1021
  transaction do
933
1022
  @message.assign_attributes(attrs)
934
1023
  @message.save!
935
- tool_call&.update!(result: @message)
1024
+ tool_call&.update!(tool_result_attributes(tool_call, message))
936
1025
 
937
1026
  persist_content(@message, message.attachments) if message.attachments.any?
938
1027
  persist_tool_calls(message.tool_calls) if message.tool_calls.present?
@@ -950,9 +1039,16 @@ module RubyLLM
950
1039
  assign_supported_attribute(attrs, :raw_reasoning, message.raw_reasoning)
951
1040
  assign_supported_attribute(attrs, :finish_reason, message.finish_reason)
952
1041
  assign_supported_attribute(attrs, :cache_until_here, message.cache_until_here?)
1042
+ assign_supported_attribute(attrs, :cache_ttl, message.cache_ttl)
953
1043
  attrs
954
1044
  end
955
1045
 
1046
+ def tool_result_attributes(tool_call, message)
1047
+ attributes = { result: @message }
1048
+ attributes[:mcp_result] = message.mcp_result&.dump if tool_call.has_attribute?(:mcp_result)
1049
+ attributes
1050
+ end
1051
+
956
1052
  def assign_supported_attribute(attributes, name, value)
957
1053
  attributes[name] = value if messages_association.klass.column_names.include?(name.to_s)
958
1054
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # RubyLLM's persistence for MCP OAuth credentials, encrypted with Active
6
+ # Record encryption.
7
+ class MCPCredential < Record # :nodoc:
8
+ self.table_name = 'ruby_llm_mcp_credentials'
9
+
10
+ belongs_to :owner, polymorphic: true, optional: true
11
+
12
+ serialize :data, coder: JSON
13
+ encrypts :data
14
+
15
+ validates :key, presence: true
16
+
17
+ class << self
18
+ def read(key)
19
+ find_by(key:)&.data
20
+ end
21
+
22
+ def write(key, data, owner: nil)
23
+ record = find_or_initialize_by(key:)
24
+ record.update!(data:, owner: owner.is_a?(::ActiveRecord::Base) ? owner : nil)
25
+ end
26
+
27
+ def delete(key)
28
+ where(key:).delete_all
29
+ end
30
+
31
+ # Updating the row locks it on every adapter; SQLite ignores FOR UPDATE.
32
+ def synchronize(key)
33
+ transaction do
34
+ where(key:).touch_all
35
+ yield
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -32,14 +32,20 @@ module RubyLLM
32
32
  usage_entries: entries,
33
33
  tool_calls: tool_calls,
34
34
  tool_call_id: parent_tool_call&.id,
35
+ mcp_result: mcp_result,
35
36
  finish_reason: optional_column(:finish_reason),
36
37
  model: entries.reverse.find(&:succeeded?)&.model,
37
- cache_until_here: cache_until_here?
38
+ cache_until_here: cache_until_here?,
39
+ cache_ttl: cache_ttl
38
40
  )
39
41
  end
40
42
 
41
43
  def ruby_llm_usage_entries # :nodoc:
42
- ruby_llm_usages.map(&:to_entry)
44
+ ruby_llm_usage_records.map(&:to_entry)
45
+ end
46
+
47
+ def ruby_llm_usage_records # :nodoc:
48
+ preloaded_records(:ruby_llm_usages) || ruby_llm_usages.to_a
43
49
  end
44
50
 
45
51
  # Returns the model ID from the last successful attempt, or +nil+.
@@ -52,9 +58,13 @@ module RubyLLM
52
58
  to_llm.model_info
53
59
  end
54
60
 
55
- # Marks the message as a prompt cache boundary and returns it.
56
- def cache_until_here
57
- update!(cache_until_here: true)
61
+ # Marks the message as a prompt cache boundary and returns it. Pass
62
+ # +ttl:+ to give the boundary its own cache lifetime, as
63
+ # RubyLLM::Message#cache_until_here describes.
64
+ def cache_until_here(ttl: nil)
65
+ attributes = { cache_until_here: true }
66
+ attributes[:cache_ttl] = ttl if ttl || has_attribute?(:cache_ttl)
67
+ update!(attributes)
58
68
  self
59
69
  end
60
70
 
@@ -63,6 +73,12 @@ module RubyLLM
63
73
  optional_column(:cache_until_here) || false
64
74
  end
65
75
 
76
+ # Returns the cache lifetime given to the message's boundary, or +nil+
77
+ # when the chat's caching policy applies.
78
+ def cache_ttl
79
+ optional_column(:cache_ttl) if cache_until_here?
80
+ end
81
+
66
82
  # Returns the reasoning the model returned as a RubyLLM::Thinking, or +nil+.
67
83
  def thinking
68
84
  RubyLLM::Thinking.build(
@@ -94,7 +110,8 @@ module RubyLLM
94
110
 
95
111
  # Returns the tool calls as RubyLLM::ToolCall values keyed by call id.
96
112
  def tool_calls
97
- ruby_llm_tool_calls.to_h { |record| [record.tool_call_id, record.to_llm] }
113
+ records = preloaded_records(:ruby_llm_tool_calls) || ruby_llm_tool_calls
114
+ records.to_h { |record| [record.tool_call_id, record.to_llm] }
98
115
  end
99
116
 
100
117
  # Returns the tool call this message answers, or +nil+.
@@ -102,6 +119,15 @@ module RubyLLM
102
119
  ruby_llm_parent_tool_call&.to_llm
103
120
  end
104
121
 
122
+ # Returns the RubyLLM::MCP::Result kept for an MCP tool with a UI, as
123
+ # RubyLLM::Message#mcp_result does, or +nil+. It persists on the tool
124
+ # call, so the UI renders again after a reload.
125
+ def mcp_result
126
+ record = ruby_llm_parent_tool_call
127
+ data = record[:mcp_result] if record&.has_attribute?(:mcp_result)
128
+ RubyLLM::MCP::Result.load(data) if data
129
+ end
130
+
105
131
  # Returns the message records that answer this message's tool calls.
106
132
  def tool_results
107
133
  ruby_llm_tool_calls.filter_map(&:result)
@@ -183,7 +209,6 @@ module RubyLLM
183
209
  action_text_attachments = action_text_attachment_sources(content)
184
210
  return [] unless content_attachments?(action_text_attachments)
185
211
 
186
- @_tempfiles = []
187
212
  collect_attachments(action_text_attachments)
188
213
  end
189
214
  end
@@ -8,7 +8,7 @@ module RubyLLM
8
8
  # point at it through +belongs_to :model+. The +listed+ scope returns the
9
9
  # models the provider still serves; +unlisted+ returns the ones kept only
10
10
  # because a chat references them.
11
- class Model < ::ActiveRecord::Base
11
+ class Model < Record
12
12
  self.table_name = 'ruby_llm_models'
13
13
 
14
14
  validates :model_id, presence: true, uniqueness: { scope: :provider }
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # RubyLLM's record of the provider files each Active Storage blob was
6
+ # uploaded to, so a chat loaded in another process sends the file it
7
+ # already uploaded instead of uploading the blob again. Rows name the
8
+ # blob by its key, which Active Storage never hands out twice, so a row
9
+ # left by a deleted blob can never match a newer one.
10
+ class ProviderFile < Record # :nodoc:
11
+ self.table_name = 'ruby_llm_provider_files'
12
+
13
+ validates :blob_key, :provider, :account, :file_id, presence: true
14
+
15
+ # Returns a store for the blob whose key the block returns, or +nil+
16
+ # while the table has not been migrated.
17
+ def self.store(&)
18
+ Store.new(&) if table_exists?
19
+ end
20
+
21
+ # Deletes the uploads recorded for +blob+ as Active Storage destroys it.
22
+ def self.forget_blob(blob)
23
+ where(blob_key: blob.key).delete_all if table_exists?
24
+ end
25
+
26
+ # The uploads of one blob. The block finds the blob the first time a
27
+ # provider asks, so attachments that are never uploaded cost nothing.
28
+ class Store # :nodoc:
29
+ def initialize(&blob_key)
30
+ @find_blob_key = blob_key
31
+ end
32
+
33
+ def fetch(provider:, account:)
34
+ record = blob_key && ProviderFile.find_by(blob_key:, provider:, account:)
35
+ RubyLLM::UploadedFile.new(id: record.file_id, provider:, expires_at: record.expires_at) if record
36
+ end
37
+
38
+ # Another process can record an upload of the same blob first; its
39
+ # file serves as well as this one.
40
+ def store(upload, provider:, account:)
41
+ return unless blob_key
42
+
43
+ ProviderFile.find_or_initialize_by(blob_key:, provider:, account:)
44
+ .update!(file_id: upload.id, expires_at: upload.expires_at)
45
+ rescue ::ActiveRecord::RecordNotUnique
46
+ nil
47
+ end
48
+
49
+ # Only a row that still names the missing file goes: another process
50
+ # may have recorded a newer upload meanwhile.
51
+ def forget(id, provider:, account:)
52
+ ProviderFile.where(blob_key:, provider:, account:, file_id: id).delete_all if blob_key
53
+ end
54
+
55
+ private
56
+
57
+ def blob_key
58
+ @blob_key = @find_blob_key.call unless defined?(@blob_key)
59
+ @blob_key
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Abstract base for RubyLLM's model, tool-call, usage, and batch records.
6
+ # Inherits the application's default connection unless configured otherwise.
7
+ # Keep these records on the same database and connection pool as chats and
8
+ # messages to preserve foreign keys and shared transactions.
9
+ #
10
+ # Rails.application.config.to_prepare do
11
+ # RubyLLM::ActiveRecord::Record.connection_specification_name =
12
+ # LlmRecord.connection_specification_name
13
+ # end
14
+ #
15
+ class Record < ::ActiveRecord::Base
16
+ self.abstract_class = true
17
+ end
18
+ end
19
+ end
@@ -5,7 +5,7 @@ require 'ruby_llm/active_record/payload_helpers'
5
5
  module RubyLLM
6
6
  module ActiveRecord
7
7
  # RubyLLM's private persistence for provider tool calls.
8
- class ToolCall < ::ActiveRecord::Base # :nodoc:
8
+ class ToolCall < Record # :nodoc:
9
9
  self.table_name = 'ruby_llm_tool_calls'
10
10
 
11
11
  include PayloadHelpers
@@ -3,11 +3,12 @@
3
3
  module RubyLLM
4
4
  module ActiveRecord
5
5
  # RubyLLM's private normalized persistence for one provider attempt.
6
- class Usage < ::ActiveRecord::Base # :nodoc:
6
+ class Usage < Record # :nodoc:
7
7
  self.table_name = 'ruby_llm_usages'
8
8
 
9
- belongs_to :chat, polymorphic: true
9
+ belongs_to :chat, polymorphic: true, optional: true
10
10
  belongs_to :message, polymorphic: true, optional: true
11
+ belongs_to :owner, polymorphic: true, optional: true
11
12
 
12
13
  validates :operation, inclusion: { in: ::RubyLLM::Accounting::Usage::Entry::OPERATIONS.map(&:to_s) }
13
14
  validates :status, inclusion: { in: ::RubyLLM::Accounting::Usage::Entry::STATUSES.map(&:to_s) }
@@ -15,13 +16,62 @@ module RubyLLM
15
16
 
16
17
  scope :chronological, -> { order(created_at: :asc, id: :asc) }
17
18
 
19
+ # Writes an entry that belongs to no chat record, attributed to its
20
+ # owner. A ledger that cannot store such rows yet is skipped, and a
21
+ # failed write is logged so it never breaks the operation that billed
22
+ # it. A savepoint keeps a failed insert from aborting the caller's
23
+ # transaction, and a thread that held no connection gives back the
24
+ # one it borrowed.
25
+ def self.record(entry)
26
+ return unless entry.model
27
+
28
+ connection_pool.with_connection do
29
+ next unless ledger_available?
30
+
31
+ transaction(requires_new: true) { create!(attributes_for(entry)) }
32
+ end
33
+ rescue StandardError => e
34
+ RubyLLM.logger.warn("RubyLLM could not record #{entry.operation} usage: #{e.class}: #{e.message}")
35
+ nil
36
+ end
37
+
38
+ def self.ledger_available?
39
+ table_exists? && columns_hash['chat_id']&.null
40
+ end
41
+
42
+ def self.attributes_for(entry)
43
+ tokens = entry.tokens
44
+ cost = entry.cost
45
+ attributes = {
46
+ operation: entry.operation,
47
+ provider: entry.provider,
48
+ model: entry.model,
49
+ status: entry.status,
50
+ input_tokens: tokens.input,
51
+ output_tokens: tokens.output,
52
+ cache_read_tokens: tokens.cache_read,
53
+ cache_write_tokens: tokens.cache_write,
54
+ thinking_tokens: tokens.thinking,
55
+ input_cost: cost.input,
56
+ output_cost: cost.output,
57
+ cache_read_cost: cost.cache_read,
58
+ cache_write_cost: cost.cache_write,
59
+ thinking_cost: cost.thinking,
60
+ total_cost: cost.total
61
+ }
62
+ attributes[:server_tool_use] = tokens.server_tool_use if column_names.include?('server_tool_use')
63
+ attributes[:owner] = entry.owner if entry.owner.is_a?(::ActiveRecord::Base) && column_names.include?('owner_id')
64
+ attributes
65
+ end
66
+
18
67
  def tokens
19
68
  RubyLLM::Tokens.new(
20
69
  input: input_tokens,
21
70
  output: output_tokens,
22
71
  cache_read: cache_read_tokens,
23
72
  cache_write: cache_write_tokens,
24
- thinking: thinking_tokens
73
+ thinking: thinking_tokens,
74
+ server_tool_use: (self[:server_tool_use] if has_attribute?(:server_tool_use))
25
75
  )
26
76
  end
27
77