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.
- checksums.yaml +4 -4
- data/README.md +65 -8
- data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
- data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
- data/lib/ruby_llm/accounting/usage.rb +86 -49
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
- data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
- data/lib/ruby_llm/active_record/message_methods.rb +32 -7
- data/lib/ruby_llm/active_record/model.rb +1 -1
- data/lib/ruby_llm/active_record/provider_file.rb +64 -0
- data/lib/ruby_llm/active_record/record.rb +19 -0
- data/lib/ruby_llm/active_record/tool_call.rb +1 -1
- data/lib/ruby_llm/active_record/usage.rb +53 -3
- data/lib/ruby_llm/agent.rb +198 -33
- data/lib/ruby_llm/aliases.json +66 -8
- data/lib/ruby_llm/attachment.rb +54 -10
- data/lib/ruby_llm/batch.rb +26 -3
- data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
- data/lib/ruby_llm/chat/tool_search.rb +26 -0
- data/lib/ruby_llm/chat.rb +488 -82
- data/lib/ruby_llm/choice.rb +38 -0
- data/lib/ruby_llm/configuration.rb +36 -11
- data/lib/ruby_llm/context.rb +45 -32
- data/lib/ruby_llm/cost.rb +42 -1
- data/lib/ruby_llm/embedding.rb +9 -2
- data/lib/ruby_llm/error.rb +34 -2
- data/lib/ruby_llm/evaluation/assertions.rb +40 -0
- data/lib/ruby_llm/evaluation/case.rb +52 -0
- data/lib/ruby_llm/evaluation/dataset.rb +65 -0
- data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
- data/lib/ruby_llm/evaluation/evidence.rb +125 -0
- data/lib/ruby_llm/evaluation/minitest.rb +26 -0
- data/lib/ruby_llm/evaluation/report.rb +103 -0
- data/lib/ruby_llm/evaluation/result.rb +75 -0
- data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
- data/lib/ruby_llm/evaluation/rspec.rb +23 -0
- data/lib/ruby_llm/evaluation/runner.rb +93 -0
- data/lib/ruby_llm/evaluation/trial.rb +101 -0
- data/lib/ruby_llm/evaluation.rb +289 -0
- data/lib/ruby_llm/image.rb +12 -4
- data/lib/ruby_llm/judge/builder.rb +51 -0
- data/lib/ruby_llm/judge/data.rb +43 -0
- data/lib/ruby_llm/judge/question.rb +101 -0
- data/lib/ruby_llm/judge.rb +179 -0
- data/lib/ruby_llm/judgment.rb +125 -0
- data/lib/ruby_llm/mcp/apps.rb +25 -0
- data/lib/ruby_llm/mcp/client.rb +213 -0
- data/lib/ruby_llm/mcp/collection.rb +56 -0
- data/lib/ruby_llm/mcp/content.rb +44 -0
- data/lib/ruby_llm/mcp/error.rb +27 -0
- data/lib/ruby_llm/mcp/http.rb +379 -0
- data/lib/ruby_llm/mcp/input_request.rb +118 -0
- data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
- data/lib/ruby_llm/mcp/listener.rb +168 -0
- data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
- data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
- data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
- data/lib/ruby_llm/mcp/oauth.rb +653 -0
- data/lib/ruby_llm/mcp/param_headers.rb +83 -0
- data/lib/ruby_llm/mcp/prompt.rb +71 -0
- data/lib/ruby_llm/mcp/resource.rb +84 -0
- data/lib/ruby_llm/mcp/resource_template.rb +99 -0
- data/lib/ruby_llm/mcp/result.rb +76 -0
- data/lib/ruby_llm/mcp/session_expired.rb +10 -0
- data/lib/ruby_llm/mcp/stdio.rb +250 -0
- data/lib/ruby_llm/mcp/task.rb +151 -0
- data/lib/ruby_llm/mcp/tool.rb +130 -0
- data/lib/ruby_llm/mcp.rb +1101 -0
- data/lib/ruby_llm/message.rb +41 -6
- data/lib/ruby_llm/model.rb +2 -1
- data/lib/ruby_llm/models/aliases.rb +5 -2
- data/lib/ruby_llm/models/lookup.rb +80 -0
- data/lib/ruby_llm/models/registry.rb +2 -2
- data/lib/ruby_llm/models/schema.rb +1 -1
- data/lib/ruby_llm/models.json +23082 -10158
- data/lib/ruby_llm/models.rb +20 -69
- data/lib/ruby_llm/moderation.rb +7 -2
- data/lib/ruby_llm/ocr.rb +7 -2
- data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
- data/lib/ruby_llm/open_telemetry.rb +82 -0
- data/lib/ruby_llm/probability.rb +30 -0
- data/lib/ruby_llm/progress.rb +44 -0
- data/lib/ruby_llm/prompt/context.rb +55 -0
- data/lib/ruby_llm/prompt.rb +1 -1
- data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
- data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
- data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
- data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
- data/lib/ruby_llm/protocol/streaming.rb +38 -31
- data/lib/ruby_llm/protocol.rb +192 -40
- data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
- data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
- data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
- data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
- data/lib/ruby_llm/protocols/anthropic.rb +20 -1
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
- data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
- data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
- data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
- data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
- data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
- data/lib/ruby_llm/protocols/cohere.rb +1 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
- data/lib/ruby_llm/protocols/converse/media.rb +16 -2
- data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
- data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
- data/lib/ruby_llm/protocols/converse.rb +3 -5
- data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
- data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
- data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
- data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
- data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
- data/lib/ruby_llm/protocols/gemini.rb +1 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
- data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
- data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
- data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
- data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
- data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
- data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
- data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
- data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
- data/lib/ruby_llm/protocols/openai/files.rb +0 -7
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
- data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
- data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
- data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
- data/lib/ruby_llm/protocols/responses/media.rb +11 -7
- data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
- data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
- data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
- data/lib/ruby_llm/protocols/responses.rb +1 -0
- data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
- data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
- data/lib/ruby_llm/protocols/system_one.rb +27 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
- data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +87 -15
- data/lib/ruby_llm/providers/anthropic.rb +23 -0
- data/lib/ruby_llm/providers/azure/chat.rb +4 -0
- data/lib/ruby_llm/providers/azure/images.rb +2 -2
- data/lib/ruby_llm/providers/azure/media.rb +8 -3
- data/lib/ruby_llm/providers/azure/videos.rb +7 -2
- data/lib/ruby_llm/providers/azure.rb +24 -4
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
- data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
- data/lib/ruby_llm/providers/bedrock.rb +38 -6
- data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
- data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
- data/lib/ruby_llm/providers/hetzner.rb +41 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
- data/lib/ruby_llm/providers/mistral/media.rb +8 -4
- data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
- data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -2
- data/lib/ruby_llm/providers/ollama/models.rb +2 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
- data/lib/ruby_llm/providers/openai.rb +14 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
- data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
- data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
- data/lib/ruby_llm/providers/perplexity.rb +11 -0
- data/lib/ruby_llm/providers/typesafe.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/vertexai.rb +27 -28
- data/lib/ruby_llm/providers/xai/chat.rb +6 -0
- data/lib/ruby_llm/providers/xai/images.rb +9 -10
- data/lib/ruby_llm/providers/xai/responses.rb +10 -8
- data/lib/ruby_llm/providers/xai/videos.rb +11 -9
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/railtie.rb +11 -0
- data/lib/ruby_llm/request_shape/part.rb +117 -0
- data/lib/ruby_llm/request_shape/problem.rb +63 -0
- data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
- data/lib/ruby_llm/request_shape/turn.rb +54 -0
- data/lib/ruby_llm/request_shape.rb +160 -0
- data/lib/ruby_llm/rerank.rb +9 -2
- data/lib/ruby_llm/research_job.rb +21 -5
- data/lib/ruby_llm/score.rb +42 -0
- data/lib/ruby_llm/search_results.rb +2 -2
- data/lib/ruby_llm/server_tool_call.rb +10 -2
- data/lib/ruby_llm/speech.rb +8 -1
- data/lib/ruby_llm/support/cancellation.rb +39 -0
- data/lib/ruby_llm/support/inspectable.rb +3 -2
- data/lib/ruby_llm/support/instrumentation.rb +38 -1
- data/lib/ruby_llm/support/process_cache.rb +60 -0
- data/lib/ruby_llm/support/progress_reporter.rb +33 -0
- data/lib/ruby_llm/thinking.rb +2 -1
- data/lib/ruby_llm/tokens.rb +57 -14
- data/lib/ruby_llm/tool.rb +50 -3
- data/lib/ruby_llm/tool_call.rb +17 -2
- data/lib/ruby_llm/transcription.rb +10 -3
- data/lib/ruby_llm/transport/connection.rb +127 -44
- data/lib/ruby_llm/transport/error_middleware.rb +90 -24
- data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
- data/lib/ruby_llm/transport/json_response.rb +33 -0
- data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +5 -3
- data/lib/ruby_llm/video_job.rb +48 -5
- data/lib/ruby_llm.rb +106 -5
- data/lib/tasks/ruby_llm/evaluations.rb +43 -0
- data/lib/tasks/ruby_llm.rake +6 -9
- data/skills/rubyllm/SKILL.md +33 -12
- metadata +95 -37
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
- 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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
704
|
+
# chat is complete or #waiting?. See RubyLLM::Chat#step.
|
|
621
705
|
#
|
|
622
|
-
# chat.step until chat.complete? || chat.
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
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!(
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
|