ruby_llm 2.0.0.rc4 → 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 +66 -9
- data/lib/generators/ruby_llm/generator_helpers.rb +72 -7
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -8
- 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 +131 -28
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +145 -46
- 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 +213 -48
- 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 +502 -96
- 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 +35 -3
- 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 +55 -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 +217 -42
- 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 +17 -49
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +91 -19
- 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 +24 -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 +23 -7
- 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 +12 -4
- 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/tools/{server_tools.rb → provider_tools.rb} +7 -7
- 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 +96 -38
- 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,25 +94,31 @@ 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,
|
|
98
102
|
max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
|
|
99
|
-
usage_recorder: nil,
|
|
100
|
-
resolution =
|
|
103
|
+
usage_recorder: nil, provider_tools: [], compaction: nil, end_user: nil, &)
|
|
104
|
+
resolution = resolve_provider_tools_for_request(provider_tools)
|
|
101
105
|
headers = resolution.headers.merge(headers) if resolution
|
|
102
106
|
headers = apply_compaction_headers(headers, compaction) if compaction
|
|
103
107
|
payload = render(
|
|
104
108
|
messages, tools:, tool_prefs:, temperature:, max_output_tokens:, provider_options:, schema:, thinking:,
|
|
105
|
-
citations:, caching:, compaction:, end_user:, before_request:,
|
|
109
|
+
citations:, caching:, compaction:, end_user:, before_request:, provider_tools:,
|
|
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,11 +126,14 @@ 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,
|
|
124
135
|
max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
|
|
125
|
-
stream: false,
|
|
136
|
+
stream: false, provider_tools: [], compaction: nil, end_user: nil)
|
|
126
137
|
payload = render_payload(
|
|
127
138
|
messages,
|
|
128
139
|
tools: tools,
|
|
@@ -139,7 +150,8 @@ module RubyLLM
|
|
|
139
150
|
payload = apply_end_user(payload, end_user) if end_user
|
|
140
151
|
payload = apply_compaction(payload, compaction) if compaction
|
|
141
152
|
payload = Support::Utils.deep_merge(payload, provider_options)
|
|
142
|
-
payload =
|
|
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"
|
|
@@ -188,8 +200,8 @@ module RubyLLM
|
|
|
188
200
|
end
|
|
189
201
|
|
|
190
202
|
# The alias table mapping portable server tool names to this protocol's
|
|
191
|
-
# wire format. Protocols with
|
|
192
|
-
# +nil+ means the protocol has no
|
|
203
|
+
# wire format. Protocols with provider-tool support override this;
|
|
204
|
+
# +nil+ means the protocol has no provider-tool support at all.
|
|
193
205
|
def server_tool_aliases
|
|
194
206
|
nil
|
|
195
207
|
end
|
|
@@ -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,34 +517,145 @@ module RubyLLM
|
|
|
470
517
|
@provider.parse_error(response)
|
|
471
518
|
end
|
|
472
519
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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)
|
|
476
549
|
return message if message.attachments.empty?
|
|
477
550
|
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
480
564
|
|
|
481
|
-
|
|
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
|
|
482
576
|
end
|
|
483
577
|
|
|
484
578
|
private
|
|
485
579
|
|
|
486
|
-
|
|
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
|
|
608
|
+
|
|
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)
|
|
614
|
+
end
|
|
615
|
+
|
|
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
|
|
622
|
+
|
|
623
|
+
message.tool_call? && message.tool_calls.each_value.any?(&:thought_signature)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Only a usage entry names the producer: a model id alone can belong
|
|
627
|
+
# to several providers.
|
|
628
|
+
def producer_slug(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
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def resolve_provider_tools_for_request(entries)
|
|
487
645
|
return nil if entries.nil? || entries.empty?
|
|
488
646
|
|
|
489
647
|
aliases = server_tool_aliases
|
|
490
648
|
unless aliases
|
|
491
649
|
raise UnsupportedServerToolError,
|
|
492
|
-
"#{@provider.name} has no
|
|
650
|
+
"#{@provider.name} has no provider-tool support through RubyLLM yet. " \
|
|
493
651
|
'Request options in the provider vocabulary can be set with with_provider_options.'
|
|
494
652
|
end
|
|
495
653
|
|
|
496
|
-
RubyLLM::Tools::
|
|
654
|
+
RubyLLM::Tools::ProviderTools.resolve(entries, aliases: aliases, owner: @provider.name)
|
|
497
655
|
end
|
|
498
656
|
|
|
499
|
-
def
|
|
500
|
-
resolution =
|
|
657
|
+
def apply_provider_tools(payload, entries)
|
|
658
|
+
resolution = resolve_provider_tools_for_request(entries)
|
|
501
659
|
return payload unless resolution
|
|
502
660
|
|
|
503
661
|
payload = Support::Utils.deep_merge(payload, resolution.payload) unless resolution.payload.empty?
|
|
@@ -505,17 +663,22 @@ module RubyLLM
|
|
|
505
663
|
payload
|
|
506
664
|
end
|
|
507
665
|
|
|
508
|
-
#
|
|
666
|
+
# Provider tools join function tools in the payload's tools array. The
|
|
509
667
|
# entry shape comes from the alias table or the caller's raw Hash.
|
|
510
668
|
def merge_server_tool_entries(payload, entries)
|
|
511
669
|
payload[:tools] = Array(payload[:tools]) + entries
|
|
512
670
|
end
|
|
513
671
|
|
|
514
|
-
def
|
|
672
|
+
def apply_tool_search(payload)
|
|
673
|
+
payload
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def track_usage(operation, on_finish: nil, model_id: nil)
|
|
515
677
|
@usage_tracker = Accounting::Usage::Tracker.new(
|
|
516
678
|
operation:,
|
|
517
679
|
provider: @provider,
|
|
518
680
|
model: @model,
|
|
681
|
+
model_id:,
|
|
519
682
|
config: @config,
|
|
520
683
|
on_finish:
|
|
521
684
|
)
|
|
@@ -546,8 +709,24 @@ module RubyLLM
|
|
|
546
709
|
return attachment if attachment.provider_file?
|
|
547
710
|
return attachment unless upload_large_attachment?(attachment)
|
|
548
711
|
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
551
730
|
end
|
|
552
731
|
|
|
553
732
|
# Uploads are memoized per provider on the attachment itself, so a chat
|
|
@@ -559,8 +738,9 @@ module RubyLLM
|
|
|
559
738
|
upload = attachment.provider_uploads[scope]
|
|
560
739
|
return upload if upload && !upload.expired?
|
|
561
740
|
|
|
562
|
-
attachment.provider_uploads[scope] =
|
|
741
|
+
attachment.provider_uploads[scope] = StoredUploads.new(@provider, attachment.provider_file_store).fetch do
|
|
563
742
|
@provider.upload_file(attachment, **provider_file_upload_options(attachment))
|
|
743
|
+
end
|
|
564
744
|
end
|
|
565
745
|
|
|
566
746
|
# A file id belongs to the account that uploaded it, so the memo is keyed
|
|
@@ -572,6 +752,19 @@ module RubyLLM
|
|
|
572
752
|
"#{@provider.slug}:#{Digest::SHA256.hexdigest(credentials.join("\0"))}"
|
|
573
753
|
end
|
|
574
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
|
+
|
|
575
768
|
def upload_large_attachment?(attachment)
|
|
576
769
|
size = attachment.byte_size
|
|
577
770
|
size && size > default_large_file_upload_threshold && provider_file_attachable?(attachment)
|
|
@@ -581,10 +774,6 @@ module RubyLLM
|
|
|
581
774
|
Float::INFINITY
|
|
582
775
|
end
|
|
583
776
|
|
|
584
|
-
def provider_file_upload_limit
|
|
585
|
-
nil
|
|
586
|
-
end
|
|
587
|
-
|
|
588
777
|
def provider_file_attachable?(_attachment)
|
|
589
778
|
false
|
|
590
779
|
end
|
|
@@ -593,20 +782,6 @@ module RubyLLM
|
|
|
593
782
|
{}
|
|
594
783
|
end
|
|
595
784
|
|
|
596
|
-
def ensure_provider_file_size!(attachment)
|
|
597
|
-
limit = provider_file_upload_limit
|
|
598
|
-
return unless limit && attachment.byte_size.to_i > limit
|
|
599
|
-
|
|
600
|
-
raise Error, "#{@provider.name} file uploads support files up to #{format_bytes(limit)}; " \
|
|
601
|
-
"#{attachment.filename} is #{format_bytes(attachment.byte_size)}"
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
def format_bytes(bytes)
|
|
605
|
-
return 'unknown size' unless bytes
|
|
606
|
-
|
|
607
|
-
"#{(bytes.to_f / (1024 * 1024)).round(1)} MB"
|
|
608
|
-
end
|
|
609
|
-
|
|
610
785
|
def validate_paint_inputs!(with:, mask:)
|
|
611
786
|
return if with.nil? && mask.nil?
|
|
612
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
|