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/provider.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
3
4
|
require 'json'
|
|
4
5
|
require 'ruby_llm/error'
|
|
5
6
|
|
|
@@ -110,6 +111,10 @@ module RubyLLM
|
|
|
110
111
|
self.class.configuration_requirements
|
|
111
112
|
end
|
|
112
113
|
|
|
114
|
+
def original_image_detail? # :nodoc:
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
113
118
|
def protocols # :nodoc:
|
|
114
119
|
self.class.protocols
|
|
115
120
|
end
|
|
@@ -200,17 +205,13 @@ module RubyLLM
|
|
|
200
205
|
)
|
|
201
206
|
end
|
|
202
207
|
|
|
203
|
-
def
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
tool_prefs: nil,
|
|
211
|
-
citations: false
|
|
212
|
-
)
|
|
213
|
-
protocol_class.new(self, model).preprocess_message(message)
|
|
208
|
+
def preprocess_messages(messages, model:, protocol: nil, unsupported_attachment: nil) # :nodoc:
|
|
209
|
+
preprocessor = preprocessing_protocol(protocol, model)
|
|
210
|
+
messages.map { |message| preprocessor.preprocess_message(message, unsupported_attachment:) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def discard_missing_uploads(messages, error, model:, protocol: nil) # :nodoc:
|
|
214
|
+
preprocessing_protocol(protocol, model).discard_missing_uploads(messages, error)
|
|
214
215
|
end
|
|
215
216
|
|
|
216
217
|
def batches? # :nodoc:
|
|
@@ -256,7 +257,13 @@ module RubyLLM
|
|
|
256
257
|
batch_tier = pricing.batch unless long_context_pricing?(pricing, tokens)
|
|
257
258
|
batch = Cost.new(tokens:, model:, category:, tier: :batch) if batch_tier
|
|
258
259
|
amounts = batch_cost_amounts(standard:, batch:, batch_tier:, model:)
|
|
259
|
-
Cost.
|
|
260
|
+
Cost.new(amounts:, missing: batch_cost_missing(amounts, standard), reported: standard.tokens?)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def batch_cost_missing(amounts, standard) # :nodoc:
|
|
264
|
+
amounts.filter_map do |component, amount|
|
|
265
|
+
component if amount.nil? && (standard.missing?(component) || standard.public_send(component)&.positive?)
|
|
266
|
+
end
|
|
260
267
|
end
|
|
261
268
|
|
|
262
269
|
def batch_cost_amounts(standard:, batch:, batch_tier:, model:) # :nodoc:
|
|
@@ -288,6 +295,21 @@ module RubyLLM
|
|
|
288
295
|
protocols.key?(:files)
|
|
289
296
|
end
|
|
290
297
|
|
|
298
|
+
# Returns a String naming the account that owns the files this provider
|
|
299
|
+
# uploads, or +nil+ to keep each upload within the process that made it.
|
|
300
|
+
# The base implementation returns +nil+. Rails chats reuse an upload in
|
|
301
|
+
# later processes only under the same identity, so build it from what
|
|
302
|
+
# decides where uploaded files live, such as the endpoint and API key,
|
|
303
|
+
# an organization or project, or a storage bucket. Digest anything
|
|
304
|
+
# secret, and leave out credentials that rotate, such as session tokens:
|
|
305
|
+
#
|
|
306
|
+
# def account_identity
|
|
307
|
+
# Digest::SHA256.hexdigest([api_base, @config.acme_api_key].join("\0"))
|
|
308
|
+
# end
|
|
309
|
+
def account_identity
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
291
313
|
def list_models # :nodoc:
|
|
292
314
|
listing_protocol.new(self).list_models
|
|
293
315
|
end
|
|
@@ -316,8 +338,8 @@ module RubyLLM
|
|
|
316
338
|
)
|
|
317
339
|
end
|
|
318
340
|
|
|
319
|
-
def research_later(prompt, **
|
|
320
|
-
fetch_protocol(:research).new(self).create_research_job(prompt, **
|
|
341
|
+
def research_later(prompt, **) # :nodoc:
|
|
342
|
+
fetch_protocol(:research).new(self).create_research_job(prompt, **)
|
|
321
343
|
end
|
|
322
344
|
|
|
323
345
|
def find_research_job(id) # :nodoc:
|
|
@@ -374,6 +396,11 @@ module RubyLLM
|
|
|
374
396
|
protocol.new(self, model).rerank(query, documents, model: model_id_for(model), top_n:, provider_options:)
|
|
375
397
|
end
|
|
376
398
|
|
|
399
|
+
def judge(input, questions:, model:, with: [], provider_options: {}) # :nodoc:
|
|
400
|
+
protocol = resolve_protocol(nil, model, operation: :judge)
|
|
401
|
+
protocol.new(self, model).judge(input, questions:, model: model_id_for(model), with:, provider_options:)
|
|
402
|
+
end
|
|
403
|
+
|
|
377
404
|
def upload_file(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil, # :nodoc:
|
|
378
405
|
provider_options: {})
|
|
379
406
|
ensure_files_supported!
|
|
@@ -466,6 +493,20 @@ module RubyLLM
|
|
|
466
493
|
nil
|
|
467
494
|
end
|
|
468
495
|
|
|
496
|
+
# Returns the thinking options that turn thinking off for +model_id+, or
|
|
497
|
+
# +nil+ when the provider has no off control beyond the model registry.
|
|
498
|
+
def thinking_off_control(_model_id) # :nodoc:
|
|
499
|
+
nil
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Returns the multiple of the input price that a prompt cache write
|
|
503
|
+
# with lifetime +ttl+ costs on +model_id+, or +nil+ when the
|
|
504
|
+
# registry's cache-write price applies. Providers whose models.dev
|
|
505
|
+
# cache-write price covers only the default lifetime override it.
|
|
506
|
+
def cache_write_input_multiplier(_model_id, _ttl) # :nodoc:
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
469
510
|
def models_dev_alias(_model_id, _models_dev_by_key, _provider_model = nil) # :nodoc:
|
|
470
511
|
nil
|
|
471
512
|
end
|
|
@@ -576,6 +617,13 @@ module RubyLLM
|
|
|
576
617
|
model_id
|
|
577
618
|
end
|
|
578
619
|
|
|
620
|
+
# Returns the id of the model that the deployment named +model_id+
|
|
621
|
+
# runs, or +nil+ when +model_id+ is not a declared deployment.
|
|
622
|
+
# Providers whose requests name deployments override it.
|
|
623
|
+
def deployed_model_id(_model_id, _config = nil)
|
|
624
|
+
nil
|
|
625
|
+
end
|
|
626
|
+
|
|
579
627
|
# Returns the global registry of providers, a hash mapping slug
|
|
580
628
|
# symbols to provider classes.
|
|
581
629
|
def providers
|
|
@@ -624,6 +672,11 @@ module RubyLLM
|
|
|
624
672
|
explicit ? fetch_protocol(explicit) : protocol_for(model, **request)
|
|
625
673
|
end
|
|
626
674
|
|
|
675
|
+
def preprocessing_protocol(protocol, model)
|
|
676
|
+
resolve_protocol(protocol, model, tools: {}, schema: nil, thinking: nil, tool_prefs: nil, citations: false)
|
|
677
|
+
.new(self, model)
|
|
678
|
+
end
|
|
679
|
+
|
|
627
680
|
def default_protocol
|
|
628
681
|
fetch_protocol(configured_protocol || self.class.default_protocol)
|
|
629
682
|
end
|
|
@@ -667,6 +720,10 @@ module RubyLLM
|
|
|
667
720
|
model.respond_to?(:id) ? model.id : model
|
|
668
721
|
end
|
|
669
722
|
|
|
723
|
+
def account_digest(*parts)
|
|
724
|
+
Digest::SHA256.hexdigest(parts.join("\0"))
|
|
725
|
+
end
|
|
726
|
+
|
|
670
727
|
def try_parse_json(maybe_json)
|
|
671
728
|
return maybe_json unless maybe_json.is_a?(String)
|
|
672
729
|
|
|
@@ -689,7 +746,22 @@ module RubyLLM
|
|
|
689
746
|
return error if error.is_a?(String)
|
|
690
747
|
|
|
691
748
|
nested_message = error['message'] if error.is_a?(Hash)
|
|
692
|
-
[nested_message, part['message'], part['detail']].find { |message| message.is_a?(String) }
|
|
749
|
+
[nested_message, part['message'], part['detail']].find { |message| message.is_a?(String) } ||
|
|
750
|
+
validation_detail_message(part['detail'])
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def validation_detail_message(detail)
|
|
754
|
+
return unless detail.is_a?(Array)
|
|
755
|
+
|
|
756
|
+
messages = detail.filter_map { |error| format_validation_error(error) }
|
|
757
|
+
messages.join('; ') unless messages.empty?
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def format_validation_error(error)
|
|
761
|
+
return unless error.is_a?(Hash)
|
|
762
|
+
|
|
763
|
+
text = [Array(error['loc']).join('.'), error['msg']&.to_s].compact.reject(&:empty?).join(': ')
|
|
764
|
+
text unless text.empty?
|
|
693
765
|
end
|
|
694
766
|
|
|
695
767
|
def ensure_configured!
|
|
@@ -4,6 +4,11 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Anthropic Claude API integration.
|
|
6
6
|
class Anthropic < Provider
|
|
7
|
+
SONNET_55_MODEL_IDS = %w[claude-sonnet-5-5 anthropic.claude-sonnet-5-5].freeze
|
|
8
|
+
private_constant :SONNET_55_MODEL_IDS
|
|
9
|
+
CACHE_WRITE_INPUT_MULTIPLIERS = { '1h' => 2 }.freeze
|
|
10
|
+
private_constant :CACHE_WRITE_INPUT_MULTIPLIERS
|
|
11
|
+
|
|
7
12
|
protocol :anthropic, Protocols::Anthropic, batches: Protocols::Anthropic::Batches
|
|
8
13
|
protocol :files, Protocols::Anthropic::Files
|
|
9
14
|
|
|
@@ -18,6 +23,10 @@ module RubyLLM
|
|
|
18
23
|
}
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
def account_identity
|
|
27
|
+
account_digest(api_base, @config.anthropic_api_key)
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
def batch_cost_multiplier(**) = 0.5
|
|
22
31
|
|
|
23
32
|
class << self
|
|
@@ -25,6 +34,20 @@ module RubyLLM
|
|
|
25
34
|
Anthropic::Capabilities
|
|
26
35
|
end
|
|
27
36
|
|
|
37
|
+
def between_tools_off?(model_id) # :nodoc:
|
|
38
|
+
SONNET_55_MODEL_IDS.include?(model_id.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def thinking_off_control(model_id) # :nodoc:
|
|
42
|
+
{ enabled: false } if between_tools_off?(model_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# models.dev prices the five-minute cache write; Anthropic bills a
|
|
46
|
+
# one-hour write at twice the input price.
|
|
47
|
+
def cache_write_input_multiplier(_model_id, ttl) # :nodoc:
|
|
48
|
+
CACHE_WRITE_INPUT_MULTIPLIERS[ttl.to_s]
|
|
49
|
+
end
|
|
50
|
+
|
|
28
51
|
def configuration_options
|
|
29
52
|
%i[anthropic_api_key anthropic_api_base]
|
|
30
53
|
end
|
|
@@ -9,8 +9,8 @@ module RubyLLM
|
|
|
9
9
|
@provider.azure_media_url(super)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def render_edit_payload(prompt, size:, provider_options:, **
|
|
13
|
-
super(prompt, size:, provider_options: { size: size }.compact.merge(provider_options), **
|
|
12
|
+
def render_edit_payload(prompt, size:, provider_options:, **)
|
|
13
|
+
super(prompt, size:, provider_options: { size: size }.compact.merge(provider_options), **)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def json_image_references?(_model)
|
|
@@ -7,17 +7,22 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
%i[image audio text].include?(attachment.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def format_content(content, attachments = [])
|
|
11
15
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
|
+
|
|
12
18
|
case attachment.type
|
|
13
19
|
when :image
|
|
14
|
-
format_image(attachment)
|
|
20
|
+
Protocols::ChatCompletions::Media.with_image_detail(format_image(attachment), attachment,
|
|
21
|
+
original_detail: true)
|
|
15
22
|
when :audio
|
|
16
23
|
Protocols::ChatCompletions::Media.format_audio(attachment)
|
|
17
24
|
when :text
|
|
18
25
|
Protocols::ChatCompletions::Media.format_text_file(attachment)
|
|
19
|
-
else
|
|
20
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
end
|
|
@@ -17,11 +17,16 @@ module RubyLLM
|
|
|
17
17
|
{ model: model, prompt: prompt }.merge(provider_options)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def parse_video_job(response, model
|
|
20
|
+
def parse_video_job(response, model:, **request)
|
|
21
21
|
id = response.body['id']
|
|
22
22
|
raise Error.new('Azure did not return a video generation job', response:) unless id
|
|
23
23
|
|
|
24
|
-
VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
|
|
24
|
+
VideoJob.new(id: id, protocol: self, model: model, raw: response.body, **request)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def video_request_settings(request)
|
|
28
|
+
width, height = request.values_at(:width, :height)
|
|
29
|
+
{ duration: request[:n_seconds], resolution: ("#{width}x#{height}" if width && height) }
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def video_job_url(job)
|
|
@@ -13,12 +13,22 @@ module RubyLLM
|
|
|
13
13
|
protocol :files, Protocols::Azure::Files
|
|
14
14
|
protocol :cohere, Azure::Cohere
|
|
15
15
|
|
|
16
|
+
def original_image_detail? # :nodoc:
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
def api_base
|
|
17
21
|
@config.azure_api_base
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
24
|
+
# Files belong to the Azure resource, whichever key or Entra ID token
|
|
25
|
+
# signs the request.
|
|
26
|
+
def account_identity
|
|
27
|
+
account_digest(azure_openai_v1_base)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Deployments named after gpt-5.4+ and gpt-6+ models need the Responses
|
|
31
|
+
# API for tool use, so they route there automatically. Deployment names often
|
|
22
32
|
# differ from model ids, so the routing stays conservative; an explicit
|
|
23
33
|
# protocol: or the azure_protocol configuration option overrides it.
|
|
24
34
|
def protocol_for(model, operation: nil, **)
|
|
@@ -28,7 +38,7 @@ module RubyLLM
|
|
|
28
38
|
return protocols[:cohere]
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
model.id.match?(/gpt-5\.[4-9]|gpt-5\d/) ? protocols[:responses] : super
|
|
41
|
+
model.id.match?(/gpt-5\.[4-9]|gpt-5\d|gpt-[6-9]/) ? protocols[:responses] : super
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
def headers
|
|
@@ -108,7 +118,17 @@ module RubyLLM
|
|
|
108
118
|
end
|
|
109
119
|
|
|
110
120
|
def configuration_options
|
|
111
|
-
%i[azure_api_base azure_api_key azure_ai_auth_token]
|
|
121
|
+
%i[azure_api_base azure_api_key azure_ai_auth_token azure_deployments]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def deployed_model_id(deployment, config = RubyLLM.config)
|
|
125
|
+
deployments = config.azure_deployments || {}
|
|
126
|
+
unless deployments.is_a?(Hash)
|
|
127
|
+
raise ConfigurationError, 'azure_deployments must be a Hash of deployment names to model ids, ' \
|
|
128
|
+
"got #{deployments.class}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
(deployments[deployment.to_s] || deployments[deployment.to_sym])&.to_s
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
def configuration_requirements
|
|
@@ -61,9 +61,7 @@ module RubyLLM
|
|
|
61
61
|
raise ArgumentError, 'Transcription requires exactly one audio file' unless attachments.one?
|
|
62
62
|
|
|
63
63
|
attachment = attachments.first
|
|
64
|
-
|
|
65
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
66
|
-
end
|
|
64
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.audio?
|
|
67
65
|
|
|
68
66
|
content = [
|
|
69
67
|
Protocols::ChatCompletions::Media.format_audio(attachment),
|
|
@@ -35,21 +35,17 @@ module RubyLLM
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def signed_post(url, payload, additional_headers = {})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@connection.post(url, payload, usage: @usage_tracker) do |req|
|
|
41
|
-
req.headers.merge!(mantle_headers(url, body))
|
|
42
|
-
req.headers.merge!(additional_headers) unless additional_headers.empty?
|
|
38
|
+
@connection.post(url, JSON.generate(payload), usage: @usage_tracker) do |req|
|
|
39
|
+
req.headers.merge!(mantle_headers.merge(additional_headers))
|
|
43
40
|
end
|
|
44
41
|
end
|
|
45
42
|
|
|
46
43
|
def stream_response(payload, additional_headers = {}, &)
|
|
47
|
-
|
|
48
|
-
super(payload, mantle_headers(stream_url, body).merge(additional_headers), &)
|
|
44
|
+
super(JSON.generate(payload), mantle_headers.merge(additional_headers), &)
|
|
49
45
|
end
|
|
50
46
|
|
|
51
|
-
def mantle_headers
|
|
52
|
-
|
|
47
|
+
def mantle_headers
|
|
48
|
+
{}
|
|
53
49
|
end
|
|
54
50
|
end
|
|
55
51
|
end
|
|
@@ -28,6 +28,20 @@ module RubyLLM
|
|
|
28
28
|
Bedrock::Capabilities
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def self.between_tools_off?(model_id) # :nodoc:
|
|
32
|
+
Providers::Anthropic.between_tools_off?(Protocols::Converse::Chat.foundation_model_id(model_id))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.thinking_off_control(model_id) # :nodoc:
|
|
36
|
+
{ enabled: false } if between_tools_off?(model_id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.cache_write_input_multiplier(model_id, ttl) # :nodoc:
|
|
40
|
+
return unless Protocols::Converse::Chat.foundation_model_id(model_id).start_with?('anthropic.')
|
|
41
|
+
|
|
42
|
+
Providers::Anthropic.cache_write_input_multiplier(model_id, ttl)
|
|
43
|
+
end
|
|
44
|
+
|
|
31
45
|
def self.resolve_registry_id(model_id, models, config = RubyLLM.config)
|
|
32
46
|
Models.resolve_registry_id(model_id, models, config)
|
|
33
47
|
end
|
|
@@ -36,6 +50,11 @@ module RubyLLM
|
|
|
36
50
|
Models.models_dev_alias(...)
|
|
37
51
|
end
|
|
38
52
|
|
|
53
|
+
def initialize(config) # :nodoc:
|
|
54
|
+
super
|
|
55
|
+
@connection = signed_connection(api_base)
|
|
56
|
+
end
|
|
57
|
+
|
|
39
58
|
def protocol_for(model, operation: nil, **)
|
|
40
59
|
return protocols[:guardrails] if operation == :moderate
|
|
41
60
|
|
|
@@ -62,13 +81,20 @@ module RubyLLM
|
|
|
62
81
|
end
|
|
63
82
|
|
|
64
83
|
def mantle_connection
|
|
65
|
-
@mantle_connection ||=
|
|
84
|
+
@mantle_connection ||= signed_connection(mantle_api_base, service: Bedrock::Mantle::SIGNING_SERVICE)
|
|
66
85
|
end
|
|
67
86
|
|
|
68
87
|
def api_base
|
|
69
88
|
@config.bedrock_api_base || "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
# Uploads are objects in the configured bucket, readable with any
|
|
92
|
+
# credentials the bucket admits.
|
|
93
|
+
def account_identity
|
|
94
|
+
bucket = @config.bedrock_batch_s3_uri
|
|
95
|
+
account_digest(bucket) if bucket
|
|
96
|
+
end
|
|
97
|
+
|
|
72
98
|
def control_api_base
|
|
73
99
|
@config.bedrock_api_base || "https://bedrock.#{bedrock_region}.amazonaws.com"
|
|
74
100
|
end
|
|
@@ -78,16 +104,12 @@ module RubyLLM
|
|
|
78
104
|
end
|
|
79
105
|
|
|
80
106
|
def agent_connection # :nodoc:
|
|
81
|
-
@agent_connection ||=
|
|
107
|
+
@agent_connection ||= signed_connection(agent_api_base)
|
|
82
108
|
end
|
|
83
109
|
|
|
84
110
|
def rerank_model_arn(model_id) # :nodoc:
|
|
85
111
|
return model_id if model_id.start_with?('arn:')
|
|
86
112
|
|
|
87
|
-
unless %w[amazon.rerank-v1:0 cohere.rerank-v3-5:0].include?(model_id)
|
|
88
|
-
raise Error, "Bedrock reranking is not supported for #{model_id.inspect}"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
113
|
"arn:aws:bedrock:#{bedrock_region}::foundation-model/#{model_id}"
|
|
92
114
|
end
|
|
93
115
|
|
|
@@ -190,6 +212,16 @@ module RubyLLM
|
|
|
190
212
|
|
|
191
213
|
private
|
|
192
214
|
|
|
215
|
+
# Signs each attempt as it is sent, so a retry never replays a
|
|
216
|
+
# signature that expired while the previous attempt waited.
|
|
217
|
+
def signed_connection(base_url, service: 'bedrock')
|
|
218
|
+
signer = lambda do |env|
|
|
219
|
+
signed = sign_headers(env.method.to_s.upcase, env.url.request_uri, env.body.to_s, base_url:, service:)
|
|
220
|
+
{ 'X-Amz-Security-Token' => nil }.merge(signed)
|
|
221
|
+
end
|
|
222
|
+
Transport::Connection.new(self, @config, api_base: base_url, signer:)
|
|
223
|
+
end
|
|
224
|
+
|
|
193
225
|
def voxtral_transcription?(operation, model_id)
|
|
194
226
|
operation == :transcribe && model_id == 'mistral.voxtral-small-24b-2507'
|
|
195
227
|
end
|
|
@@ -57,12 +57,17 @@ module RubyLLM
|
|
|
57
57
|
payload
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
def supported_attachment?(attachment)
|
|
61
|
+
Protocols::ChatCompletions::Media.supported_attachment?(
|
|
62
|
+
attachment, document_attachments: :none, audio_attachments: false
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
60
66
|
def format_content(content, attachments = [])
|
|
61
67
|
Protocols::ChatCompletions::Media.format_content(
|
|
62
68
|
content,
|
|
63
69
|
attachments,
|
|
64
70
|
document_attachments: :none,
|
|
65
|
-
image_attachments: false,
|
|
66
71
|
audio_attachments: false
|
|
67
72
|
)
|
|
68
73
|
end
|
|
@@ -61,6 +61,12 @@ module RubyLLM
|
|
|
61
61
|
def format_document(document)
|
|
62
62
|
raise UnsupportedAttachmentError, document.mime_type
|
|
63
63
|
end
|
|
64
|
+
|
|
65
|
+
def supported_attachment?(attachment)
|
|
66
|
+
return attachment.image? if attachment.provider_file?
|
|
67
|
+
|
|
68
|
+
%i[image text].include?(attachment.type)
|
|
69
|
+
end
|
|
64
70
|
end
|
|
65
71
|
end
|
|
66
72
|
end
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class GPUStack
|
|
6
|
-
#
|
|
7
|
-
# as base64 data URLs so the cluster never has to fetch remote sources.
|
|
6
|
+
# Formats media content for GPUStack chat completions.
|
|
8
7
|
module Media
|
|
9
8
|
module_function
|
|
10
9
|
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
%i[image audio video text].include?(attachment.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
11
14
|
def format_content(content, attachments = [])
|
|
12
15
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
|
+
|
|
13
18
|
case attachment.type
|
|
14
19
|
when :image
|
|
15
20
|
format_image(attachment)
|
|
@@ -19,8 +24,6 @@ module RubyLLM
|
|
|
19
24
|
format_video(attachment)
|
|
20
25
|
when :text
|
|
21
26
|
Protocols::ChatCompletions::Media.format_text_file(attachment)
|
|
22
|
-
else
|
|
23
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
|
@@ -38,7 +41,7 @@ module RubyLLM
|
|
|
38
41
|
def format_video(video)
|
|
39
42
|
{
|
|
40
43
|
type: 'video_url',
|
|
41
|
-
video_url: { url: video.
|
|
44
|
+
video_url: { url: video.for_llm }
|
|
42
45
|
}
|
|
43
46
|
end
|
|
44
47
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Hetzner
|
|
6
|
+
# Chat methods of the Hetzner Inference API integration
|
|
7
|
+
module Chat
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def format_role(role)
|
|
11
|
+
role.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def supported_attachment?(attachment)
|
|
15
|
+
Protocols::ChatCompletions::Media.supported_attachment?(
|
|
16
|
+
attachment, document_attachments: :none, audio_attachments: false
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def format_content(content, attachments = [])
|
|
21
|
+
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
22
|
+
if attachment.type == :image && !attachment.provider_file?
|
|
23
|
+
format_inline_image(attachment)
|
|
24
|
+
else
|
|
25
|
+
Protocols::ChatCompletions::Media.format_attachment(
|
|
26
|
+
attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Hetzner's servers can't fetch URLs, so remote images travel inline too.
|
|
33
|
+
def format_inline_image(image)
|
|
34
|
+
{ type: 'image_url', image_url: { url: image.for_llm } }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Hetzner Inference API integration.
|
|
6
|
+
class Hetzner < Provider
|
|
7
|
+
# Hetzner's dialect of the Chat Completions API.
|
|
8
|
+
class ChatCompletions < Protocols::ChatCompletions
|
|
9
|
+
include Hetzner::Chat
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
protocol :chat_completions, ChatCompletions
|
|
13
|
+
|
|
14
|
+
def api_base
|
|
15
|
+
@config.hetzner_api_base || 'https://inference.hetzner.com/api/v1'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def headers
|
|
19
|
+
{ 'Authorization' => "Bearer #{@config.hetzner_api_key}" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def configuration_options
|
|
24
|
+
%i[hetzner_api_key hetzner_api_base]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configuration_requirements
|
|
28
|
+
%i[hetzner_api_key]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns +true+: Hetzner Inference is experimental and changes its
|
|
32
|
+
# model selection faster than the registry tracks it, so model ids
|
|
33
|
+
# are accepted as given. Call RubyLLM.models.refresh to pull the
|
|
34
|
+
# live catalog.
|
|
35
|
+
def assume_models_exist?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|