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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A RequestShape describes a conversation request as the provider received
|
|
5
|
+
# it, without its contents: each turn's role and parts with their sizes,
|
|
6
|
+
# the tools on offer, the thinking settings, and the problems RubyLLM finds
|
|
7
|
+
# in it, such as a part with no data or a tool call with no result. Read it
|
|
8
|
+
# from Error#request_shape when a provider rejects a request without saying
|
|
9
|
+
# what is wrong with it.
|
|
10
|
+
#
|
|
11
|
+
# begin
|
|
12
|
+
# chat.ask "What's due today?"
|
|
13
|
+
# rescue RubyLLM::BadRequestError => e
|
|
14
|
+
# Rails.logger.error("#{e.message}\n#{e.request_shape}")
|
|
15
|
+
# Rails.error.report(e, context: { request_shape: e.request_shape.to_h })
|
|
16
|
+
# raise
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# A shape holds no text, tool arguments, tool results, signatures, URLs,
|
|
20
|
+
# file data, or setting values beyond thinking settings, so you can log it
|
|
21
|
+
# and send it to an error tracker. A long conversation keeps its first 10
|
|
22
|
+
# and last 50 turns.
|
|
23
|
+
class RequestShape
|
|
24
|
+
include Support::Inspectable
|
|
25
|
+
|
|
26
|
+
# The provider's slug, such as <tt>"gemini"</tt>.
|
|
27
|
+
attr_reader :provider
|
|
28
|
+
|
|
29
|
+
# The id of the model the request went to.
|
|
30
|
+
attr_reader :model
|
|
31
|
+
|
|
32
|
+
# Which model call of the current turn the request makes, counting from
|
|
33
|
+
# 1, so +3+ follows two tool rounds. Returns +nil+ when the request holds
|
|
34
|
+
# no user turn to count from.
|
|
35
|
+
attr_reader :step
|
|
36
|
+
|
|
37
|
+
# The names of the payload's top-level fields, without their values.
|
|
38
|
+
attr_reader :payload_keys
|
|
39
|
+
|
|
40
|
+
# The thinking settings the request sends, such as an effort or a budget,
|
|
41
|
+
# as a Hash of the provider's setting names and values.
|
|
42
|
+
attr_reader :thinking_settings
|
|
43
|
+
|
|
44
|
+
# The Part objects of the instructions the request sends apart from its
|
|
45
|
+
# turns, or an empty Array when the provider takes instructions as a turn.
|
|
46
|
+
attr_reader :instructions
|
|
47
|
+
|
|
48
|
+
# The Turn objects of the conversation, in the order the request sends
|
|
49
|
+
# them. A long conversation keeps its first 10 and last 50 turns.
|
|
50
|
+
attr_reader :turns
|
|
51
|
+
|
|
52
|
+
# The number of turns the request sends, including omitted ones.
|
|
53
|
+
attr_reader :turn_count
|
|
54
|
+
|
|
55
|
+
# The number of turns left out of #turns.
|
|
56
|
+
attr_reader :omitted_turns
|
|
57
|
+
|
|
58
|
+
# The names of the tools the request offers, in order. Provider tools
|
|
59
|
+
# keep the name their provider's API gives them.
|
|
60
|
+
attr_reader :tool_names
|
|
61
|
+
|
|
62
|
+
# The ToolRound objects of the turns kept, one for each model turn that
|
|
63
|
+
# called tools.
|
|
64
|
+
attr_reader :tool_rounds
|
|
65
|
+
|
|
66
|
+
# The Problem objects RubyLLM finds in the request, across every turn.
|
|
67
|
+
attr_reader :problems
|
|
68
|
+
|
|
69
|
+
# Creates a shape. Protocols build shapes in
|
|
70
|
+
# Protocol#parse_request_shape; every attribute but +turns+ is optional.
|
|
71
|
+
def initialize(turns:, provider: nil, model: nil, step: nil, payload_keys: [], thinking_settings: {},
|
|
72
|
+
instructions: [], turn_count: nil, omitted_turns: 0, tool_names: [], tool_rounds: [], problems: [])
|
|
73
|
+
@turns = turns
|
|
74
|
+
@provider = provider
|
|
75
|
+
@model = model
|
|
76
|
+
@step = step
|
|
77
|
+
@payload_keys = payload_keys
|
|
78
|
+
@thinking_settings = thinking_settings
|
|
79
|
+
@instructions = instructions
|
|
80
|
+
@turn_count = turn_count || turns.size
|
|
81
|
+
@omitted_turns = omitted_turns
|
|
82
|
+
@tool_names = tool_names
|
|
83
|
+
@tool_rounds = tool_rounds
|
|
84
|
+
@problems = problems
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the shape as text, one turn per line:
|
|
88
|
+
#
|
|
89
|
+
# gemini, gemini-2.5-flash, model call 1 of the turn
|
|
90
|
+
# payload keys: contents, generationConfig, tools
|
|
91
|
+
# thinking: includeThoughts true, thinkingBudget -1
|
|
92
|
+
# #0 user: text (17 chars)
|
|
93
|
+
# #1 model: call find_tasks (args 40 chars), signed
|
|
94
|
+
# #2 user: result find_tasks (6859 chars)
|
|
95
|
+
# #3 model: thinking (no text), signed, text (1075 chars)
|
|
96
|
+
# #4 user: text (31 chars)
|
|
97
|
+
# tools: find_tasks
|
|
98
|
+
# tool round at #1: 1 call, 1 result, paired
|
|
99
|
+
# problem at #3, part 0: thinking part carries no data
|
|
100
|
+
#
|
|
101
|
+
def to_s
|
|
102
|
+
[summary_line, list_line('payload keys', payload_keys), settings_line, list_line('instructions', instructions),
|
|
103
|
+
*turn_lines, list_line('tools', tool_names), *tool_rounds.map(&:to_s), *problem_lines].compact.join("\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the shape as a Hash with the same keys every time, for
|
|
107
|
+
# structured logs and error trackers.
|
|
108
|
+
def to_h
|
|
109
|
+
{
|
|
110
|
+
provider: provider, model: model, step: step, payload_keys: payload_keys,
|
|
111
|
+
thinking_settings: thinking_settings, instructions: instructions.map(&:to_h),
|
|
112
|
+
turn_count: turn_count, omitted_turns: omitted_turns, turns: turns.map(&:to_h),
|
|
113
|
+
tool_names: tool_names, tool_rounds: tool_rounds.map(&:to_h), problems: problems.map(&:to_h)
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def inspect_attributes # :nodoc:
|
|
118
|
+
{ provider: provider, model: model, turns: turn_count, problems: problems.size }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def summary_line
|
|
124
|
+
line = [provider, model, step_description].compact.join(', ')
|
|
125
|
+
line unless line.empty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def step_description
|
|
129
|
+
return unless step
|
|
130
|
+
|
|
131
|
+
rounds = step - 1
|
|
132
|
+
description = "model call #{step} of the turn"
|
|
133
|
+
return description unless rounds.positive?
|
|
134
|
+
|
|
135
|
+
"#{description}, after #{rounds} tool #{rounds == 1 ? 'round' : 'rounds'}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def list_line(label, items)
|
|
139
|
+
"#{label}: #{items.join(', ')}" if items.any?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def settings_line
|
|
143
|
+
"thinking: #{thinking_settings.map { |name, value| "#{name} #{value}" }.join(', ')}" if thinking_settings.any?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def turn_lines
|
|
147
|
+
turns.each_cons(2).flat_map { |turn, following| [turn.to_s, *gap_line(turn, following)] }
|
|
148
|
+
.push(*turns.last&.to_s)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def gap_line(turn, following)
|
|
152
|
+
gap = following.index - turn.index - 1
|
|
153
|
+
"(#{gap} #{gap == 1 ? 'turn' : 'turns'} omitted)" if gap.positive?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def problem_lines
|
|
157
|
+
problems.any? ? problems.map(&:to_s) : ['no problems found']
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/ruby_llm/rerank.rb
CHANGED
|
@@ -49,6 +49,8 @@ module RubyLLM
|
|
|
49
49
|
Cost.new(tokens:, model: model_info, category: :embeddings)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
attr_writer :model_info # :nodoc:
|
|
53
|
+
|
|
52
54
|
def model_info # :nodoc:
|
|
53
55
|
@model_info ||= RubyLLM.models.find(model)
|
|
54
56
|
rescue ModelNotFoundError
|
|
@@ -64,6 +66,8 @@ module RubyLLM
|
|
|
64
66
|
# provider-specific and have no cross-provider default. +top_n:+
|
|
65
67
|
# limits how many results come back. +provider_options:+ merges
|
|
66
68
|
# options into the request in the provider's vocabulary.
|
|
69
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
70
|
+
# over RubyLLM.with_usage_owner.
|
|
67
71
|
#
|
|
68
72
|
# RubyLLM.rerank "what is ruby", docs, model: "voyageai/rerank-2.5-lite", provider: :openrouter
|
|
69
73
|
#
|
|
@@ -74,7 +78,8 @@ module RubyLLM
|
|
|
74
78
|
context: nil,
|
|
75
79
|
top_n: nil,
|
|
76
80
|
provider_options: {},
|
|
77
|
-
metadata: nil
|
|
81
|
+
metadata: nil,
|
|
82
|
+
owner: nil)
|
|
78
83
|
config = context&.config || RubyLLM.config
|
|
79
84
|
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
80
85
|
config: config)
|
|
@@ -94,7 +99,9 @@ module RubyLLM
|
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
RubyLLM.instrument('rerank.ruby_llm', payload, config: config) do |event|
|
|
97
|
-
result =
|
|
102
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
103
|
+
provider_instance.rerank(query, documents, model:, top_n:, provider_options:)
|
|
104
|
+
end
|
|
98
105
|
event[:result] = result
|
|
99
106
|
event[:tokens] = result.tokens
|
|
100
107
|
event[:cost] = result.cost
|
|
@@ -68,14 +68,18 @@ module RubyLLM
|
|
|
68
68
|
# Submits one research task without waiting. +provider:+ and +agent:+
|
|
69
69
|
# are required; +with:+ attaches documents or images where supported.
|
|
70
70
|
# +provider_tools:+ accepts an array of aliases or a Hash of aliases
|
|
71
|
-
# and their options, as on Chat#with_provider_tools.
|
|
71
|
+
# and their options, as on Chat#with_provider_tools. The job records
|
|
72
|
+
# its usage, attributed to +owner:+ or the owner of
|
|
73
|
+
# RubyLLM.with_usage_owner, when it finishes.
|
|
72
74
|
def self.research_later(prompt, provider:, agent:, with: nil, provider_tools: nil,
|
|
73
|
-
context: nil, provider_options: {}, metadata: nil)
|
|
75
|
+
context: nil, provider_options: {}, metadata: nil, owner: nil)
|
|
74
76
|
config = context&.config || RubyLLM.config
|
|
75
77
|
instance = Provider.resolve!(provider).new(config)
|
|
76
78
|
payload = { provider: instance.slug, agent:, prompt:, metadata: }
|
|
77
79
|
RubyLLM.instrument('research_job.ruby_llm', payload, config:) do |event|
|
|
78
|
-
job =
|
|
80
|
+
job = Accounting::Usage.owned_by(owner) do
|
|
81
|
+
instance.research_later(prompt, agent:, with:, provider_tools:, provider_options:)
|
|
82
|
+
end
|
|
79
83
|
event[:job_id] = job.id
|
|
80
84
|
event[:status] = job.status
|
|
81
85
|
job
|
|
@@ -85,9 +89,9 @@ module RubyLLM
|
|
|
85
89
|
# Runs a research task and returns its Message. On a timeout or
|
|
86
90
|
# interrupt, attempts to cancel the remote task before raising an
|
|
87
91
|
# error retaining the job. +timeout:+ and +interval:+ are seconds.
|
|
88
|
-
def self.research(prompt, timeout: 600, interval: 5, **
|
|
92
|
+
def self.research(prompt, timeout: 600, interval: 5, **)
|
|
89
93
|
validate_polling_options(timeout, interval)
|
|
90
|
-
job = research_later(prompt, **
|
|
94
|
+
job = research_later(prompt, **)
|
|
91
95
|
job.wait(timeout:, interval:).message
|
|
92
96
|
rescue Interrupt => e
|
|
93
97
|
raise unless job
|
|
@@ -123,6 +127,7 @@ module RubyLLM
|
|
|
123
127
|
@provider = provider.to_sym
|
|
124
128
|
@agent = agent
|
|
125
129
|
@protocol = protocol
|
|
130
|
+
@usage_owner = Accounting::Usage.owner
|
|
126
131
|
apply_state(state)
|
|
127
132
|
end
|
|
128
133
|
|
|
@@ -216,12 +221,23 @@ module RubyLLM
|
|
|
216
221
|
raise Error.new("Research request failed: #{e.message} (job #{id})", job: self, response:), cause: e
|
|
217
222
|
end
|
|
218
223
|
|
|
224
|
+
# A job found by ID may have been submitted and recorded elsewhere, so
|
|
225
|
+
# only a finish this object observes records usage.
|
|
219
226
|
def apply_state(state)
|
|
227
|
+
observed_finish = pending? && state.fetch(:status) != :pending
|
|
220
228
|
@status = state.fetch(:status)
|
|
221
229
|
@raw = state[:raw]
|
|
222
230
|
@error = state[:error]
|
|
223
231
|
@message = state[:message]
|
|
224
232
|
@tokens = state[:tokens] || @message&.tokens || Tokens.new
|
|
233
|
+
record_usage if observed_finish
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def record_usage
|
|
237
|
+
status = { completed: :succeeded, incomplete: :succeeded, cancelled: :cancelled }.fetch(@status, :failed)
|
|
238
|
+
entry = Accounting::Usage::Entry.new(operation: :research, provider:, model: agent, status:, tokens:, cost:,
|
|
239
|
+
owner: @usage_owner)
|
|
240
|
+
Accounting::Usage.report(entry, config: @protocol.config)
|
|
225
241
|
end
|
|
226
242
|
|
|
227
243
|
def attempt_cancellation
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A probability-weighted position on a judgment's ordered levels.
|
|
5
|
+
class Score
|
|
6
|
+
include Support::Inspectable
|
|
7
|
+
|
|
8
|
+
# Returns the weighted score, which can fall between level indexes.
|
|
9
|
+
attr_reader :score
|
|
10
|
+
|
|
11
|
+
# Returns the ordered descriptions, preserving structured Hashes and Arrays.
|
|
12
|
+
attr_reader :levels
|
|
13
|
+
|
|
14
|
+
# Returns each zero-based Integer level index's probability.
|
|
15
|
+
attr_reader :probabilities
|
|
16
|
+
|
|
17
|
+
# Returns the provider's confidence in the distribution, between 0 and 1.
|
|
18
|
+
attr_reader :confidence
|
|
19
|
+
|
|
20
|
+
def initialize(score:, levels:, probabilities:, confidence:) # :nodoc:
|
|
21
|
+
@score = score
|
|
22
|
+
@levels = Judge::Data.copy(levels)
|
|
23
|
+
@probabilities = probabilities.dup.freeze
|
|
24
|
+
@confidence = confidence
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns :score.
|
|
29
|
+
def type
|
|
30
|
+
:score
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the answer as a Hash.
|
|
34
|
+
def to_h
|
|
35
|
+
{ type:, score:, levels:, probabilities:, confidence: }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inspect_attributes # :nodoc:
|
|
39
|
+
{ score:, confidence: }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -39,22 +39,30 @@ module RubyLLM
|
|
|
39
39
|
# conversation is sent back to the provider.
|
|
40
40
|
attr_reader :raw
|
|
41
41
|
|
|
42
|
+
# The HTML of the search suggestions the provider requires you to show
|
|
43
|
+
# with a grounded answer, such as Google's for Google Search grounding,
|
|
44
|
+
# or +nil+. Only the live response carries them: #to_h leaves them out,
|
|
45
|
+
# so a persisted chat never stores them.
|
|
46
|
+
attr_reader :search_suggestions
|
|
47
|
+
|
|
42
48
|
def self.from_h(data) # :nodoc:
|
|
43
49
|
data = Support::Utils.deep_symbolize_keys(data)
|
|
44
50
|
new(type: data[:type], name: data[:name], id: data[:id], input: data[:input],
|
|
45
51
|
result: data[:result], raw: data[:raw])
|
|
46
52
|
end
|
|
47
53
|
|
|
48
|
-
def initialize(type:, raw:, name: nil, id: nil, input: nil, result: nil) # :nodoc:
|
|
54
|
+
def initialize(type:, raw:, name: nil, id: nil, input: nil, result: nil, search_suggestions: nil) # :nodoc:
|
|
49
55
|
@type = type
|
|
50
56
|
@name = name
|
|
51
57
|
@id = id
|
|
52
58
|
@input = input
|
|
53
59
|
@result = result
|
|
54
60
|
@raw = raw
|
|
61
|
+
@search_suggestions = search_suggestions
|
|
55
62
|
end
|
|
56
63
|
|
|
57
|
-
# Returns the call's attributes as a Hash, omitting +nil+ values
|
|
64
|
+
# Returns the call's attributes as a Hash, omitting +nil+ values and
|
|
65
|
+
# #search_suggestions.
|
|
58
66
|
def to_h
|
|
59
67
|
{
|
|
60
68
|
type: type,
|
data/lib/ruby_llm/speech.rb
CHANGED
|
@@ -54,6 +54,8 @@ module RubyLLM
|
|
|
54
54
|
# that is not in the registry. +provider_options:+ takes options in the
|
|
55
55
|
# provider's request vocabulary, such as +instructions:+ and +speed:+
|
|
56
56
|
# for OpenAI, and merges them into the request as-is.
|
|
57
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
58
|
+
# over RubyLLM.with_usage_owner.
|
|
57
59
|
#
|
|
58
60
|
# speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
|
|
59
61
|
# speech.save "welcome.mp3"
|
|
@@ -82,6 +84,7 @@ module RubyLLM
|
|
|
82
84
|
context: nil,
|
|
83
85
|
provider_options: {},
|
|
84
86
|
metadata: nil,
|
|
87
|
+
owner: nil,
|
|
85
88
|
&block)
|
|
86
89
|
config = context&.config || RubyLLM.config
|
|
87
90
|
model ||= config.default_speech_model
|
|
@@ -105,7 +108,9 @@ module RubyLLM
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
RubyLLM.instrument('speech.ruby_llm', payload, config: config) do |event|
|
|
108
|
-
result =
|
|
111
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
112
|
+
provider_instance.speak(input, model:, voice:, format:, provider_options:, &block)
|
|
113
|
+
end
|
|
109
114
|
event[:result] = result
|
|
110
115
|
event[:response_model] = result.model
|
|
111
116
|
event[:voice] = result.voice
|
|
@@ -137,6 +142,8 @@ module RubyLLM
|
|
|
137
142
|
Cost.new(tokens:, model: model_info, category: :audio_tokens)
|
|
138
143
|
end
|
|
139
144
|
|
|
145
|
+
attr_writer :model_info # :nodoc:
|
|
146
|
+
|
|
140
147
|
def model_info # :nodoc:
|
|
141
148
|
@model_info ||= RubyLLM.models.find(model)
|
|
142
149
|
rescue ModelNotFoundError
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support
|
|
5
|
+
# Lets work that runs inside a chat operation, such as a tool waiting on
|
|
6
|
+
# a remote server, stop when the chat is cancelled. The chat installs a
|
|
7
|
+
# checkpoint for the current fiber; #check raises CancelledError once
|
|
8
|
+
# the chat has been cancelled.
|
|
9
|
+
module Cancellation # :nodoc:
|
|
10
|
+
KEY = :ruby_llm_cancellation_checkpoint
|
|
11
|
+
CHECK_INTERVAL = 0.1
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def watch(checkpoint)
|
|
16
|
+
previous = Thread.current[KEY]
|
|
17
|
+
Thread.current[KEY] = checkpoint
|
|
18
|
+
yield
|
|
19
|
+
ensure
|
|
20
|
+
Thread.current[KEY] = previous
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def check
|
|
24
|
+
Thread.current[KEY]&.call
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pause(seconds)
|
|
28
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
29
|
+
loop do
|
|
30
|
+
check
|
|
31
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
|
+
break unless remaining.positive?
|
|
33
|
+
|
|
34
|
+
sleep [remaining, CHECK_INTERVAL].min
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -17,9 +17,10 @@ module RubyLLM
|
|
|
17
17
|
attributes = inspect_attributes
|
|
18
18
|
.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
19
19
|
.map { |name, value| "#{name}: #{format_for_inspect(value)}" }
|
|
20
|
-
|
|
20
|
+
class_name = self.class.ancestors.find(&:name).name
|
|
21
|
+
return "#<#{class_name}>" if attributes.empty?
|
|
21
22
|
|
|
22
|
-
"#<#{
|
|
23
|
+
"#<#{class_name} #{attributes.join(', ')}>"
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def pretty_print(printer) # :nodoc:
|
|
@@ -5,15 +5,51 @@ module RubyLLM
|
|
|
5
5
|
module Instrumentation # :nodoc:
|
|
6
6
|
WORKFLOW_CONTEXT_KEY = :ruby_llm_workflow_context
|
|
7
7
|
|
|
8
|
+
@subscribers = [].freeze
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
8
11
|
module_function
|
|
9
12
|
|
|
10
|
-
def instrument(name, payload = nil, config: nil, **attributes)
|
|
13
|
+
def instrument(name, payload = nil, config: nil, **attributes, &block)
|
|
11
14
|
payload ||= {}
|
|
12
15
|
payload = payload.merge(attributes) unless attributes.empty?
|
|
13
16
|
workflow_context = current_workflow
|
|
14
17
|
payload = payload.merge(workflow_context) if workflow_context
|
|
15
18
|
instrumenter = (config || RubyLLM.config).instrumenter
|
|
19
|
+
subscribers = @subscribers
|
|
20
|
+
return dispatch(instrumenter, name, payload, &block) if subscribers.empty?
|
|
21
|
+
|
|
22
|
+
dispatch = -> { dispatch(instrumenter, name, payload, &block) }
|
|
23
|
+
subscribers.reverse_each do |subscriber|
|
|
24
|
+
inner = dispatch
|
|
25
|
+
dispatch = -> { subscriber.instrument(name, payload, &inner) }
|
|
26
|
+
end
|
|
27
|
+
dispatch.call
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subscribe(subscriber)
|
|
31
|
+
@mutex.synchronize { @subscribers = (@subscribers | [subscriber]).freeze }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unsubscribe(subscriber)
|
|
35
|
+
@mutex.synchronize { @subscribers = (@subscribers - [subscriber]).freeze }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def capture_context
|
|
39
|
+
@subscribers.filter_map do |subscriber|
|
|
40
|
+
[subscriber, subscriber.capture_context] if subscriber.respond_to?(:capture_context)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def with_context(context, &block)
|
|
45
|
+
context.reverse_each do |subscriber, value|
|
|
46
|
+
inner = block
|
|
47
|
+
block = -> { subscriber.with_context(value, &inner) }
|
|
48
|
+
end
|
|
49
|
+
block.call
|
|
50
|
+
end
|
|
16
51
|
|
|
52
|
+
def dispatch(instrumenter, name, payload)
|
|
17
53
|
if instrumenter.respond_to?(:instrument)
|
|
18
54
|
if block_given?
|
|
19
55
|
instrumenter.instrument(name, payload) { yield(payload) }
|
|
@@ -24,6 +60,7 @@ module RubyLLM
|
|
|
24
60
|
yield(payload)
|
|
25
61
|
end
|
|
26
62
|
end
|
|
63
|
+
private_class_method :dispatch
|
|
27
64
|
|
|
28
65
|
def current_workflow
|
|
29
66
|
Thread.current[WORKFLOW_CONTEXT_KEY]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support # :nodoc:
|
|
5
|
+
class ProcessCache # :nodoc:
|
|
6
|
+
LIMIT = 64
|
|
7
|
+
|
|
8
|
+
def initialize(limit: LIMIT)
|
|
9
|
+
@limit = limit
|
|
10
|
+
@lock = Mutex.new
|
|
11
|
+
@pid = Process.pid
|
|
12
|
+
@entries = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Builds outside the lock, so a slow build never stalls other threads or
|
|
16
|
+
# fibers. When two callers race, the first value stored wins.
|
|
17
|
+
def fetch(key)
|
|
18
|
+
@lock.synchronize { touch(key) } || store(key, yield)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear
|
|
22
|
+
@lock.synchronize { @entries.clear }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(key)
|
|
26
|
+
@lock.synchronize do
|
|
27
|
+
forget_inherited_entries
|
|
28
|
+
@entries.delete(key)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def touch(key)
|
|
35
|
+
forget_inherited_entries
|
|
36
|
+
value = @entries.delete(key)
|
|
37
|
+
@entries[key] = value if value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def store(key, value)
|
|
41
|
+
@lock.synchronize do
|
|
42
|
+
value = touch(key) || value
|
|
43
|
+
@entries[key] = value
|
|
44
|
+
@entries.shift while @entries.size > @limit
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A forked child shares its parent's sockets and credentials. Closing
|
|
50
|
+
# them here would tear down the parent's connections, so the child only
|
|
51
|
+
# forgets them.
|
|
52
|
+
def forget_inherited_entries
|
|
53
|
+
return if @pid == Process.pid
|
|
54
|
+
|
|
55
|
+
@pid = Process.pid
|
|
56
|
+
@entries = {}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support
|
|
5
|
+
# Carries the progress of work that runs inside a chat operation, such
|
|
6
|
+
# as a tool call, to whoever listens for it. The chat installs a
|
|
7
|
+
# listener for the current execution context; #report hands it a
|
|
8
|
+
# Progress. Each concurrent tool call installs its own listener, so its
|
|
9
|
+
# reports reach only its own callback. The listener lives in fiber
|
|
10
|
+
# storage, which fibers and threads the tool starts inherit.
|
|
11
|
+
module ProgressReporter # :nodoc:
|
|
12
|
+
KEY = :ruby_llm_progress_listener
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def listen(listener)
|
|
17
|
+
previous = Fiber[KEY]
|
|
18
|
+
Fiber[KEY] = listener
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
Fiber[KEY] = previous
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def listener
|
|
25
|
+
Fiber[KEY]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def report(progress)
|
|
29
|
+
listener&.call(progress)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/ruby_llm/thinking.rb
CHANGED
|
@@ -69,8 +69,9 @@ module RubyLLM
|
|
|
69
69
|
|
|
70
70
|
budget = model.reasoning_option(:budget_tokens)
|
|
71
71
|
return { budget: 0 } if budget && budget[:min].is_a?(Numeric) && budget[:min] <= 0
|
|
72
|
+
return { enabled: false } if model.reasoning_option(:toggle) || budget
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
model.provider_class&.thinking_off_control(model.id)
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
private
|