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/protocol.rb
CHANGED
|
@@ -37,6 +37,7 @@ module RubyLLM
|
|
|
37
37
|
class Protocol
|
|
38
38
|
include Streaming
|
|
39
39
|
include BinaryStreaming
|
|
40
|
+
include RequestShapes
|
|
40
41
|
|
|
41
42
|
# The Provider this protocol talks through.
|
|
42
43
|
attr_reader :provider
|
|
@@ -77,6 +78,7 @@ module RubyLLM
|
|
|
77
78
|
abstract :render_transcription_payload, :transcription_url, :parse_transcription_response
|
|
78
79
|
abstract :render_ocr_payload, :ocr_url, :parse_ocr_response
|
|
79
80
|
abstract :render_rerank_payload, :rerank_url, :parse_rerank_response
|
|
81
|
+
abstract :render_judgment_payload, :judgment_url, :parse_judgment_response
|
|
80
82
|
abstract :render_count_tokens_payload, :count_tokens_url, :parse_count_tokens_response
|
|
81
83
|
abstract :render_tokenization_payload, :tokenization_url, :parse_tokenization_response
|
|
82
84
|
abstract :render_compaction_payload, :compaction_url, :parse_compaction_response
|
|
@@ -92,6 +94,8 @@ module RubyLLM
|
|
|
92
94
|
def tool_approval_response(tool_call, approved:)
|
|
93
95
|
Message.new(role: :tool, content: approved ? 'Approved' : 'Denied', tool_call_id: tool_call.id,
|
|
94
96
|
raw_content: render_tool_approval_response(tool_call, approved:))
|
|
97
|
+
rescue NotImplementedError
|
|
98
|
+
raise Error, "#{@provider.name} doesn't support remote tool approvals"
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def complete(messages, tools:, temperature:, provider_options: {}, headers: {}, schema: nil, thinking: nil,
|
|
@@ -106,11 +110,15 @@ module RubyLLM
|
|
|
106
110
|
stream: block_given?
|
|
107
111
|
)
|
|
108
112
|
|
|
113
|
+
# The caller's block can raise the error of another operation it ran.
|
|
114
|
+
yielding = false
|
|
109
115
|
track_usage(:chat, on_finish: usage_recorder) do
|
|
110
116
|
if block_given?
|
|
111
117
|
stream_response(payload, headers) do |chunk|
|
|
112
118
|
@usage_tracker.observe(chunk)
|
|
119
|
+
yielding = true
|
|
113
120
|
yield chunk
|
|
121
|
+
yielding = false
|
|
114
122
|
end
|
|
115
123
|
else
|
|
116
124
|
sync_response payload, headers
|
|
@@ -118,6 +126,9 @@ module RubyLLM
|
|
|
118
126
|
end
|
|
119
127
|
rescue NotImplementedError
|
|
120
128
|
raise Error, "#{@provider.name} doesn't support chat"
|
|
129
|
+
rescue Error => e
|
|
130
|
+
claim_error(e, payload) unless yielding
|
|
131
|
+
raise
|
|
121
132
|
end
|
|
122
133
|
|
|
123
134
|
def render(messages, tools:, temperature:, provider_options: {}, schema: nil, thinking: nil,
|
|
@@ -140,6 +151,7 @@ module RubyLLM
|
|
|
140
151
|
payload = apply_compaction(payload, compaction) if compaction
|
|
141
152
|
payload = Support::Utils.deep_merge(payload, provider_options)
|
|
142
153
|
payload = apply_provider_tools(payload, provider_tools)
|
|
154
|
+
payload = apply_tool_search(payload)
|
|
143
155
|
apply_before_request_hooks(payload, before_request)
|
|
144
156
|
rescue NotImplementedError
|
|
145
157
|
raise Error, "#{@provider.name} doesn't support chat"
|
|
@@ -208,6 +220,9 @@ module RubyLLM
|
|
|
208
220
|
parse_count_tokens_response post_count_tokens(payload)
|
|
209
221
|
rescue NotImplementedError
|
|
210
222
|
raise Error, "#{@provider.name} doesn't support token counting"
|
|
223
|
+
rescue Error => e
|
|
224
|
+
claim_error(e, payload)
|
|
225
|
+
raise
|
|
211
226
|
end
|
|
212
227
|
|
|
213
228
|
def list_models
|
|
@@ -233,6 +248,9 @@ module RubyLLM
|
|
|
233
248
|
end
|
|
234
249
|
rescue NotImplementedError
|
|
235
250
|
raise Error, "#{@provider.name} doesn't support manual compaction"
|
|
251
|
+
rescue Error => e
|
|
252
|
+
claim_error(e, payload)
|
|
253
|
+
raise
|
|
236
254
|
end
|
|
237
255
|
|
|
238
256
|
def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {})
|
|
@@ -310,11 +328,30 @@ module RubyLLM
|
|
|
310
328
|
url = video_request_url(payload)
|
|
311
329
|
end
|
|
312
330
|
response = post_video(url, payload)
|
|
313
|
-
parse_video_job(response, model
|
|
331
|
+
parse_video_job(response, model:, **parse_video_request(payload))
|
|
314
332
|
rescue NotImplementedError
|
|
315
333
|
raise Error, "#{@provider.name} doesn't support video generation"
|
|
316
334
|
end
|
|
317
335
|
|
|
336
|
+
# Returns the duration in seconds and the resolution a rendered video
|
|
337
|
+
# request asked for, so the VideoJob keeps them. Protocols whose API
|
|
338
|
+
# names them differently override video_request_settings.
|
|
339
|
+
def parse_video_request(payload)
|
|
340
|
+
settings = video_request_settings(Support::Utils.deep_symbolize_keys(payload))
|
|
341
|
+
{ duration: parse_video_seconds(settings[:duration]), resolution: settings[:resolution] }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def video_request_settings(request)
|
|
345
|
+
request.slice(:duration, :resolution)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def parse_video_seconds(value)
|
|
349
|
+
return value unless value.is_a?(String)
|
|
350
|
+
|
|
351
|
+
seconds = value.delete_suffix('s')
|
|
352
|
+
Integer(seconds, exception: false) || Float(seconds, exception: false)
|
|
353
|
+
end
|
|
354
|
+
|
|
318
355
|
def post_video(url, payload)
|
|
319
356
|
@connection.post url, payload, idempotent: false
|
|
320
357
|
end
|
|
@@ -445,6 +482,16 @@ module RubyLLM
|
|
|
445
482
|
raise Error, "#{@provider.name} doesn't support explicit content caching"
|
|
446
483
|
end
|
|
447
484
|
|
|
485
|
+
def judge(input, questions:, model:, with: [], provider_options: {}) # :nodoc:
|
|
486
|
+
track_usage(:judgment) do
|
|
487
|
+
payload = render_judgment_payload(input, questions:, model:, with:, provider_options:)
|
|
488
|
+
response = @connection.post judgment_url, payload, usage: @usage_tracker
|
|
489
|
+
parse_judgment_response(response, questions:)
|
|
490
|
+
end
|
|
491
|
+
rescue NotImplementedError
|
|
492
|
+
raise Error, "#{@provider.name} doesn't support judgments"
|
|
493
|
+
end
|
|
494
|
+
|
|
448
495
|
def find_cache(name)
|
|
449
496
|
response = @connection.get cache_url(name)
|
|
450
497
|
parse_cache_response(response.body)
|
|
@@ -470,32 +517,108 @@ module RubyLLM
|
|
|
470
517
|
@provider.parse_error(response)
|
|
471
518
|
end
|
|
472
519
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
520
|
+
# Returns a RequestShape describing +payload+, a conversation request
|
|
521
|
+
# this protocol rendered, as JSON parsed back with String keys.
|
|
522
|
+
# Error#request_shape asks for it when the request fails. The default
|
|
523
|
+
# returns +nil+; protocols that render conversations override it,
|
|
524
|
+
# naming each turn's role and measuring its parts:
|
|
525
|
+
#
|
|
526
|
+
# def parse_request_shape(payload)
|
|
527
|
+
# turns = payload['messages'].each_with_index.map do |message, index|
|
|
528
|
+
# text = RubyLLM::RequestShape::Part.new(kind: :text, size: message['content'].length)
|
|
529
|
+
# RubyLLM::RequestShape::Turn.new(index:, role: message['role'], parts: [text])
|
|
530
|
+
# end
|
|
531
|
+
# RubyLLM::RequestShape.new(turns:)
|
|
532
|
+
# end
|
|
533
|
+
#
|
|
534
|
+
def parse_request_shape(_payload)
|
|
535
|
+
nil
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# A description must not raise inside the rescue that asks for it.
|
|
539
|
+
def request_shape(payload) # :nodoc:
|
|
540
|
+
payload = Support::Utils.deep_stringify_keys(payload)
|
|
541
|
+
parse_request_shape(payload) if payload.is_a?(Hash)
|
|
542
|
+
rescue StandardError => e
|
|
543
|
+
RubyLLM.logger.debug { "#{self.class} could not describe its request: #{e.class}" }
|
|
544
|
+
nil
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def preprocess_message(message, unsupported_attachment: nil)
|
|
548
|
+
message = without_foreign_native_content(message) if foreign_native_content?(message)
|
|
477
549
|
return message if message.attachments.empty?
|
|
478
550
|
|
|
479
|
-
|
|
480
|
-
|
|
551
|
+
upload = message.role == :user && auto_upload_large_files?
|
|
552
|
+
prepared = message.attachments.map do |attachment|
|
|
553
|
+
replacement = replace_unsupported_attachment(attachment, unsupported_attachment, message)
|
|
554
|
+
upload ? preprocess_attachment(replacement) : replacement
|
|
555
|
+
end
|
|
556
|
+
return message if prepared == message.attachments
|
|
557
|
+
|
|
558
|
+
message.with_attachments(prepared)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def supported_attachment?(_attachment) # :nodoc:
|
|
562
|
+
true
|
|
563
|
+
end
|
|
481
564
|
|
|
482
|
-
|
|
565
|
+
# A provider can delete a file RubyLLM uploaded for an attachment. When
|
|
566
|
+
# a request fails naming such a file, or with a 404 that names none, the
|
|
567
|
+
# attachments forget those uploads so the next request uploads them
|
|
568
|
+
# again. Returns the uploads it forgot.
|
|
569
|
+
def discard_missing_uploads(messages, error)
|
|
570
|
+
scope = provider_upload_scope
|
|
571
|
+
missing_uploads(messages, error, scope).map do |attachment, upload|
|
|
572
|
+
attachment.provider_uploads.delete(scope)
|
|
573
|
+
StoredUploads.new(@provider, attachment.provider_file_store).forget(upload)
|
|
574
|
+
upload
|
|
575
|
+
end
|
|
483
576
|
end
|
|
484
577
|
|
|
485
578
|
private
|
|
486
579
|
|
|
487
|
-
#
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
580
|
+
# An error raised before the request went out answers no request.
|
|
581
|
+
def claim_error(error, payload)
|
|
582
|
+
return if error.request_protocol || error.response.nil? || payload.nil?
|
|
583
|
+
|
|
584
|
+
error.request_protocol = self
|
|
585
|
+
error.request_payload = payload
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# A thinking signature or a provider-shaped content block belongs to the
|
|
589
|
+
# model that produced it, and one provider can serve models that do not
|
|
590
|
+
# read each other's, so a message another model produced replays
|
|
591
|
+
# without them. A message with no known producer replays as it is.
|
|
592
|
+
def foreign_native_content?(message)
|
|
593
|
+
return false unless message.role == :assistant && carries_native_content?(message)
|
|
594
|
+
|
|
595
|
+
producer = producing_entry(message)
|
|
596
|
+
!producer.nil? && (producer.provider != @provider.slug || !current_model?(producer.model))
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Usage records the id a chat resolved its model to, and a message built
|
|
600
|
+
# by hand may name the model by an alias of it.
|
|
601
|
+
def current_model?(model_id)
|
|
602
|
+
return true if @model.nil? || model_id == @model.id
|
|
603
|
+
|
|
604
|
+
RubyLLM.models.find(model_id, provider: @provider.slug, config: @config).id == @model.id
|
|
605
|
+
rescue ModelNotFoundError
|
|
606
|
+
false
|
|
607
|
+
end
|
|
492
608
|
|
|
493
|
-
|
|
494
|
-
|
|
609
|
+
# Another model of this provider gets the raw content any of its models
|
|
610
|
+
# reads, such as a tool search, and none of the producing model's own.
|
|
611
|
+
def without_foreign_native_content(message)
|
|
612
|
+
portable = portable_raw_content(message) if producer_slug(message) == @provider.slug
|
|
613
|
+
message.without_native_content(raw_content: portable)
|
|
495
614
|
end
|
|
496
615
|
|
|
497
|
-
def
|
|
498
|
-
|
|
616
|
+
def portable_raw_content(_message)
|
|
617
|
+
nil
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def carries_native_content?(message)
|
|
621
|
+
return true if message.thinking || message.raw_reasoning || message.raw_content
|
|
499
622
|
|
|
500
623
|
message.tool_call? && message.tool_calls.each_value.any?(&:thought_signature)
|
|
501
624
|
end
|
|
@@ -503,7 +626,19 @@ module RubyLLM
|
|
|
503
626
|
# Only a usage entry names the producer: a model id alone can belong
|
|
504
627
|
# to several providers.
|
|
505
628
|
def producer_slug(message)
|
|
506
|
-
message
|
|
629
|
+
producing_entry(message)&.provider
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def producing_entry(message)
|
|
633
|
+
message.ruby_llm_usage_entries.reverse.find(&:succeeded?)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# A thinking signature can be another provider's opaque blob, and a
|
|
637
|
+
# message restored without its usage could come from anyone, so a
|
|
638
|
+
# protocol that verifies signatures gets back only its own.
|
|
639
|
+
def own_signature(message)
|
|
640
|
+
signature = message.thinking&.signature
|
|
641
|
+
signature if signature && producer_slug(message) == @provider.slug
|
|
507
642
|
end
|
|
508
643
|
|
|
509
644
|
def resolve_provider_tools_for_request(entries)
|
|
@@ -534,11 +669,16 @@ module RubyLLM
|
|
|
534
669
|
payload[:tools] = Array(payload[:tools]) + entries
|
|
535
670
|
end
|
|
536
671
|
|
|
537
|
-
def
|
|
672
|
+
def apply_tool_search(payload)
|
|
673
|
+
payload
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def track_usage(operation, on_finish: nil, model_id: nil)
|
|
538
677
|
@usage_tracker = Accounting::Usage::Tracker.new(
|
|
539
678
|
operation:,
|
|
540
679
|
provider: @provider,
|
|
541
680
|
model: @model,
|
|
681
|
+
model_id:,
|
|
542
682
|
config: @config,
|
|
543
683
|
on_finish:
|
|
544
684
|
)
|
|
@@ -569,8 +709,24 @@ module RubyLLM
|
|
|
569
709
|
return attachment if attachment.provider_file?
|
|
570
710
|
return attachment unless upload_large_attachment?(attachment)
|
|
571
711
|
|
|
572
|
-
|
|
573
|
-
|
|
712
|
+
Attachment.new(provider_upload(attachment), resolution: attachment.resolution, config: @config)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def supported_message_attachment?(_message, attachment)
|
|
716
|
+
supported_attachment?(attachment)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def replace_unsupported_attachment(attachment, handler, message)
|
|
720
|
+
return attachment if handler.nil? || supported_message_attachment?(message, attachment)
|
|
721
|
+
|
|
722
|
+
replacement = handler.call(attachment)
|
|
723
|
+
return attachment if replacement.nil?
|
|
724
|
+
unless replacement.is_a?(Attachment)
|
|
725
|
+
raise ArgumentError, 'convert_unsupported_attachments must return a RubyLLM::Attachment or nil'
|
|
726
|
+
end
|
|
727
|
+
raise UnsupportedAttachmentError, replacement.mime_type unless supported_message_attachment?(message, replacement)
|
|
728
|
+
|
|
729
|
+
replacement
|
|
574
730
|
end
|
|
575
731
|
|
|
576
732
|
# Uploads are memoized per provider on the attachment itself, so a chat
|
|
@@ -582,8 +738,9 @@ module RubyLLM
|
|
|
582
738
|
upload = attachment.provider_uploads[scope]
|
|
583
739
|
return upload if upload && !upload.expired?
|
|
584
740
|
|
|
585
|
-
attachment.provider_uploads[scope] =
|
|
741
|
+
attachment.provider_uploads[scope] = StoredUploads.new(@provider, attachment.provider_file_store).fetch do
|
|
586
742
|
@provider.upload_file(attachment, **provider_file_upload_options(attachment))
|
|
743
|
+
end
|
|
587
744
|
end
|
|
588
745
|
|
|
589
746
|
# A file id belongs to the account that uploaded it, so the memo is keyed
|
|
@@ -595,6 +752,19 @@ module RubyLLM
|
|
|
595
752
|
"#{@provider.slug}:#{Digest::SHA256.hexdigest(credentials.join("\0"))}"
|
|
596
753
|
end
|
|
597
754
|
|
|
755
|
+
def missing_uploads(messages, error, scope)
|
|
756
|
+
uploads = messages.flat_map(&:attachments).filter_map do |attachment|
|
|
757
|
+
upload = attachment.provider_uploads[scope]
|
|
758
|
+
[attachment, upload] if upload
|
|
759
|
+
end
|
|
760
|
+
named = uploads.select { |_, upload| names_file?(error, upload.id) }
|
|
761
|
+
named.empty? && error.response&.status == 404 ? uploads : named
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def names_file?(error, id)
|
|
765
|
+
[error.message, error.response&.body].any? { |text| text.to_s.include?(id) }
|
|
766
|
+
end
|
|
767
|
+
|
|
598
768
|
def upload_large_attachment?(attachment)
|
|
599
769
|
size = attachment.byte_size
|
|
600
770
|
size && size > default_large_file_upload_threshold && provider_file_attachable?(attachment)
|
|
@@ -604,10 +774,6 @@ module RubyLLM
|
|
|
604
774
|
Float::INFINITY
|
|
605
775
|
end
|
|
606
776
|
|
|
607
|
-
def provider_file_upload_limit
|
|
608
|
-
nil
|
|
609
|
-
end
|
|
610
|
-
|
|
611
777
|
def provider_file_attachable?(_attachment)
|
|
612
778
|
false
|
|
613
779
|
end
|
|
@@ -616,20 +782,6 @@ module RubyLLM
|
|
|
616
782
|
{}
|
|
617
783
|
end
|
|
618
784
|
|
|
619
|
-
def ensure_provider_file_size!(attachment)
|
|
620
|
-
limit = provider_file_upload_limit
|
|
621
|
-
return unless limit && attachment.byte_size.to_i > limit
|
|
622
|
-
|
|
623
|
-
raise Error, "#{@provider.name} file uploads support files up to #{format_bytes(limit)}; " \
|
|
624
|
-
"#{attachment.filename} is #{format_bytes(attachment.byte_size)}"
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
def format_bytes(bytes)
|
|
628
|
-
return 'unknown size' unless bytes
|
|
629
|
-
|
|
630
|
-
"#{(bytes.to_f / (1024 * 1024)).round(1)} MB"
|
|
631
|
-
end
|
|
632
|
-
|
|
633
785
|
def validate_paint_inputs!(with:, mask:)
|
|
634
786
|
return if with.nil? && mask.nil?
|
|
635
787
|
|
|
@@ -11,7 +11,6 @@ module RubyLLM
|
|
|
11
11
|
}.freeze
|
|
12
12
|
|
|
13
13
|
ANTHROPIC_INLINE_REQUEST_LIMIT = 24 * 1024 * 1024
|
|
14
|
-
ANTHROPIC_FILE_UPLOAD_LIMIT = 500 * 1024 * 1024
|
|
15
14
|
CACHE_CONTROL_TYPE = 'ephemeral'
|
|
16
15
|
PROMPT_CACHE_OPTIONS = %i[ttl].freeze
|
|
17
16
|
BETA_HEADER = 'anthropic-beta'
|
|
@@ -19,6 +18,7 @@ module RubyLLM
|
|
|
19
18
|
COMPACTION_EDIT_TYPE = 'compact_20260112'
|
|
20
19
|
COUNT_TOKENS_KEYS = %i[model messages system tools tool_choice thinking].freeze
|
|
21
20
|
THINKING_BLOCK_TYPES = %w[thinking redacted_thinking].freeze
|
|
21
|
+
PORTABLE_BLOCK_TYPES = %w[text tool_use].freeze
|
|
22
22
|
|
|
23
23
|
module_function
|
|
24
24
|
|
|
@@ -64,7 +64,7 @@ module RubyLLM
|
|
|
64
64
|
thinking: thinking,
|
|
65
65
|
citations: citations,
|
|
66
66
|
caching: caching
|
|
67
|
-
).slice(*COUNT_TOKENS_KEYS)
|
|
67
|
+
).then { |payload| Tools.apply_tool_search(payload) }.slice(*COUNT_TOKENS_KEYS)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def parse_count_tokens_response(response)
|
|
@@ -90,7 +90,7 @@ module RubyLLM
|
|
|
90
90
|
# own block in the resulting array.
|
|
91
91
|
system_messages.flat_map do |msg|
|
|
92
92
|
blocks = Media.format_content(msg.content, msg.attachments).dup
|
|
93
|
-
cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:) : blocks
|
|
93
|
+
cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) : blocks
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ module RubyLLM
|
|
|
100
100
|
model: model.id,
|
|
101
101
|
messages: format_messages(chat_messages, thinking:, citations:, caching:),
|
|
102
102
|
stream: stream,
|
|
103
|
-
max_tokens: max_output_tokens || model.max_output_tokens ||
|
|
103
|
+
max_tokens: max_output_tokens || model.max_output_tokens || DEFAULT_MAX_OUTPUT_TOKENS
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
add_thinking_fields(payload, thinking, model)
|
|
@@ -115,7 +115,7 @@ module RubyLLM
|
|
|
115
115
|
messages.each do |msg|
|
|
116
116
|
if msg.tool_result?
|
|
117
117
|
tool_result_blocks << Tools.format_tool_result_block(msg)
|
|
118
|
-
inject_cache_control(tool_result_blocks, caching:) if cache_boundary?(msg, caching:)
|
|
118
|
+
inject_cache_control(tool_result_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
119
119
|
next
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -196,10 +196,6 @@ module RubyLLM
|
|
|
196
196
|
ANTHROPIC_INLINE_REQUEST_LIMIT
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
-
def provider_file_upload_limit
|
|
200
|
-
ANTHROPIC_FILE_UPLOAD_LIMIT
|
|
201
|
-
end
|
|
202
|
-
|
|
203
199
|
def provider_file_attachable?(attachment)
|
|
204
200
|
attachment.image? || attachment.pdf? || attachment.text?
|
|
205
201
|
end
|
|
@@ -329,6 +325,7 @@ module RubyLLM
|
|
|
329
325
|
output_tokens: usage['output_tokens'],
|
|
330
326
|
cache_read_tokens: extract_cache_read_tokens(data),
|
|
331
327
|
cache_write_tokens: extract_cache_write_tokens(data),
|
|
328
|
+
cache_write_tokens_by_ttl: extract_cache_write_by_ttl(data),
|
|
332
329
|
thinking_tokens: thinking_tokens,
|
|
333
330
|
server_tool_use: usage['server_tool_use'],
|
|
334
331
|
finish_reason: normalize_finish_reason(data['stop_reason']),
|
|
@@ -352,12 +349,20 @@ module RubyLLM
|
|
|
352
349
|
end
|
|
353
350
|
end
|
|
354
351
|
|
|
352
|
+
def portable_raw_content(message)
|
|
353
|
+
blocks = message.raw_content
|
|
354
|
+
return unless blocks.is_a?(Array) && blocks.any? { |block| Tools.tool_search_block?(block) }
|
|
355
|
+
|
|
356
|
+
kept = blocks.reject { |block| THINKING_BLOCK_TYPES.include?(block['type']) }
|
|
357
|
+
kept if kept.all? { |block| PORTABLE_BLOCK_TYPES.include?(block['type']) || Tools.tool_search_block?(block) }
|
|
358
|
+
end
|
|
359
|
+
|
|
355
360
|
# Turns that used server tools replay their provider-shaped blocks
|
|
356
361
|
# verbatim: the API requires the tool_use/result blocks and their
|
|
357
362
|
# citations back exactly as returned.
|
|
358
363
|
def format_raw_assistant_message(msg, caching: nil)
|
|
359
364
|
blocks = msg.raw_content.dup
|
|
360
|
-
inject_cache_control(blocks, caching:) if cache_boundary?(msg, caching:)
|
|
365
|
+
inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
361
366
|
|
|
362
367
|
{ role: 'assistant', content: blocks }
|
|
363
368
|
end
|
|
@@ -366,7 +371,7 @@ module RubyLLM
|
|
|
366
371
|
content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
|
|
367
372
|
|
|
368
373
|
append_formatted_content(content_blocks, msg, citations: citations)
|
|
369
|
-
inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
|
|
374
|
+
inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
370
375
|
|
|
371
376
|
{
|
|
372
377
|
role: convert_role(msg.role),
|
|
@@ -386,7 +391,7 @@ module RubyLLM
|
|
|
386
391
|
input: tool_call.arguments
|
|
387
392
|
}
|
|
388
393
|
end
|
|
389
|
-
inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
|
|
394
|
+
inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
390
395
|
|
|
391
396
|
{
|
|
392
397
|
role: 'assistant',
|
|
@@ -404,24 +409,16 @@ module RubyLLM
|
|
|
404
409
|
blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
|
|
405
410
|
return Support::Utils.deep_dup(blocks) if blocks
|
|
406
411
|
|
|
407
|
-
[build_thinking_block(msg
|
|
412
|
+
[build_thinking_block(msg)].compact
|
|
408
413
|
end
|
|
409
414
|
|
|
410
|
-
|
|
411
|
-
|
|
415
|
+
# Claude takes a thinking block back only with the signature it issued.
|
|
416
|
+
def build_thinking_block(msg)
|
|
417
|
+
signature = own_signature(msg) if msg.thinking
|
|
418
|
+
return unless signature
|
|
412
419
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
type: 'thinking',
|
|
416
|
-
thinking: thinking.text,
|
|
417
|
-
signature: thinking.signature
|
|
418
|
-
}.compact
|
|
419
|
-
elsif thinking.signature
|
|
420
|
-
{
|
|
421
|
-
type: 'redacted_thinking',
|
|
422
|
-
data: thinking.signature
|
|
423
|
-
}
|
|
424
|
-
end
|
|
420
|
+
text = msg.thinking.text
|
|
421
|
+
text ? { type: 'thinking', thinking: text, signature: } : { type: 'redacted_thinking', data: signature }
|
|
425
422
|
end
|
|
426
423
|
|
|
427
424
|
def append_formatted_content(content_blocks, msg, citations: false)
|
|
@@ -432,22 +429,23 @@ module RubyLLM
|
|
|
432
429
|
caching != false && message.cache_until_here?
|
|
433
430
|
end
|
|
434
431
|
|
|
435
|
-
def inject_cache_control(blocks, caching: nil)
|
|
432
|
+
def inject_cache_control(blocks, caching: nil, ttl: nil)
|
|
436
433
|
return blocks if blocks.empty?
|
|
437
434
|
|
|
438
435
|
last = blocks.last
|
|
439
436
|
return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
|
|
440
437
|
return blocks unless last.is_a?(Hash)
|
|
441
438
|
|
|
442
|
-
blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
|
|
439
|
+
blocks[-1] = last.merge(cache_control: prompt_cache_control(caching, ttl:))
|
|
443
440
|
blocks
|
|
444
441
|
end
|
|
445
442
|
|
|
446
|
-
def prompt_cache_control(caching = nil)
|
|
443
|
+
def prompt_cache_control(caching = nil, ttl: nil)
|
|
447
444
|
options = prompt_cache_options(caching)
|
|
445
|
+
ttl ||= options[:ttl]
|
|
448
446
|
|
|
449
447
|
{ type: CACHE_CONTROL_TYPE }.tap do |control|
|
|
450
|
-
control[:ttl] =
|
|
448
|
+
control[:ttl] = ttl if ttl
|
|
451
449
|
end
|
|
452
450
|
end
|
|
453
451
|
|
|
@@ -485,7 +483,8 @@ module RubyLLM
|
|
|
485
483
|
|
|
486
484
|
def build_thinking_payload(thinking, model, max_tokens)
|
|
487
485
|
return nil unless thinking&.enabled?
|
|
488
|
-
|
|
486
|
+
|
|
487
|
+
return { thinking: { type: thinking_off_type(model) } } if thinking.enabled == false
|
|
489
488
|
|
|
490
489
|
effort = resolve_effort(thinking)
|
|
491
490
|
return nil if effort == 'none'
|
|
@@ -533,6 +532,14 @@ module RubyLLM
|
|
|
533
532
|
budget.clamp(minimum, [max_tokens - 1, minimum].max)
|
|
534
533
|
end
|
|
535
534
|
|
|
535
|
+
def thinking_off_type(model)
|
|
536
|
+
between_tools_off?(model.id) ? 'between_tools' : 'disabled'
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def between_tools_off?(model_id)
|
|
540
|
+
@provider.class.respond_to?(:between_tools_off?) && @provider.class.between_tools_off?(model_id)
|
|
541
|
+
end
|
|
542
|
+
|
|
536
543
|
def resolve_effort(thinking)
|
|
537
544
|
effort = thinking.effort.to_s
|
|
538
545
|
effort.empty? ? nil : effort
|
|
@@ -7,11 +7,17 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
attachment.provider_file? || %i[image pdf text].include?(attachment.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def format_content(content, attachments = [], citations: false)
|
|
11
15
|
parts = []
|
|
12
16
|
parts << format_text(content) unless content.nil? || content.empty?
|
|
13
17
|
|
|
14
18
|
attachments.each do |attachment|
|
|
19
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
20
|
+
|
|
15
21
|
if attachment.provider_file?
|
|
16
22
|
parts << format_provider_file(attachment, citations: citations)
|
|
17
23
|
next
|
|
@@ -24,8 +30,6 @@ module RubyLLM
|
|
|
24
30
|
parts << format_pdf(attachment, citations: citations)
|
|
25
31
|
when :text
|
|
26
32
|
parts << (citations ? format_text_document(attachment) : format_text_file(attachment))
|
|
27
|
-
else
|
|
28
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
|
|
@@ -102,6 +102,16 @@ module RubyLLM
|
|
|
102
102
|
breakdown.values.compact.sum
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def extract_cache_write_by_ttl(data)
|
|
106
|
+
breakdown = message_usage(data)['cache_creation'] || delta_usage(data)['cache_creation']
|
|
107
|
+
return unless breakdown.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
breakdown.filter_map do |key, count|
|
|
110
|
+
ttl = key[/\Aephemeral_(.+)_input_tokens\z/, 1]
|
|
111
|
+
[ttl, count] if ttl
|
|
112
|
+
end.to_h
|
|
113
|
+
end
|
|
114
|
+
|
|
105
115
|
def message_usage(data)
|
|
106
116
|
aggregate_usage(data.dig('message', 'usage'))
|
|
107
117
|
end
|
|
@@ -121,7 +131,12 @@ module RubyLLM
|
|
|
121
131
|
|
|
122
132
|
usage.merge(ITERATION_TOKEN_KEYS.to_h do |key|
|
|
123
133
|
[key, iterations.sum { |iteration| iteration[key].to_i }]
|
|
124
|
-
end)
|
|
134
|
+
end, { 'cache_creation' => iteration_cache_creation(iterations) }.compact)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def iteration_cache_creation(iterations)
|
|
138
|
+
breakdowns = iterations.map { |iteration| iteration['cache_creation'] }.grep(Hash)
|
|
139
|
+
breakdowns.reduce { |total, breakdown| total.merge(breakdown) { |_, sum, count| sum.to_i + count.to_i } }
|
|
125
140
|
end
|
|
126
141
|
end
|
|
127
142
|
end
|