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
|
@@ -31,8 +31,8 @@ module RubyLLM
|
|
|
31
31
|
"#{@provider.model_path(@model.id)}:countTokens"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def render_count_tokens_payload(messages, **
|
|
35
|
-
count_tokens_request(messages, **
|
|
34
|
+
def render_count_tokens_payload(messages, **)
|
|
35
|
+
count_tokens_request(messages, **)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def caches_url
|
|
@@ -35,6 +35,7 @@ module RubyLLM
|
|
|
35
35
|
parts = data.dig('candidates', 0, 'content', 'parts') || []
|
|
36
36
|
segments = parts.filter_map { |part| parse_transcription_segment(part) }
|
|
37
37
|
text = parts.filter_map { |part| part['text'] || part.dig('audioTranscription', 'text') }.join
|
|
38
|
+
raise_if_transcription_blocked(data, text, response)
|
|
38
39
|
words = segments.flat_map { |segment| segment['words'] }
|
|
39
40
|
RubyLLM::Transcription.new(text:, model:, segments: segments.empty? ? nil : segments,
|
|
40
41
|
words: words.empty? ? nil : words, **extract_usage(data))
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'stringio'
|
|
4
|
-
|
|
5
3
|
module RubyLLM
|
|
6
4
|
module Providers
|
|
7
5
|
# Google Vertex AI implementation
|
|
@@ -28,6 +26,18 @@ module RubyLLM
|
|
|
28
26
|
VertexAI::Capabilities
|
|
29
27
|
end
|
|
30
28
|
|
|
29
|
+
def between_tools_off?(model_id) # :nodoc:
|
|
30
|
+
Providers::Anthropic.between_tools_off?(model_id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def thinking_off_control(model_id) # :nodoc:
|
|
34
|
+
{ enabled: false } if between_tools_off?(model_id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cache_write_input_multiplier(model_id, ttl) # :nodoc:
|
|
38
|
+
Providers::Anthropic.cache_write_input_multiplier(model_id, ttl) if model_id.to_s.start_with?('claude')
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
def models_dev_alias(...)
|
|
32
42
|
VertexAI::Models.models_dev_alias(...)
|
|
33
43
|
end
|
|
@@ -68,11 +78,6 @@ module RubyLLM
|
|
|
68
78
|
"#{location_path}/publishers/#{publisher}/models/#{model}"
|
|
69
79
|
end
|
|
70
80
|
|
|
71
|
-
def initialize(config)
|
|
72
|
-
super
|
|
73
|
-
@authorizer = nil
|
|
74
|
-
end
|
|
75
|
-
|
|
76
81
|
def batch_protocol
|
|
77
82
|
batch_protocol_for_name(:gemini)
|
|
78
83
|
end
|
|
@@ -111,6 +116,13 @@ module RubyLLM
|
|
|
111
116
|
api_base_for(@config.vertexai_location)
|
|
112
117
|
end
|
|
113
118
|
|
|
119
|
+
# Uploads are objects in the configured bucket, readable with any
|
|
120
|
+
# credentials the bucket admits.
|
|
121
|
+
def account_identity
|
|
122
|
+
bucket = @config.vertexai_batch_gcs_uri
|
|
123
|
+
account_digest(bucket) if bucket
|
|
124
|
+
end
|
|
125
|
+
|
|
114
126
|
def api_base_for(location)
|
|
115
127
|
return @config.vertexai_api_base if @config.vertexai_api_base
|
|
116
128
|
|
|
@@ -128,23 +140,26 @@ module RubyLLM
|
|
|
128
140
|
|
|
129
141
|
def ranking_connection # :nodoc:
|
|
130
142
|
base = @config.vertexai_ranking_api_base || 'https://discoveryengine.googleapis.com/v1'
|
|
131
|
-
@ranking_connection ||= Transport::Connection.new(
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
@ranking_connection ||= Transport::Connection.new(
|
|
144
|
+
self, @config, api_base: base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
|
|
145
|
+
)
|
|
134
146
|
end
|
|
135
147
|
|
|
136
148
|
# The rescue can't name Google::Auth::AuthorizationError directly:
|
|
137
149
|
# when googleauth is missing, evaluating the constant would replace
|
|
138
150
|
# the helpful install error with a NameError.
|
|
139
151
|
def headers
|
|
140
|
-
|
|
141
|
-
@authorizer.apply({})
|
|
152
|
+
Credentials.for(@config).headers
|
|
142
153
|
rescue StandardError => e
|
|
143
154
|
raise unless defined?(Google::Auth::AuthorizationError) && e.is_a?(Google::Auth::AuthorizationError)
|
|
144
155
|
|
|
145
156
|
raise UnauthorizedError, "Invalid Google Cloud credentials for Vertex AI: #{e.message}"
|
|
146
157
|
end
|
|
147
158
|
|
|
159
|
+
def google_credentials # :nodoc:
|
|
160
|
+
Credentials.for(@config).authorizer
|
|
161
|
+
end
|
|
162
|
+
|
|
148
163
|
class << self
|
|
149
164
|
def configuration_options
|
|
150
165
|
%i[
|
|
@@ -172,22 +187,6 @@ module RubyLLM
|
|
|
172
187
|
end
|
|
173
188
|
end
|
|
174
189
|
|
|
175
|
-
def initialize_authorizer
|
|
176
|
-
require 'googleauth'
|
|
177
|
-
@authorizer =
|
|
178
|
-
if @config.vertexai_service_account_key
|
|
179
|
-
::Google::Auth::ServiceAccountCredentials.make_creds(
|
|
180
|
-
json_key_io: StringIO.new(@config.vertexai_service_account_key),
|
|
181
|
-
scope: SCOPES
|
|
182
|
-
)
|
|
183
|
-
else
|
|
184
|
-
::Google::Auth.get_application_default(SCOPES)
|
|
185
|
-
end
|
|
186
|
-
rescue LoadError
|
|
187
|
-
raise Error,
|
|
188
|
-
'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
|
189
|
-
end
|
|
190
|
-
|
|
191
190
|
def batch_protocol_name_for(model)
|
|
192
191
|
case model
|
|
193
192
|
when %r{/} then :chat_completions
|
|
@@ -10,6 +10,12 @@ module RubyLLM
|
|
|
10
10
|
role.to_s
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def supported_attachment?(attachment)
|
|
14
|
+
Protocols::ChatCompletions::Media.supported_attachment?(
|
|
15
|
+
attachment, document_attachments: :none, audio_attachments: false
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def format_content(content, attachments = [])
|
|
14
20
|
Protocols::ChatCompletions::Media.format_content(
|
|
15
21
|
content,
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class XAI
|
|
6
|
-
# Image generation and editing for the xAI API.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# and has no mask support.
|
|
6
|
+
# Image generation and editing for the xAI API. Editing takes JSON
|
|
7
|
+
# image references (URL, data URI, or file id) instead of multipart
|
|
8
|
+
# uploads, up to three per request, and has no mask support.
|
|
10
9
|
module Images
|
|
11
10
|
module_function
|
|
12
11
|
|
|
@@ -15,21 +14,21 @@ module RubyLLM
|
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def render_image_payload(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
|
|
18
|
-
return render_edit_payload(prompt, model:, count:, with:, provider_options:) if editing?(with, mask)
|
|
17
|
+
return render_edit_payload(prompt, model:, size:, count:, with:, provider_options:) if editing?(with, mask)
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
payload = { model: model, prompt: prompt }
|
|
19
|
+
payload = { model: model, prompt: prompt, size: size }.compact
|
|
22
20
|
payload[:n] = count if count
|
|
23
21
|
|
|
24
22
|
payload.merge(provider_options)
|
|
25
23
|
end
|
|
26
24
|
|
|
27
|
-
def render_edit_payload(prompt, model:, with:, provider_options:, count: nil)
|
|
25
|
+
def render_edit_payload(prompt, model:, with:, provider_options:, size: nil, count: nil)
|
|
28
26
|
payload = {
|
|
29
27
|
model: model,
|
|
30
28
|
prompt: prompt,
|
|
31
|
-
images: image_references(with)
|
|
32
|
-
|
|
29
|
+
images: image_references(with),
|
|
30
|
+
size: size
|
|
31
|
+
}.compact
|
|
33
32
|
payload[:n] = count if count
|
|
34
33
|
|
|
35
34
|
payload.merge(provider_options)
|
|
@@ -27,24 +27,26 @@ module RubyLLM
|
|
|
27
27
|
mcp: Protocols::Responses::SERVER_TOOL_ALIASES.fetch(:mcp)
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
SERVER_TOOL_USAGE_NAMES = { 'code_interpreter' => 'code_execution' }.freeze
|
|
31
31
|
|
|
32
32
|
def server_tool_aliases
|
|
33
33
|
SERVER_TOOL_ALIASES
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def parse_usage(usage)
|
|
37
|
-
super.merge(
|
|
38
|
-
server_tool_use: server_side_tool_usage(usage),
|
|
39
|
-
reported_cost: reported_cost(usage)
|
|
40
|
-
)
|
|
37
|
+
super.merge(reported_cost: reported_cost(usage))
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
private
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
# Each *_calls detail counts one tool; the totals and the X Search
|
|
43
|
+
# fetch counts beside them do not.
|
|
44
|
+
def parse_server_tool_use(response)
|
|
45
|
+
details = response.dig('usage', 'server_side_tool_usage_details').to_h
|
|
46
|
+
details.filter_map do |counter, count|
|
|
47
|
+
tool = counter[/\A(.+)_calls\z/, 1]
|
|
48
|
+
["#{SERVER_TOOL_USAGE_NAMES.fetch(tool, tool)}_requests", count] if tool
|
|
49
|
+
end.to_h
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
end
|
|
@@ -5,6 +5,8 @@ module RubyLLM
|
|
|
5
5
|
class XAI
|
|
6
6
|
# Grok Imagine generation, editing, and extension jobs.
|
|
7
7
|
module Videos
|
|
8
|
+
include XAI::ReportedCost
|
|
9
|
+
|
|
8
10
|
def video_url
|
|
9
11
|
'videos/generations'
|
|
10
12
|
end
|
|
@@ -32,11 +34,11 @@ module RubyLLM
|
|
|
32
34
|
{ model: model, prompt: prompt, video: video_reference(video) }.merge(provider_options)
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
def parse_video_job(response, model
|
|
37
|
+
def parse_video_job(response, model:, **request)
|
|
36
38
|
id = response.body['request_id']
|
|
37
39
|
raise Error.new('xAI did not return a video request id', response:) unless id
|
|
38
40
|
|
|
39
|
-
VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
|
|
41
|
+
VideoJob.new(id: id, protocol: self, model: model, raw: response.body, **request)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def video_job_url(job)
|
|
@@ -45,11 +47,12 @@ module RubyLLM
|
|
|
45
47
|
|
|
46
48
|
def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
|
|
47
49
|
body = response.body
|
|
48
|
-
case body['status']
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
state = case body['status']
|
|
51
|
+
when 'done' then { status: :completed }
|
|
52
|
+
when 'failed', 'expired' then { status: :failed, error: body['error'] || body['status'] }
|
|
53
|
+
else { status: :pending }
|
|
54
|
+
end
|
|
55
|
+
state.merge({ raw: body, reported_cost: reported_cost(body['usage'] || {}) }.compact)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def download_video(job)
|
|
@@ -77,9 +80,8 @@ module RubyLLM
|
|
|
77
80
|
|
|
78
81
|
def video_reference(attachment)
|
|
79
82
|
return { file_id: attachment.provider_file_id } if attachment.provider_file?
|
|
80
|
-
return { url: attachment.source.to_s } if attachment.url?
|
|
81
83
|
|
|
82
|
-
{ url: attachment.
|
|
84
|
+
{ url: attachment.url_or_data_uri }
|
|
83
85
|
end
|
|
84
86
|
end
|
|
85
87
|
end
|
data/lib/ruby_llm/railtie.rb
CHANGED
|
@@ -15,16 +15,27 @@ if defined?(Rails::Railtie)
|
|
|
15
15
|
|
|
16
16
|
initializer 'ruby_llm.active_record' do
|
|
17
17
|
ActiveSupport.on_load :active_record do
|
|
18
|
+
require 'ruby_llm/active_record/record'
|
|
18
19
|
require 'ruby_llm/active_record/payload_helpers'
|
|
19
20
|
require 'ruby_llm/active_record/model'
|
|
20
21
|
require 'ruby_llm/active_record/tool_call'
|
|
21
22
|
require 'ruby_llm/active_record/usage'
|
|
22
23
|
require 'ruby_llm/active_record/batch'
|
|
24
|
+
require 'ruby_llm/active_record/mcp_credential'
|
|
25
|
+
require 'ruby_llm/active_record/provider_file'
|
|
23
26
|
require 'ruby_llm/active_record/chat_methods'
|
|
24
27
|
require 'ruby_llm/active_record/message_methods'
|
|
25
28
|
|
|
26
29
|
require 'ruby_llm/active_record/acts_as'
|
|
27
30
|
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
|
|
31
|
+
RubyLLM.config.mcp_credential_store ||= RubyLLM::ActiveRecord::MCPCredential
|
|
32
|
+
RubyLLM::Accounting::Usage.ledger ||= RubyLLM::ActiveRecord::Usage
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
initializer 'ruby_llm.active_storage' do
|
|
37
|
+
ActiveSupport.on_load :active_storage_blob do
|
|
38
|
+
after_destroy { RubyLLM::ActiveRecord::ProviderFile.forget_blob(self) }
|
|
28
39
|
end
|
|
29
40
|
end
|
|
30
41
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class RequestShape
|
|
5
|
+
# A Part describes one piece of a turn by its kind and size, never by
|
|
6
|
+
# its content.
|
|
7
|
+
#
|
|
8
|
+
# part.kind # => :tool_call
|
|
9
|
+
# part.name # => "find_tasks"
|
|
10
|
+
# part.size # => 42
|
|
11
|
+
# part.unit # => :chars
|
|
12
|
+
# part.signed? # => true
|
|
13
|
+
# part.to_s # => "call find_tasks (args 42 chars), signed"
|
|
14
|
+
#
|
|
15
|
+
class Part
|
|
16
|
+
include Support::Inspectable
|
|
17
|
+
|
|
18
|
+
TEXT_KINDS = %i[text thinking].freeze
|
|
19
|
+
private_constant :TEXT_KINDS
|
|
20
|
+
|
|
21
|
+
# The kind of piece: +:text+, +:thinking+, +:image+, +:audio+,
|
|
22
|
+
# +:video+, +:document+, +:tool_call+, +:tool_result+, or +:other+
|
|
23
|
+
# for a piece RubyLLM does not classify, such as a provider tool step.
|
|
24
|
+
attr_reader :kind
|
|
25
|
+
|
|
26
|
+
# The size of the piece as the request sends it, in #unit: the
|
|
27
|
+
# characters of text, thinking, tool call arguments, and a tool
|
|
28
|
+
# result's text, or of its JSON when it is structured data, and the
|
|
29
|
+
# bytes of inline media. Media inside a tool result follows it as
|
|
30
|
+
# parts of its own. Returns +nil+ when there is nothing to measure,
|
|
31
|
+
# such as media sent by URL or thinking the provider encrypted.
|
|
32
|
+
attr_reader :size
|
|
33
|
+
|
|
34
|
+
# The unit of #size: +:chars+ or +:bytes+, or +nil+ without a size.
|
|
35
|
+
attr_reader :unit
|
|
36
|
+
|
|
37
|
+
# The tool's name for a tool call, and for a tool result the name of
|
|
38
|
+
# the call it answers, which is +nil+ when no call in the request
|
|
39
|
+
# matches it. For an +:other+ piece, the provider's name for it.
|
|
40
|
+
attr_reader :name
|
|
41
|
+
|
|
42
|
+
# Where media comes from: +:inline+ for bytes in the request, +:url+
|
|
43
|
+
# for a link the provider fetches, or +:file+ for a file stored with
|
|
44
|
+
# the provider. Returns +nil+ for pieces that are not media.
|
|
45
|
+
attr_reader :source
|
|
46
|
+
|
|
47
|
+
# The MIME type of media, as the request gives it or as its format
|
|
48
|
+
# implies, or +nil+.
|
|
49
|
+
attr_reader :mime_type
|
|
50
|
+
|
|
51
|
+
# Creates a part. Every attribute but +kind+ is optional. A +size+
|
|
52
|
+
# counts characters unless +unit+ says +:bytes+; pass +signed: true+
|
|
53
|
+
# when the piece carries a thinking signature.
|
|
54
|
+
def initialize(kind:, size: nil, unit: nil, name: nil, source: nil, mime_type: nil, signed: false)
|
|
55
|
+
@kind = kind
|
|
56
|
+
@size = size
|
|
57
|
+
@unit = size && (unit || :chars)
|
|
58
|
+
@name = name
|
|
59
|
+
@source = source
|
|
60
|
+
@mime_type = mime_type
|
|
61
|
+
@signed = signed
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns whether the piece carries a thinking signature, the token a
|
|
65
|
+
# provider issues to verify replayed thinking or tool calls.
|
|
66
|
+
def signed?
|
|
67
|
+
@signed
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns the part in words, with its size and unit:
|
|
71
|
+
#
|
|
72
|
+
# text (13 chars)
|
|
73
|
+
# image/png (34512 bytes)
|
|
74
|
+
# document (url)
|
|
75
|
+
# result find_tasks (6859 chars)
|
|
76
|
+
# thinking (no text), signed
|
|
77
|
+
#
|
|
78
|
+
def to_s
|
|
79
|
+
signed? ? "#{description}, signed" : description
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns the part as a Hash, with its size under its unit, omitting
|
|
83
|
+
# attributes it does not have.
|
|
84
|
+
def to_h
|
|
85
|
+
hash = { kind: kind, name: name }
|
|
86
|
+
hash[unit] = size if size
|
|
87
|
+
hash.merge(source: source, mime_type: mime_type, signed: signed? || nil).compact
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def inspect_attributes # :nodoc:
|
|
91
|
+
to_h
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def description
|
|
97
|
+
case kind
|
|
98
|
+
when :tool_call then ['call', name, size && "(args #{measured})"].compact.join(' ')
|
|
99
|
+
when :tool_result then ['result', name, size && "(#{measured})"].compact.join(' ')
|
|
100
|
+
when :other then name || 'part of no known kind'
|
|
101
|
+
else "#{mime_type || kind} (#{size ? measured : missing})"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def measured
|
|
106
|
+
noun = { chars: 'char', bytes: 'byte' }.fetch(unit, unit.to_s)
|
|
107
|
+
"#{size} #{noun}#{'s' unless size == 1}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def missing
|
|
111
|
+
return source.to_s if source && source != :inline
|
|
112
|
+
|
|
113
|
+
TEXT_KINDS.include?(kind) ? 'no text' : 'no data'
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class RequestShape
|
|
5
|
+
# A Problem is something in a request that providers are known to
|
|
6
|
+
# reject, found by the protocol that rendered it: a part with no data, a
|
|
7
|
+
# tool round whose calls and results do not pair up, a step that lacks
|
|
8
|
+
# the signature the provider requires, or roles out of order.
|
|
9
|
+
#
|
|
10
|
+
# problem.kind # => :part_without_data
|
|
11
|
+
# problem.turn # => 3
|
|
12
|
+
# problem.part # => 0
|
|
13
|
+
# problem.to_s # => "problem at #3, part 0: thinking part carries no data"
|
|
14
|
+
#
|
|
15
|
+
class Problem
|
|
16
|
+
include Support::Inspectable
|
|
17
|
+
|
|
18
|
+
# What is wrong: +:part_without_data+, +:empty_turn+,
|
|
19
|
+
# +:unpaired_round+, +:unmatched_result+, +:unsigned_call+,
|
|
20
|
+
# +:unsigned_thinking+, or +:role_order+.
|
|
21
|
+
attr_reader :kind
|
|
22
|
+
|
|
23
|
+
# The index of the turn the problem is in, or +nil+.
|
|
24
|
+
attr_reader :turn
|
|
25
|
+
|
|
26
|
+
# The position of the part in the turn's parts, or +nil+ when the
|
|
27
|
+
# problem concerns the whole turn.
|
|
28
|
+
attr_reader :part
|
|
29
|
+
|
|
30
|
+
# The id of the call an unmatched tool result names, the only id a
|
|
31
|
+
# shape shows, or +nil+.
|
|
32
|
+
attr_reader :call_id
|
|
33
|
+
|
|
34
|
+
# The problem in words.
|
|
35
|
+
attr_reader :message
|
|
36
|
+
|
|
37
|
+
# Creates a problem of +kind+ described by +message+.
|
|
38
|
+
def initialize(kind:, message:, turn: nil, part: nil, call_id: nil)
|
|
39
|
+
@kind = kind
|
|
40
|
+
@message = message
|
|
41
|
+
@turn = turn
|
|
42
|
+
@part = part
|
|
43
|
+
@call_id = call_id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the problem as one line, with where it is.
|
|
47
|
+
def to_s
|
|
48
|
+
location = ["##{turn}", part && "part #{part}"].compact.join(', ') if turn
|
|
49
|
+
line = location ? "problem at #{location}: #{message}" : "problem: #{message}"
|
|
50
|
+
call_id ? "#{line} (call id #{call_id})" : line
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the problem as a Hash, omitting attributes it does not have.
|
|
54
|
+
def to_h
|
|
55
|
+
{ kind: kind, turn: turn, part: part, call_id: call_id, message: message }.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect_attributes # :nodoc:
|
|
59
|
+
to_h
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class RequestShape
|
|
5
|
+
# A ToolRound describes a model turn that called tools and the results
|
|
6
|
+
# the request sends back before the next model turn: how many of each,
|
|
7
|
+
# and whether they pair up the way the provider matches them.
|
|
8
|
+
#
|
|
9
|
+
# round.turn # => 1
|
|
10
|
+
# round.calls # => 2
|
|
11
|
+
# round.results # => 1
|
|
12
|
+
# round.paired? # => false
|
|
13
|
+
# round.to_s # => "tool round at #1: 2 calls, 1 result, not paired"
|
|
14
|
+
#
|
|
15
|
+
class ToolRound
|
|
16
|
+
include Support::Inspectable
|
|
17
|
+
|
|
18
|
+
# The index of the turn that made the calls.
|
|
19
|
+
attr_reader :turn
|
|
20
|
+
|
|
21
|
+
# The number of tool calls in the round.
|
|
22
|
+
attr_reader :calls
|
|
23
|
+
|
|
24
|
+
# The number of tool results sent back for the round.
|
|
25
|
+
attr_reader :results
|
|
26
|
+
|
|
27
|
+
# Creates a round at +turn+ with the number of +calls+ and +results+,
|
|
28
|
+
# and whether they are +paired+.
|
|
29
|
+
def initialize(turn:, calls:, results:, paired:)
|
|
30
|
+
@turn = turn
|
|
31
|
+
@calls = calls
|
|
32
|
+
@results = results
|
|
33
|
+
@paired = paired
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns whether every call has its result, matched the way the
|
|
37
|
+
# provider matches them: by call id, or by position and tool name.
|
|
38
|
+
def paired?
|
|
39
|
+
@paired
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the round in words.
|
|
43
|
+
def to_s
|
|
44
|
+
"tool round at ##{turn}: #{counted(calls, 'call')}, #{counted(results, 'result')}, " \
|
|
45
|
+
"#{paired? ? 'paired' : 'not paired'}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the round as a Hash with its turn, counts, and pairing.
|
|
49
|
+
def to_h
|
|
50
|
+
{ turn: turn, calls: calls, results: results, paired: paired? }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inspect_attributes # :nodoc:
|
|
54
|
+
to_h
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def counted(count, noun)
|
|
60
|
+
"#{count} #{noun}#{'s' unless count == 1}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class RequestShape
|
|
5
|
+
# A Turn describes one entry of a request's conversation: a message, or
|
|
6
|
+
# one item where the provider sends a model's reasoning and tool calls as
|
|
7
|
+
# items of their own.
|
|
8
|
+
#
|
|
9
|
+
# turn = error.request_shape.turns.last
|
|
10
|
+
# turn.index # => 4
|
|
11
|
+
# turn.role # => "user"
|
|
12
|
+
# turn.to_s # => "#4 user: text (31 chars)"
|
|
13
|
+
#
|
|
14
|
+
class Turn
|
|
15
|
+
include Support::Inspectable
|
|
16
|
+
|
|
17
|
+
# The turn's 0-indexed position in the request, the position a
|
|
18
|
+
# provider's error message counts from.
|
|
19
|
+
attr_reader :index
|
|
20
|
+
|
|
21
|
+
# The turn's role as the payload names it, such as <tt>"model"</tt>,
|
|
22
|
+
# or for an entry that names no role, its type, such as
|
|
23
|
+
# <tt>"function_call"</tt>. Returns +nil+ when the entry names neither.
|
|
24
|
+
attr_reader :role
|
|
25
|
+
|
|
26
|
+
# The Part objects of the turn, in order.
|
|
27
|
+
attr_reader :parts
|
|
28
|
+
|
|
29
|
+
# Creates a turn at +index+ with +role+ and +parts+.
|
|
30
|
+
def initialize(index:, role:, parts: [])
|
|
31
|
+
@index = index
|
|
32
|
+
@role = role
|
|
33
|
+
@parts = parts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the turn as one line: its index, its role, and its parts.
|
|
37
|
+
#
|
|
38
|
+
# turn.to_s # => "#3 model: thinking (11 chars), text (13 chars), signed"
|
|
39
|
+
#
|
|
40
|
+
def to_s
|
|
41
|
+
"##{[index, role].compact.join(' ')}: #{parts.empty? ? 'no parts' : parts.join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the turn as a Hash with its index, role, and parts.
|
|
45
|
+
def to_h
|
|
46
|
+
{ index: index, role: role, parts: parts.map(&:to_h) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect_attributes # :nodoc:
|
|
50
|
+
{ index: index, role: role, parts: parts.join(', ') }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|