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
data/lib/ruby_llm/tokens.rb
CHANGED
|
@@ -15,6 +15,10 @@ module RubyLLM
|
|
|
15
15
|
class Tokens
|
|
16
16
|
include Support::Inspectable
|
|
17
17
|
|
|
18
|
+
READERS = %i[
|
|
19
|
+
input output cache_read cache_write thinking cache_write_by_ttl server_tool_use reported_cost
|
|
20
|
+
].freeze # :nodoc:
|
|
21
|
+
|
|
18
22
|
# The number of standard (non-cached) input tokens, or +nil+ if the
|
|
19
23
|
# provider did not report it.
|
|
20
24
|
attr_reader :input
|
|
@@ -32,14 +36,30 @@ module RubyLLM
|
|
|
32
36
|
# +nil+ if the provider did not report it.
|
|
33
37
|
attr_reader :cache_write
|
|
34
38
|
|
|
39
|
+
# The tokens in #cache_write split by cache lifetime, as a Hash of the
|
|
40
|
+
# TTL Strings that <tt>with_caching(ttl:)</tt> accepts and Integer
|
|
41
|
+
# counts, or +nil+ if the provider did not report a split. Lifetimes
|
|
42
|
+
# with no writes are left out. Providers that price a longer lifetime
|
|
43
|
+
# higher, such as Anthropic's one-hour cache, use it to price #cache_write.
|
|
44
|
+
#
|
|
45
|
+
# response.tokens.cache_write_by_ttl # => {"5m" => 1024, "1h" => 2048}
|
|
46
|
+
#
|
|
47
|
+
attr_reader :cache_write_by_ttl
|
|
48
|
+
|
|
35
49
|
# The number of thinking (reasoning) tokens, or +nil+ if the provider
|
|
36
50
|
# does not report them.
|
|
37
51
|
attr_reader :thinking
|
|
38
52
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
53
|
+
# How many times each provider-executed tool ran, as a Hash of String
|
|
54
|
+
# keys and Integer counts, or +nil+ if the provider reported none.
|
|
55
|
+
# Web searches are <tt>"web_search_requests"</tt> on every provider
|
|
56
|
+
# that reports them. Other tools follow the same form, such as
|
|
57
|
+
# <tt>"web_fetch_requests"</tt> and <tt>"code_execution_requests"</tt>.
|
|
58
|
+
# Tools that did not run are left out. Providers bill these per use,
|
|
59
|
+
# not in tokens.
|
|
60
|
+
#
|
|
61
|
+
# response.tokens.server_tool_use # => {"web_search_requests" => 2}
|
|
62
|
+
#
|
|
43
63
|
attr_reader :server_tool_use
|
|
44
64
|
|
|
45
65
|
# The exact cost of the call in US dollars as reported by the
|
|
@@ -54,16 +74,20 @@ module RubyLLM
|
|
|
54
74
|
# tokens = RubyLLM::Tokens.new(input: 100, output: 20)
|
|
55
75
|
# RubyLLM.models.find("gpt-5.6").cost_for(tokens).total
|
|
56
76
|
#
|
|
57
|
-
# +
|
|
77
|
+
# +cache_write_by_ttl:+ splits the cache writes by lifetime, keyed as
|
|
78
|
+
# #cache_write_by_ttl describes;
|
|
79
|
+
# +server_tool_use:+ counts the provider-executed tools that ran, keyed
|
|
80
|
+
# as #server_tool_use describes;
|
|
58
81
|
# +reported_cost:+ holds the provider's total price in US dollars.
|
|
59
82
|
def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil, thinking: nil,
|
|
60
|
-
server_tool_use: nil, reported_cost: nil)
|
|
83
|
+
cache_write_by_ttl: nil, server_tool_use: nil, reported_cost: nil)
|
|
61
84
|
@input = input
|
|
62
85
|
@output = output
|
|
63
86
|
@cache_read = cache_read
|
|
64
87
|
@cache_write = cache_write
|
|
65
88
|
@thinking = thinking
|
|
66
|
-
@
|
|
89
|
+
@cache_write_by_ttl = positive_counts(cache_write_by_ttl)
|
|
90
|
+
@server_tool_use = positive_counts(server_tool_use)
|
|
67
91
|
@reported_cost = reported_cost
|
|
68
92
|
end
|
|
69
93
|
|
|
@@ -78,23 +102,33 @@ module RubyLLM
|
|
|
78
102
|
reported = tokens.filter_map { |usage| usage.public_send(component) }
|
|
79
103
|
[component, reported.empty? ? nil : reported.sum]
|
|
80
104
|
end
|
|
81
|
-
new(**values,
|
|
105
|
+
new(**values, cache_write_by_ttl: aggregate_counts(tokens, :cache_write_by_ttl),
|
|
106
|
+
server_tool_use: aggregate_counts(tokens, :server_tool_use))
|
|
82
107
|
end
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
# Returns Tokens with each count +other+ reported replacing this one.
|
|
110
|
+
def merge(other) # :nodoc:
|
|
111
|
+
self.class.new(**READERS.to_h do |reader|
|
|
112
|
+
value = other.public_send(reader)
|
|
113
|
+
[reader, value.nil? ? public_send(reader) : value]
|
|
114
|
+
end)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.aggregate_counts(tokens, reader) # :nodoc:
|
|
118
|
+
reported = tokens.filter_map(&reader)
|
|
86
119
|
return nil if reported.empty?
|
|
87
120
|
|
|
88
121
|
reported.each_with_object({}) do |counters, total|
|
|
89
|
-
counters.each { |
|
|
122
|
+
counters.each { |key, count| total[key] = total.fetch(key, 0) + count.to_i }
|
|
90
123
|
end
|
|
91
124
|
end
|
|
92
|
-
private_class_method :
|
|
125
|
+
private_class_method :aggregate_counts
|
|
93
126
|
|
|
94
127
|
# Returns the counts as a hash with keys +:input_tokens+,
|
|
95
128
|
# +:output_tokens+, +:cache_read_tokens+, +:cache_write_tokens+, and
|
|
96
|
-
# +:thinking_tokens+, omitting +nil+ counts. Includes
|
|
97
|
-
#
|
|
129
|
+
# +:thinking_tokens+, omitting +nil+ counts. Includes
|
|
130
|
+
# +:cache_write_tokens_by_ttl+ and +:server_tool_use+ when reported.
|
|
131
|
+
# The provider's reported cost is available separately
|
|
98
132
|
# through #reported_cost.
|
|
99
133
|
#
|
|
100
134
|
# response.tokens.to_h
|
|
@@ -107,6 +141,7 @@ module RubyLLM
|
|
|
107
141
|
cache_read_tokens: cache_read,
|
|
108
142
|
cache_write_tokens: cache_write,
|
|
109
143
|
thinking_tokens: thinking,
|
|
144
|
+
cache_write_tokens_by_ttl: cache_write_by_ttl,
|
|
110
145
|
server_tool_use: server_tool_use
|
|
111
146
|
}.compact
|
|
112
147
|
end
|
|
@@ -118,8 +153,16 @@ module RubyLLM
|
|
|
118
153
|
cache_read: cache_read,
|
|
119
154
|
cache_write: cache_write,
|
|
120
155
|
thinking: thinking,
|
|
156
|
+
cache_write_by_ttl: cache_write_by_ttl,
|
|
121
157
|
server_tool_use: server_tool_use
|
|
122
158
|
}.compact
|
|
123
159
|
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def positive_counts(counts)
|
|
164
|
+
used = counts.to_h.to_h { |key, count| [key.to_s, count.to_i] }.select { |_, count| count.positive? }
|
|
165
|
+
used unless used.empty?
|
|
166
|
+
end
|
|
124
167
|
end
|
|
125
168
|
end
|
data/lib/ruby_llm/tool.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'delegate'
|
|
3
4
|
require 'schematist'
|
|
4
5
|
|
|
5
6
|
module RubyLLM
|
|
@@ -49,6 +50,7 @@ module RubyLLM
|
|
|
49
50
|
@parameters_schema_definition
|
|
50
51
|
@requires_approval
|
|
51
52
|
@approval_resolver
|
|
53
|
+
@deferred
|
|
52
54
|
].freeze
|
|
53
55
|
private_constant :DUPED_INHERITED_CONFIG, :COPIED_INHERITED_CONFIG
|
|
54
56
|
|
|
@@ -74,9 +76,13 @@ module RubyLLM
|
|
|
74
76
|
# WeatherLookup.tool_name # => "weather_lookup"
|
|
75
77
|
#
|
|
76
78
|
def tool_name
|
|
79
|
+
return @tool_name if @tool_name
|
|
80
|
+
|
|
77
81
|
normalized = name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
|
|
78
82
|
ascii_name = normalized.encode('ASCII', replace: '').gsub(/[^a-zA-Z0-9_-]/, '-')
|
|
79
|
-
Support::Utils.underscore(ascii_name).delete_suffix('_tool')
|
|
83
|
+
derived = Support::Utils.underscore(ascii_name).delete_suffix('_tool')
|
|
84
|
+
@tool_name = derived if name
|
|
85
|
+
derived
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
# :call-seq:
|
|
@@ -107,8 +113,8 @@ module RubyLLM
|
|
|
107
113
|
# parameter :units, type: :string, description: "metric or imperial", required: false
|
|
108
114
|
# end
|
|
109
115
|
#
|
|
110
|
-
def parameter(name, **
|
|
111
|
-
declared_parameters[name] = Parameter.new(name, **
|
|
116
|
+
def parameter(name, **)
|
|
117
|
+
declared_parameters[name] = Parameter.new(name, **)
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
def declared_parameters # :nodoc:
|
|
@@ -195,10 +201,29 @@ module RubyLLM
|
|
|
195
201
|
self
|
|
196
202
|
end
|
|
197
203
|
|
|
204
|
+
# Keeps this tool's definition out of the model's context until the
|
|
205
|
+
# provider's tool search loads it. Pass +defer: false+ to
|
|
206
|
+
# Chat#with_tools to offer it up front in one chat. See
|
|
207
|
+
# Chat#deferred_tools.
|
|
208
|
+
#
|
|
209
|
+
# class DeepResearch < RubyLLM::Tool
|
|
210
|
+
# description "Runs a multi-step web investigation"
|
|
211
|
+
# defer
|
|
212
|
+
# end
|
|
213
|
+
#
|
|
214
|
+
def defer
|
|
215
|
+
@deferred = true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def deferred? # :nodoc:
|
|
219
|
+
@deferred || false
|
|
220
|
+
end
|
|
221
|
+
|
|
198
222
|
def split_result(result) # :nodoc:
|
|
199
223
|
case result
|
|
200
224
|
when Attachment then ['', [result]]
|
|
201
225
|
when Array then split_array_result(result)
|
|
226
|
+
when MCP::Result then split_result(result.content)
|
|
202
227
|
else [result_content(result), []]
|
|
203
228
|
end
|
|
204
229
|
end
|
|
@@ -258,6 +283,9 @@ module RubyLLM
|
|
|
258
283
|
self.class.provider_options
|
|
259
284
|
end
|
|
260
285
|
|
|
286
|
+
class Deferred < SimpleDelegator # :nodoc:
|
|
287
|
+
end
|
|
288
|
+
|
|
261
289
|
# Returns the JSON Schema for the tool's arguments, whether declared
|
|
262
290
|
# explicitly or inferred from the +execute+ signature.
|
|
263
291
|
def parameters_schema
|
|
@@ -313,6 +341,25 @@ module RubyLLM
|
|
|
313
341
|
raise NotImplementedError, 'Subclasses must implement #execute'
|
|
314
342
|
end
|
|
315
343
|
|
|
344
|
+
# Reports what the tool is doing while #execute runs, so an application
|
|
345
|
+
# can show it before the result arrives. Chat#after_tool_progress
|
|
346
|
+
# callbacks receive it as a Progress. Pass +value:+ and +total:+ when
|
|
347
|
+
# you can count the work. Outside a chat, the report goes nowhere.
|
|
348
|
+
#
|
|
349
|
+
# def execute(url:)
|
|
350
|
+
# progress "Downloading #{File.basename(url)}"
|
|
351
|
+
# pages = Scanner.pages(url)
|
|
352
|
+
# pages.each_with_index.map do |page, index|
|
|
353
|
+
# progress "Reading page #{index + 1} of #{pages.size}", value: index + 1, total: pages.size
|
|
354
|
+
# page.text
|
|
355
|
+
# end.join("\n")
|
|
356
|
+
# end
|
|
357
|
+
#
|
|
358
|
+
def progress(message = nil, value: nil, total: nil)
|
|
359
|
+
Support::ProgressReporter.report(Progress.new(value:, total:, message:))
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
316
363
|
protected
|
|
317
364
|
|
|
318
365
|
def validate_keyword_arguments(arguments) # :nodoc:
|
data/lib/ruby_llm/tool_call.rb
CHANGED
|
@@ -21,7 +21,10 @@ module RubyLLM
|
|
|
21
21
|
# The name of the tool the model wants to invoke.
|
|
22
22
|
attr_reader :name
|
|
23
23
|
|
|
24
|
-
# The arguments the model supplied for the invocation, as a Hash.
|
|
24
|
+
# The arguments the model supplied for the invocation, as a Hash. Keys
|
|
25
|
+
# are ordered the way PostgreSQL +jsonb+ and MySQL +json+ store them,
|
|
26
|
+
# shorter keys first, so a call reloaded from the database renders the
|
|
27
|
+
# same request bytes and keeps the provider's prompt cache.
|
|
25
28
|
attr_reader :arguments
|
|
26
29
|
|
|
27
30
|
# Returns +true+ if the provider executes this call after approval.
|
|
@@ -35,7 +38,7 @@ module RubyLLM
|
|
|
35
38
|
def initialize(id:, name:, arguments: {}, thought_signature: nil, remote: false) # :nodoc:
|
|
36
39
|
@id = id
|
|
37
40
|
@name = name
|
|
38
|
-
@arguments = arguments
|
|
41
|
+
@arguments = canonical_arguments(arguments)
|
|
39
42
|
@thought_signature = thought_signature
|
|
40
43
|
@remote = remote
|
|
41
44
|
end
|
|
@@ -54,5 +57,17 @@ module RubyLLM
|
|
|
54
57
|
thought_signature: @thought_signature
|
|
55
58
|
}.compact
|
|
56
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def canonical_arguments(value)
|
|
64
|
+
case value
|
|
65
|
+
when Hash
|
|
66
|
+
value.sort_by { |key, _| [key.to_s.bytesize, key.to_s] }
|
|
67
|
+
.to_h { |key, nested| [key, canonical_arguments(nested)] }
|
|
68
|
+
when Array then value.map { |item| canonical_arguments(item) }
|
|
69
|
+
else value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
57
72
|
end
|
|
58
73
|
end
|
|
@@ -61,6 +61,8 @@ module RubyLLM
|
|
|
61
61
|
Cost.new(tokens:, model: model_info, category: :audio_tokens)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
attr_writer :model_info # :nodoc:
|
|
65
|
+
|
|
64
66
|
def model_info # :nodoc:
|
|
65
67
|
@model_info ||= RubyLLM.models.find(model)
|
|
66
68
|
rescue ModelNotFoundError
|
|
@@ -96,6 +98,8 @@ module RubyLLM
|
|
|
96
98
|
# +:segment+ or +:character+; unsupported granularities raise ArgumentError.
|
|
97
99
|
# +provider_options:+ takes options in the provider's request vocabulary
|
|
98
100
|
# and merges them into the rendered request as-is.
|
|
101
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
102
|
+
# over RubyLLM.with_usage_owner.
|
|
99
103
|
#
|
|
100
104
|
# Given a block, the transcript streams: each TranscriptionChunk is
|
|
101
105
|
# yielded as it arrives and the completed Transcription is still
|
|
@@ -124,6 +128,7 @@ module RubyLLM
|
|
|
124
128
|
speaker_references: nil,
|
|
125
129
|
provider_options: {},
|
|
126
130
|
metadata: nil,
|
|
131
|
+
owner: nil,
|
|
127
132
|
&block)
|
|
128
133
|
config = context&.config || RubyLLM.config
|
|
129
134
|
model ||= config.default_transcription_model
|
|
@@ -143,9 +148,11 @@ module RubyLLM
|
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
RubyLLM.instrument('transcription.ruby_llm', payload, config: config) do |event|
|
|
146
|
-
result =
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
152
|
+
provider_instance.transcribe(audio_file, model:, language:, format:, timestamps:, speaker_names:,
|
|
153
|
+
speaker_references:, provider_options:, prompt:,
|
|
154
|
+
temperature:, &block)
|
|
155
|
+
end
|
|
149
156
|
event[:result] = result
|
|
150
157
|
event[:response_model] = result.model
|
|
151
158
|
event[:tokens] = result.tokens
|
|
@@ -4,6 +4,7 @@ require 'faraday'
|
|
|
4
4
|
require 'faraday/multipart'
|
|
5
5
|
require 'faraday/retry'
|
|
6
6
|
require 'ruby_llm/transport/error_middleware'
|
|
7
|
+
require 'ruby_llm/transport/signing_middleware'
|
|
7
8
|
require 'ruby_llm/transport/usage_middleware'
|
|
8
9
|
require 'timeout'
|
|
9
10
|
|
|
@@ -15,7 +16,17 @@ module RubyLLM
|
|
|
15
16
|
IDEMPOTENT_KEY = :ruby_llm_idempotent
|
|
16
17
|
STREAM_PROGRESS_KEY = :ruby_llm_stream_progress
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
# The key a shared Faraday connection is cached under: the provider,
|
|
20
|
+
# whether it streams, and everything ::build reads.
|
|
21
|
+
Settings = Struct.new(
|
|
22
|
+
:provider, :stream, :api_base, :adapter, :timeout, :proxy, :logger, :log_bodies, :log_regexp_timeout,
|
|
23
|
+
:max_retries, :retry_interval, :retry_max_interval, :retry_interval_randomness, :retry_backoff_factor,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
CACHE = Support::ProcessCache.new
|
|
28
|
+
|
|
29
|
+
attr_reader :provider, :config
|
|
19
30
|
|
|
20
31
|
def self.basic(config = RubyLLM.config, &)
|
|
21
32
|
Faraday.new do |f|
|
|
@@ -32,34 +43,61 @@ module RubyLLM
|
|
|
32
43
|
end
|
|
33
44
|
end
|
|
34
45
|
|
|
35
|
-
def
|
|
46
|
+
def self.cache
|
|
47
|
+
CACHE
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The result is shared across threads and contexts, so the middleware
|
|
51
|
+
# stack is built now rather than lazily, without a lock, on the first
|
|
52
|
+
# request, and the defaults every request copies are frozen.
|
|
53
|
+
def self.build(settings)
|
|
54
|
+
connection = Faraday.new(settings.api_base) do |faraday|
|
|
55
|
+
setup_timeout(faraday, settings)
|
|
56
|
+
setup_logging(faraday, settings)
|
|
57
|
+
setup_retry(faraday, settings)
|
|
58
|
+
setup_middleware(faraday, settings)
|
|
59
|
+
setup_http_proxy(faraday, settings)
|
|
60
|
+
end
|
|
61
|
+
[connection.headers, connection.params, connection.options].each(&:freeze)
|
|
62
|
+
connection.app
|
|
63
|
+
connection
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(provider, config, api_base: nil, headers: {}, signer: nil)
|
|
36
67
|
@provider = provider
|
|
37
68
|
@config = config
|
|
69
|
+
@headers = headers
|
|
70
|
+
@signer = signer
|
|
71
|
+
@settings = settings_for(api_base || provider.api_base)
|
|
72
|
+
@stream_settings = @settings.dup.tap { |settings| settings.stream = true }.freeze
|
|
73
|
+
connection
|
|
74
|
+
end
|
|
38
75
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
# The Faraday connection shared by every request with these settings in
|
|
77
|
+
# this process. Looked up per request, so an object that outlives a fork
|
|
78
|
+
# never reaches the parent's sockets. Streaming requests get their own:
|
|
79
|
+
# some adapters, such as httpx, only stream if their first request did.
|
|
80
|
+
def connection(stream: false)
|
|
81
|
+
settings = stream ? @stream_settings : @settings
|
|
82
|
+
CACHE.fetch(settings) { self.class.build(settings) }
|
|
46
83
|
end
|
|
47
84
|
|
|
48
|
-
def post(url, payload, usage: nil, idempotent: true, &)
|
|
85
|
+
def post(url, payload, usage: nil, idempotent: true, stream: false, &)
|
|
49
86
|
instrument_request(:post, url) do
|
|
50
|
-
|
|
51
|
-
req
|
|
87
|
+
response = connection(stream:).post url, payload do |req|
|
|
88
|
+
prepare(req)
|
|
52
89
|
set_usage_tracker(req, usage) if usage
|
|
53
90
|
mark_non_idempotent(req) unless idempotent
|
|
54
91
|
yield req if block_given?
|
|
55
92
|
end
|
|
93
|
+
release_request(response)
|
|
56
94
|
end
|
|
57
95
|
end
|
|
58
96
|
|
|
59
97
|
def get(url, &)
|
|
60
98
|
instrument_request(:get, url) do
|
|
61
|
-
|
|
62
|
-
req
|
|
99
|
+
connection.get url do |req|
|
|
100
|
+
prepare(req)
|
|
63
101
|
yield req if block_given?
|
|
64
102
|
end
|
|
65
103
|
end
|
|
@@ -67,17 +105,18 @@ module RubyLLM
|
|
|
67
105
|
|
|
68
106
|
def patch(url, payload, &)
|
|
69
107
|
instrument_request(:patch, url) do
|
|
70
|
-
|
|
71
|
-
req
|
|
108
|
+
response = connection.patch url, payload do |req|
|
|
109
|
+
prepare(req)
|
|
72
110
|
yield req if block_given?
|
|
73
111
|
end
|
|
112
|
+
release_request(response)
|
|
74
113
|
end
|
|
75
114
|
end
|
|
76
115
|
|
|
77
116
|
def delete(url, &)
|
|
78
117
|
instrument_request(:delete, url) do
|
|
79
|
-
|
|
80
|
-
req
|
|
118
|
+
connection.delete url do |req|
|
|
119
|
+
prepare(req)
|
|
81
120
|
yield req if block_given?
|
|
82
121
|
end
|
|
83
122
|
end
|
|
@@ -99,35 +138,74 @@ module RubyLLM
|
|
|
99
138
|
end
|
|
100
139
|
end
|
|
101
140
|
|
|
102
|
-
|
|
103
|
-
|
|
141
|
+
# The response outlives the call as Message#raw and other results. A chat
|
|
142
|
+
# request body is the whole serialized conversation, and a streaming
|
|
143
|
+
# callback closes over the payload it was built from.
|
|
144
|
+
def release_request(response)
|
|
145
|
+
response.env.request_body = nil
|
|
146
|
+
response.env.request.on_data = nil
|
|
147
|
+
response.env.request.context&.delete(ErrorMiddleware::STREAM_RESET_KEY)
|
|
148
|
+
response
|
|
104
149
|
end
|
|
105
150
|
|
|
106
|
-
def
|
|
151
|
+
def settings_for(api_base)
|
|
152
|
+
Settings.new(
|
|
153
|
+
provider: @provider.class,
|
|
154
|
+
stream: false,
|
|
155
|
+
api_base: api_base,
|
|
156
|
+
adapter: @config.faraday_adapter,
|
|
157
|
+
timeout: @config.request_timeout,
|
|
158
|
+
proxy: @config.http_proxy,
|
|
159
|
+
logger: RubyLLM.logger,
|
|
160
|
+
log_bodies: RubyLLM.logger.debug?,
|
|
161
|
+
log_regexp_timeout: @config.log_regexp_timeout,
|
|
162
|
+
max_retries: @config.max_retries,
|
|
163
|
+
retry_interval: @config.retry_interval,
|
|
164
|
+
retry_max_interval: @config.retry_max_interval,
|
|
165
|
+
retry_interval_randomness: @config.retry_interval_randomness,
|
|
166
|
+
retry_backoff_factor: @config.retry_backoff_factor
|
|
167
|
+
).freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Credentials, the signer, and the provider that parses errors travel
|
|
171
|
+
# with each request, because the Faraday connection is shared across
|
|
172
|
+
# contexts.
|
|
173
|
+
def prepare(request)
|
|
174
|
+
request.headers.merge!(@headers).merge!(@provider.headers)
|
|
175
|
+
context = request.options.context ||= {}
|
|
176
|
+
context[ErrorMiddleware::PROVIDER_KEY] = @provider
|
|
177
|
+
context[SigningMiddleware::CONTEXT_KEY] = @signer if @signer
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.setup_timeout(faraday, settings)
|
|
181
|
+
faraday.options.timeout = settings.timeout
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def self.setup_logging(faraday, settings)
|
|
107
185
|
faraday.response :logger,
|
|
108
|
-
|
|
109
|
-
bodies:
|
|
186
|
+
settings.logger,
|
|
187
|
+
bodies: settings.log_bodies,
|
|
110
188
|
errors: true,
|
|
111
189
|
headers: false,
|
|
112
190
|
log_level: :debug do |logger|
|
|
113
|
-
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
|
|
114
|
-
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
|
|
191
|
+
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}', settings), '[BASE64 DATA]')
|
|
192
|
+
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}', settings), '[EMBEDDINGS ARRAY]')
|
|
115
193
|
end
|
|
116
194
|
end
|
|
117
195
|
|
|
118
|
-
def logging_regexp(pattern)
|
|
119
|
-
return Regexp.new(pattern) if
|
|
196
|
+
def self.logging_regexp(pattern, settings)
|
|
197
|
+
return Regexp.new(pattern) if settings.log_regexp_timeout.nil?
|
|
120
198
|
|
|
121
|
-
Regexp.new(pattern, timeout:
|
|
199
|
+
Regexp.new(pattern, timeout: settings.log_regexp_timeout)
|
|
122
200
|
end
|
|
123
201
|
|
|
124
|
-
def setup_retry(faraday)
|
|
202
|
+
def self.setup_retry(faraday, settings)
|
|
125
203
|
faraday.request :retry, {
|
|
126
|
-
max:
|
|
127
|
-
interval:
|
|
128
|
-
max_interval:
|
|
129
|
-
interval_randomness:
|
|
130
|
-
backoff_factor:
|
|
204
|
+
max: settings.max_retries,
|
|
205
|
+
interval: settings.retry_interval,
|
|
206
|
+
max_interval: settings.retry_max_interval,
|
|
207
|
+
interval_randomness: settings.retry_interval_randomness,
|
|
208
|
+
backoff_factor: settings.retry_backoff_factor,
|
|
131
209
|
methods: Faraday::Retry::Middleware::IDEMPOTENT_METHODS,
|
|
132
210
|
retry_if: lambda { |env, _exception|
|
|
133
211
|
env[:method] == :post && idempotent?(env) && !stream_delivered?(env)
|
|
@@ -137,34 +215,36 @@ module RubyLLM
|
|
|
137
215
|
faraday.use :llm_usage
|
|
138
216
|
end
|
|
139
217
|
|
|
140
|
-
def stream_delivered?(env)
|
|
218
|
+
def self.stream_delivered?(env)
|
|
141
219
|
env[:request]&.context&.dig(STREAM_PROGRESS_KEY, :started)
|
|
142
220
|
end
|
|
143
221
|
|
|
144
|
-
def idempotent?(env)
|
|
222
|
+
def self.idempotent?(env)
|
|
145
223
|
env[:request]&.context&.dig(IDEMPOTENT_KEY) != false
|
|
146
224
|
end
|
|
147
225
|
|
|
148
|
-
def setup_middleware(faraday)
|
|
226
|
+
def self.setup_middleware(faraday, settings)
|
|
149
227
|
faraday.request :multipart
|
|
150
228
|
faraday.request :json
|
|
151
|
-
faraday.
|
|
152
|
-
faraday.
|
|
153
|
-
faraday.
|
|
229
|
+
faraday.use :llm_signing
|
|
230
|
+
faraday.use JsonResponse
|
|
231
|
+
faraday.adapter(settings.adapter)
|
|
232
|
+
faraday.use :llm_errors
|
|
154
233
|
end
|
|
155
234
|
|
|
156
|
-
def setup_http_proxy(faraday)
|
|
157
|
-
return unless
|
|
235
|
+
def self.setup_http_proxy(faraday, settings)
|
|
236
|
+
return unless settings.proxy
|
|
158
237
|
|
|
159
|
-
faraday.proxy =
|
|
238
|
+
faraday.proxy = settings.proxy
|
|
160
239
|
end
|
|
161
240
|
|
|
162
|
-
def retry_exceptions
|
|
241
|
+
def self.retry_exceptions
|
|
163
242
|
[
|
|
164
243
|
Errno::ETIMEDOUT,
|
|
165
244
|
Timeout::Error,
|
|
166
245
|
Faraday::TimeoutError,
|
|
167
246
|
Faraday::ConnectionFailed,
|
|
247
|
+
Faraday::SSLError,
|
|
168
248
|
Faraday::RetriableResponse,
|
|
169
249
|
RubyLLM::RateLimitError,
|
|
170
250
|
RubyLLM::ServerError,
|
|
@@ -173,6 +253,9 @@ module RubyLLM
|
|
|
173
253
|
]
|
|
174
254
|
end
|
|
175
255
|
|
|
256
|
+
private_class_method :setup_timeout, :setup_logging, :logging_regexp, :setup_retry, :stream_delivered?,
|
|
257
|
+
:idempotent?, :setup_middleware, :setup_http_proxy, :retry_exceptions
|
|
258
|
+
|
|
176
259
|
def set_usage_tracker(request, tracker)
|
|
177
260
|
context = request.options.context ||= {}
|
|
178
261
|
context[UsageMiddleware::CONTEXT_KEY] = tracker
|