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
|
@@ -10,6 +10,10 @@ module RubyLLM
|
|
|
10
10
|
module Tools
|
|
11
11
|
MULTIMODAL_FUNCTION_RESPONSE_GENERATION = Gem::Version.new('3')
|
|
12
12
|
|
|
13
|
+
# The signature Google documents for a function call Gemini did not
|
|
14
|
+
# make, such as one from another provider. Vertex AI accepts no other.
|
|
15
|
+
PLACEHOLDER_SIGNATURE = 'skip_thought_signature_validator'
|
|
16
|
+
|
|
13
17
|
def format_tools(tools)
|
|
14
18
|
return [] if tools.empty?
|
|
15
19
|
|
|
@@ -18,14 +22,13 @@ module RubyLLM
|
|
|
18
22
|
}]
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
# Gemini signs only the first of parallel calls, so each call goes back
|
|
26
|
+
# with its own signature or none, never one another part carried.
|
|
27
|
+
def format_tool_call(msg)
|
|
22
28
|
parts = []
|
|
23
29
|
|
|
24
30
|
parts.concat(Media.format_content(msg.content, msg.attachments)) if msg.content && !msg.content.empty?
|
|
25
31
|
|
|
26
|
-
fallback_signature = msg.thinking&.signature
|
|
27
|
-
used_fallback = false
|
|
28
|
-
|
|
29
32
|
msg.tool_calls.each_value do |tool_call|
|
|
30
33
|
part = {
|
|
31
34
|
functionCall: {
|
|
@@ -33,13 +36,7 @@ module RubyLLM
|
|
|
33
36
|
args: tool_call.arguments
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
signature = tool_call.thought_signature
|
|
38
|
-
if signature.nil? && fallback_signature && !used_fallback
|
|
39
|
-
signature = fallback_signature
|
|
40
|
-
used_fallback = true
|
|
41
|
-
end
|
|
42
|
-
part[:thoughtSignature] = signature if signature
|
|
39
|
+
part[:thoughtSignature] = tool_call.thought_signature if tool_call.thought_signature
|
|
43
40
|
parts << part
|
|
44
41
|
end
|
|
45
42
|
|
|
@@ -95,6 +92,15 @@ module RubyLLM
|
|
|
95
92
|
|
|
96
93
|
private
|
|
97
94
|
|
|
95
|
+
# Gemini 3 refuses a step of the current turn whose first function
|
|
96
|
+
# call carries no signature.
|
|
97
|
+
def sign_step(parts)
|
|
98
|
+
first_call = parts.find { |part| part.key?(:functionCall) }
|
|
99
|
+
return parts if first_call.nil? || first_call.key?(:thoughtSignature)
|
|
100
|
+
|
|
101
|
+
parts.map { |part| part.equal?(first_call) ? part.merge(thoughtSignature: PLACEHOLDER_SIGNATURE) : part }
|
|
102
|
+
end
|
|
103
|
+
|
|
98
104
|
# functionResponse.parts only accepts inline bytes, and pre-Gemini 3 models reject it
|
|
99
105
|
def partition_tool_result_attachments(attachments)
|
|
100
106
|
return [[], []] if attachments.empty?
|
|
@@ -75,6 +75,7 @@ module RubyLLM
|
|
|
75
75
|
def parse_transcription_response(response, model:)
|
|
76
76
|
data = response.body
|
|
77
77
|
text = extract_text(data)
|
|
78
|
+
raise_if_transcription_blocked(data, text, response)
|
|
78
79
|
|
|
79
80
|
usage = extract_usage(data)
|
|
80
81
|
|
|
@@ -86,6 +87,18 @@ module RubyLLM
|
|
|
86
87
|
)
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
# Gemini returns no candidates only for a prompt it blocked.
|
|
91
|
+
def raise_if_transcription_blocked(data, text, response)
|
|
92
|
+
return unless text.to_s.empty? && data.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
reason = data.dig('candidates', 0, 'finishReason') || data.dig('promptFeedback', 'blockReason')
|
|
95
|
+
return unless data.dig('candidates', 0).nil? || normalize_finish_reason(reason) == :content_filter
|
|
96
|
+
|
|
97
|
+
usage = extract_usage(data)
|
|
98
|
+
@usage_tracker&.observe_tokens(Tokens.new(input: usage[:input_tokens], output: usage[:output_tokens]))
|
|
99
|
+
raise ContentFilterError.new(['Gemini blocked the transcription', reason].compact.join(': '), response:)
|
|
100
|
+
end
|
|
101
|
+
|
|
89
102
|
def extract_text(data)
|
|
90
103
|
candidate = data.is_a?(Hash) ? data.dig('candidates', 0) : nil
|
|
91
104
|
return unless candidate
|
|
@@ -18,11 +18,16 @@ module RubyLLM
|
|
|
18
18
|
Support::Utils.deep_merge({ instances: [instance] }, provider_options)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def parse_video_job(response, model
|
|
21
|
+
def parse_video_job(response, model:, **request)
|
|
22
22
|
name = response.body['name']
|
|
23
23
|
raise Error.new('Gemini did not return a video generation operation', response:) unless name
|
|
24
24
|
|
|
25
|
-
VideoJob.new(id: name, protocol: self, model: model, raw: response.body)
|
|
25
|
+
VideoJob.new(id: name, protocol: self, model: model, raw: response.body, **request)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def video_request_settings(request)
|
|
29
|
+
parameters = request[:parameters] || {}
|
|
30
|
+
{ duration: parameters[:durationSeconds], resolution: parameters[:resolution] }
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def render_video_extension_payload(prompt, extend:, provider_options: {}, **)
|
|
@@ -10,8 +10,6 @@ module RubyLLM
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def render_video_payload(prompt, model:, with: [], provider_options: {})
|
|
13
|
-
raise ArgumentError, 'vLLM-Omni video generation requires a prompt' if prompt.nil?
|
|
14
|
-
|
|
15
13
|
options = video_options(provider_options)
|
|
16
14
|
payload = { model: model, prompt: prompt }.merge(video_references(with)).merge(options).compact
|
|
17
15
|
payload.transform_values do |value|
|
|
@@ -25,12 +23,16 @@ module RubyLLM
|
|
|
25
23
|
end
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def parse_video_job(response, model
|
|
26
|
+
def parse_video_job(response, model:, **request)
|
|
29
27
|
body = response.body
|
|
30
28
|
id = body['id']
|
|
31
29
|
raise Error.new('GPUStack did not return a video job id', response:) unless id
|
|
32
30
|
|
|
33
|
-
VideoJob.new(id: id, protocol: self, model: body['model'] || model, **video_job_state(body))
|
|
31
|
+
VideoJob.new(id: id, protocol: self, model: body['model'] || model, **video_job_state(body), **request)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def video_request_settings(request)
|
|
35
|
+
{ duration: request[:seconds], resolution: request[:size] }
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def video_job_url(job)
|
|
@@ -60,7 +62,7 @@ module RubyLLM
|
|
|
60
62
|
|
|
61
63
|
def video_references(attachments)
|
|
62
64
|
attachments.group_by(&:type).to_h do |type, group|
|
|
63
|
-
values = group.map { |attachment| { "#{type}_url" =>
|
|
65
|
+
values = group.map { |attachment| { "#{type}_url" => attachment.for_llm } }
|
|
64
66
|
[:"#{type}_reference", values.one? ? values.first : values]
|
|
65
67
|
end
|
|
66
68
|
end
|
|
@@ -86,10 +88,6 @@ module RubyLLM
|
|
|
86
88
|
raise UnsupportedAttachmentError, attachment.mime_type
|
|
87
89
|
end
|
|
88
90
|
end
|
|
89
|
-
|
|
90
|
-
def video_reference(attachment)
|
|
91
|
-
attachment.url? ? attachment.source.to_s : attachment.for_llm
|
|
92
|
-
end
|
|
93
91
|
end
|
|
94
92
|
end
|
|
95
93
|
end
|
|
@@ -5,6 +5,7 @@ module RubyLLM
|
|
|
5
5
|
class Interactions
|
|
6
6
|
module Chat # :nodoc:
|
|
7
7
|
FINISH_REASONS = { 'completed' => :stop, 'requires_action' => :tool_calls, 'incomplete' => :max_tokens }.freeze
|
|
8
|
+
SERVER_TOOL_USAGE_NAMES = { 'google_search' => 'web_search' }.freeze
|
|
8
9
|
|
|
9
10
|
module_function
|
|
10
11
|
|
|
@@ -34,9 +35,6 @@ module RubyLLM
|
|
|
34
35
|
raise ArgumentError, 'Gemini Interactions does not expose a thinking-off control'
|
|
35
36
|
end
|
|
36
37
|
raise ArgumentError, 'Gemini Interactions accepts thinking effort, not a token budget' if thinking.budget
|
|
37
|
-
if thinking.effort && !%i[minimal low medium high].include?(thinking.effort)
|
|
38
|
-
raise ArgumentError, 'Gemini Interactions thinking effort must be minimal, low, medium, or high'
|
|
39
|
-
end
|
|
40
38
|
|
|
41
39
|
{ thinking_level: thinking.effort&.to_s,
|
|
42
40
|
thinking_summaries: render_interaction_summaries(thinking.display) }.compact
|
|
@@ -50,20 +48,25 @@ module RubyLLM
|
|
|
50
48
|
display == :omitted ? 'none' : 'auto'
|
|
51
49
|
end
|
|
52
50
|
|
|
51
|
+
# Gemini checks signatures only in the current turn, which starts at
|
|
52
|
+
# the last user message.
|
|
53
53
|
def format_interaction_input(messages)
|
|
54
54
|
calls = messages.flat_map { |message| message.tool_calls.to_h.values }.to_h { |call| [call.id, call] }
|
|
55
|
-
messages.
|
|
56
|
-
|
|
55
|
+
turn_start = messages.rindex { |message| message.role == :user }
|
|
56
|
+
messages.each_with_index.flat_map do |message, index|
|
|
57
|
+
next [] if message.role == :system
|
|
58
|
+
|
|
59
|
+
render_interaction_turn(message, calls, current: turn_start.nil? || index > turn_start)
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
def render_interaction_turn(message, calls)
|
|
63
|
+
def render_interaction_turn(message, calls, current:)
|
|
61
64
|
if interaction_state?(message.raw_content)
|
|
62
65
|
render_interaction_history(message.raw_content.dig('response', 'steps') || [])
|
|
63
66
|
elsif message.tool_result?
|
|
64
67
|
[render_interaction_result(message, calls)]
|
|
65
68
|
else
|
|
66
|
-
render_interaction_message(message)
|
|
69
|
+
render_interaction_message(message, current:)
|
|
67
70
|
end
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -73,19 +76,24 @@ module RubyLLM
|
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def render_interaction_history(steps)
|
|
79
|
+
answered = interaction_answered_calls(steps)
|
|
76
80
|
steps.map do |step|
|
|
77
|
-
step['type'].to_s.start_with?('mcp_server_')
|
|
81
|
+
if step['type'].to_s.start_with?('mcp_server_') || answered.include?(step['id'] || step['call_id'])
|
|
82
|
+
step.except('signature')
|
|
83
|
+
else
|
|
84
|
+
step
|
|
85
|
+
end
|
|
78
86
|
end
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
def render_interaction_message(message)
|
|
89
|
+
def render_interaction_message(message, current:)
|
|
82
90
|
steps = []
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
content = render_interaction_content(message.content, message.attachments)
|
|
92
|
+
steps << { type: message.role == :assistant ? 'model_output' : 'user_input', content: } if content.any?
|
|
93
|
+
message.tool_calls.to_h.values.each_with_index do |call, position|
|
|
94
|
+
signature = call.thought_signature || (Gemini::Tools::PLACEHOLDER_SIGNATURE if current && position.zero?)
|
|
95
|
+
steps << { type: 'function_call', id: call.id, name: call.name, arguments: call.arguments,
|
|
96
|
+
signature: }.compact
|
|
89
97
|
end
|
|
90
98
|
steps
|
|
91
99
|
end
|
|
@@ -104,19 +112,39 @@ module RubyLLM
|
|
|
104
112
|
steps = data.fetch('steps', [])
|
|
105
113
|
content = parse_interaction_content(steps)
|
|
106
114
|
calls = parse_interaction_calls(steps)
|
|
107
|
-
|
|
115
|
+
status = interaction_status(data, calls, steps)
|
|
116
|
+
if status == 'requires_action' && calls.empty?
|
|
108
117
|
raise Error.new('Gemini interaction requires an unsupported action', response: raw)
|
|
109
118
|
end
|
|
110
119
|
|
|
111
120
|
Message.new(role: :assistant, content: content[:text], attachments: content[:attachments],
|
|
112
121
|
citations: content[:citations], thinking: parse_interaction_thinking(steps),
|
|
113
122
|
tool_calls: calls, server_tool_calls: parse_interaction_server_calls(steps),
|
|
114
|
-
raw_content:
|
|
123
|
+
raw_content: kept_interaction(data, steps),
|
|
115
124
|
model: model, raw: raw, cost: cost,
|
|
116
|
-
finish_reason: interaction_finish_reason(
|
|
125
|
+
finish_reason: interaction_finish_reason(status, calls),
|
|
126
|
+
server_tool_use: parse_interaction_server_tool_use(data['usage'] || {}),
|
|
117
127
|
**parse_interaction_usage(data['usage'] || {}))
|
|
118
128
|
end
|
|
119
129
|
|
|
130
|
+
def interaction_status(data, calls, steps)
|
|
131
|
+
return data['status'] unless data['status'] == 'requires_action' && calls.empty?
|
|
132
|
+
return data['status'] if interaction_answered_calls(steps).empty?
|
|
133
|
+
|
|
134
|
+
steps.any? { |step| step['type'] == 'model_output' } ? 'completed' : data['status']
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_interaction_server_tool_use(usage)
|
|
138
|
+
Array(usage['grounding_tool_count']).to_h do |grounding|
|
|
139
|
+
tool = grounding['type']
|
|
140
|
+
["#{SERVER_TOOL_USAGE_NAMES.fetch(tool, tool)}_requests", grounding['count']]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def kept_interaction(data, steps)
|
|
145
|
+
{ 'response' => data.merge('steps' => steps.map { |step| without_search_suggestions(step) }) }
|
|
146
|
+
end
|
|
147
|
+
|
|
120
148
|
def interaction_finish_reason(status, calls)
|
|
121
149
|
return :tool_calls unless calls.empty?
|
|
122
150
|
|
|
@@ -11,15 +11,22 @@ module RubyLLM
|
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
13
|
def render_interaction_content(text, attachments)
|
|
14
|
-
parts = text.
|
|
14
|
+
parts = text.to_s.empty? ? [] : [{ type: 'text', text: text.to_s }]
|
|
15
15
|
parts + attachments.map { |attachment| render_interaction_attachment(attachment) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def supported_attachment?(attachment)
|
|
19
|
+
attachment.text? || !interaction_attachment_type(attachment).nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
def render_interaction_attachment(attachment)
|
|
23
|
+
unless supported_attachment?(attachment)
|
|
24
|
+
raise ArgumentError, "Gemini Interactions does not support #{attachment.mime_type} input"
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
return { type: 'text', text: attachment.content } if attachment.text?
|
|
20
28
|
|
|
21
29
|
type = interaction_attachment_type(attachment)
|
|
22
|
-
raise ArgumentError, "Gemini Interactions does not support #{attachment.mime_type} input" unless type
|
|
23
30
|
|
|
24
31
|
part = { type: type, mime_type: attachment.mime_type }
|
|
25
32
|
if attachment.provider_file?
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Interactions
|
|
6
|
+
# Describes Interactions requests as RequestShape objects. Each input
|
|
7
|
+
# step is a turn. A function result answers its call by id, and like
|
|
8
|
+
# Gemini, the first call of each step in the current turn needs a
|
|
9
|
+
# signature.
|
|
10
|
+
module RequestShapes
|
|
11
|
+
SPEAKERS = { 'user_input' => :user, 'function_result' => :tool }.freeze
|
|
12
|
+
MEDIA_KINDS = { 'image' => :image, 'audio' => :audio, 'video' => :video, 'document' => :document }.freeze
|
|
13
|
+
|
|
14
|
+
def parse_request_shape(payload)
|
|
15
|
+
input = payload['input']
|
|
16
|
+
return unless input.is_a?(Array)
|
|
17
|
+
|
|
18
|
+
turns = input.each_with_index.map do |entry, index|
|
|
19
|
+
entry = shape_hash(entry)
|
|
20
|
+
shape_turn(index, entry['type'], SPEAKERS.fetch(entry['type'], :model), shape_interaction_entry(entry))
|
|
21
|
+
end
|
|
22
|
+
shape_request(turns, payload:, rules: %i[signed_steps],
|
|
23
|
+
instructions: shape_instructions(payload['system_instruction']),
|
|
24
|
+
tool_names: shape_list(payload['tools']).map { |tool| shape_tool_name(tool) },
|
|
25
|
+
thinking_settings: shape_settings(payload['generation_config'], 'thinking_level',
|
|
26
|
+
'thinking_summaries'))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def shape_interaction_entry(entry)
|
|
32
|
+
signed = shape_signature?(entry['signature'])
|
|
33
|
+
case entry['type']
|
|
34
|
+
when 'user_input', 'model_output' then shape_interaction_content(entry['content'])
|
|
35
|
+
when 'function_call' then [shape_tool_call(entry['name'], entry['arguments'], id: entry['id'], signed:)]
|
|
36
|
+
when 'function_result'
|
|
37
|
+
shape_tool_result(entry['result'], name: entry['name'], id: entry['call_id']) do |part|
|
|
38
|
+
shape_interaction_part(part)
|
|
39
|
+
end
|
|
40
|
+
when 'thought' then [shape_summary(entry['summary'], signed:)]
|
|
41
|
+
else [shape_other(entry['type'], signed:)]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def shape_interaction_content(content)
|
|
46
|
+
return [shape_text(content)] if content.is_a?(String)
|
|
47
|
+
|
|
48
|
+
shape_list(content).map { |part| shape_interaction_part(shape_hash(part)) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def shape_interaction_part(part)
|
|
52
|
+
kind = MEDIA_KINDS[part['type']]
|
|
53
|
+
return shape_text(part['text']) if part['type'] == 'text'
|
|
54
|
+
return shape_other(part['type']) unless kind
|
|
55
|
+
return shape_inline(part['data'], kind:, mime_type: part['mime_type']) if part.key?('data')
|
|
56
|
+
|
|
57
|
+
shape_url(part['uri'], kind:, mime_type: part['mime_type'])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -4,6 +4,13 @@ module RubyLLM
|
|
|
4
4
|
module Protocols
|
|
5
5
|
class Interactions
|
|
6
6
|
module Streaming # :nodoc:
|
|
7
|
+
ERROR_STATUSES = {
|
|
8
|
+
'invalid_request' => 400, 'failed_precondition' => 400, 'authentication' => 401,
|
|
9
|
+
'payment_required' => 402, 'permission_denied' => 403, 'not_found' => 404,
|
|
10
|
+
'rate_limit_exceeded' => 429, 'quota_exceeded' => 429, 'too_many_requests' => 429,
|
|
11
|
+
'api_error' => 500, 'service_unavailable' => 503, 'deadline_exceeded' => 504
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
7
14
|
module_function
|
|
8
15
|
|
|
9
16
|
def stream_response(payload, additional_headers = {})
|
|
@@ -78,6 +85,14 @@ module RubyLLM
|
|
|
78
85
|
@interaction_response.merge('steps' => steps)
|
|
79
86
|
end
|
|
80
87
|
|
|
88
|
+
def parse_streaming_error(data)
|
|
89
|
+
error = JSON.parse(data)
|
|
90
|
+
error = error['error'] if error.is_a?(Hash)
|
|
91
|
+
return super unless error.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
[ERROR_STATUSES.fetch(error['code'], 500), error['message']]
|
|
94
|
+
end
|
|
95
|
+
|
|
81
96
|
def final_interaction_chunk
|
|
82
97
|
message = parse_completion_body(streamed_interaction, raw: nil)
|
|
83
98
|
Chunk.new(role: :assistant, content: nil, model: message.model, tokens: message.tokens,
|
|
@@ -23,13 +23,20 @@ module RubyLLM
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def parse_interaction_calls(steps)
|
|
26
|
-
|
|
26
|
+
answered = interaction_answered_calls(steps)
|
|
27
|
+
steps.select { |step| step['type'] == 'function_call' && !answered.include?(step['id']) }.to_h do |step|
|
|
27
28
|
[step.fetch('id'), ToolCall.new(id: step.fetch('id'), name: step.fetch('name'),
|
|
28
29
|
arguments: parse_interaction_arguments(step['arguments']),
|
|
29
30
|
thought_signature: step['signature'])]
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def interaction_answered_calls(steps)
|
|
35
|
+
calls = steps.filter_map { |step| step['id'] if step['type'] == 'function_call' }
|
|
36
|
+
results = steps.filter_map { |step| step['call_id'] if step['type'] == 'function_result' }
|
|
37
|
+
calls & results
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
def parse_interaction_arguments(arguments)
|
|
34
41
|
return {} if arguments.nil? || (arguments.is_a?(String) && arguments.empty?)
|
|
35
42
|
|
|
@@ -39,14 +46,31 @@ module RubyLLM
|
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def parse_interaction_server_calls(steps)
|
|
49
|
+
answered = interaction_answered_calls(steps)
|
|
42
50
|
steps.filter_map do |step|
|
|
43
51
|
type = step['type'].to_s
|
|
44
|
-
next unless type.end_with?('_call', '_result')
|
|
52
|
+
next unless type.end_with?('_call', '_result')
|
|
53
|
+
next if type.start_with?('function_') && !answered.include?(step['id'] || step['call_id'])
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
kept = without_search_suggestions(step)
|
|
56
|
+
ServerToolCall.new(type: type, id: kept['id'] || kept['call_id'], name: kept['name'],
|
|
57
|
+
input: kept['arguments'], result: kept['result'], raw: kept,
|
|
58
|
+
search_suggestions: search_suggestions(step))
|
|
48
59
|
end
|
|
49
60
|
end
|
|
61
|
+
|
|
62
|
+
# Google's terms forbid storing search suggestions, and a replayed
|
|
63
|
+
# google_search_result keeps the shape it needs without them.
|
|
64
|
+
def without_search_suggestions(step)
|
|
65
|
+
return step unless step['type'] == 'google_search_result'
|
|
66
|
+
|
|
67
|
+
step.merge('result' => Array(step['result']).map { |item| item.except('search_suggestions') })
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def search_suggestions(step)
|
|
71
|
+
suggestions = Array(step['result']).filter_map { |item| item['search_suggestions'] if item.is_a?(Hash) }
|
|
72
|
+
suggestions.join unless suggestions.empty?
|
|
73
|
+
end
|
|
50
74
|
end
|
|
51
75
|
end
|
|
52
76
|
end
|
|
@@ -24,17 +24,7 @@ module RubyLLM
|
|
|
24
24
|
payload = { model:, store: false,
|
|
25
25
|
input: [{ type: 'audio', mime_type: attachment.mime_type, data: attachment.encoded }],
|
|
26
26
|
generation_config: { transcription_config: config } }
|
|
27
|
-
|
|
28
|
-
validate_transcription_config(payload.dig(:generation_config, :transcription_config))
|
|
29
|
-
payload
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def validate_transcription_config(config)
|
|
33
|
-
mode = config[:mode]
|
|
34
|
-
return unless config[:custom_vocabulary] && mode.is_a?(Hash)
|
|
35
|
-
return unless mode[:diarization_mode] || mode[:timestamp_granularities]
|
|
36
|
-
|
|
37
|
-
raise ArgumentError, 'Gemini custom vocabulary cannot be combined with diarization or word timestamps'
|
|
27
|
+
Support::Utils.deep_merge(payload, provider_options)
|
|
38
28
|
end
|
|
39
29
|
|
|
40
30
|
def parse_transcription_response(response, model:)
|
|
@@ -18,18 +18,13 @@ module RubyLLM
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
def render_embedding_payload(text,
|
|
21
|
+
def render_embedding_payload(text, dimensions:, provider_options:, task_type: nil, **)
|
|
22
22
|
payload = {
|
|
23
23
|
input_type: task_type || 'search_document'
|
|
24
24
|
}
|
|
25
25
|
texts = [text].flatten.compact.map(&:to_s).reject(&:empty?)
|
|
26
26
|
payload[:texts] = texts unless texts.empty?
|
|
27
|
-
|
|
28
|
-
if dimensions
|
|
29
|
-
raise Error, "#{model} does not support custom dimensions" unless cohere_v4?(model)
|
|
30
|
-
|
|
31
|
-
payload[:output_dimension] = dimensions
|
|
32
|
-
end
|
|
27
|
+
payload[:output_dimension] = dimensions if dimensions
|
|
33
28
|
|
|
34
29
|
deep_merge_provider_options(payload, provider_options)
|
|
35
30
|
end
|
|
@@ -41,10 +36,6 @@ module RubyLLM
|
|
|
41
36
|
|
|
42
37
|
Embedding.new(vectors:, model:)
|
|
43
38
|
end
|
|
44
|
-
|
|
45
|
-
def cohere_v4?(model)
|
|
46
|
-
model.to_s.include?('cohere.embed-v4')
|
|
47
|
-
end
|
|
48
39
|
end
|
|
49
40
|
end
|
|
50
41
|
end
|
|
@@ -17,8 +17,8 @@ module RubyLLM
|
|
|
17
17
|
requests.each do |request|
|
|
18
18
|
values = request.fetch(:text)
|
|
19
19
|
values = [values] unless values.is_a?(Array)
|
|
20
|
-
unless values.any? && values.all?
|
|
21
|
-
raise ArgumentError, 'Bedrock embedding batches require nonempty text strings'
|
|
20
|
+
unless values.any? && values.all?(String)
|
|
21
|
+
raise ArgumentError, 'Bedrock embedding batches require a nonempty list of text strings'
|
|
22
22
|
end
|
|
23
23
|
unless request.fetch(:payload).key?(:inputText)
|
|
24
24
|
raise ArgumentError, 'Bedrock embedding batches require Titan embedding requests'
|
|
@@ -11,7 +11,6 @@ module RubyLLM
|
|
|
11
11
|
INPAINT_MODEL = 'stability.stable-image-inpaint-v1:0'
|
|
12
12
|
MODELS = (GENERATION_MODELS + [INPAINT_MODEL]).freeze
|
|
13
13
|
ASPECT_RATIOS = %w[16:9 1:1 21:9 2:3 3:2 4:5 5:4 9:16 9:21].freeze
|
|
14
|
-
IMAGE_TYPES = %w[image/jpeg image/png image/webp].freeze
|
|
15
14
|
|
|
16
15
|
def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
|
|
17
16
|
track_usage(:image) do
|
|
@@ -39,10 +38,7 @@ module RubyLLM
|
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
images.map do |data|
|
|
42
|
-
mime_type
|
|
43
|
-
raise Error.new('Bedrock returned invalid image data', response:) unless IMAGE_TYPES.include?(mime_type)
|
|
44
|
-
|
|
45
|
-
Image.new(data:, model:, mime_type:)
|
|
41
|
+
Image.new(data:, model:, mime_type: RubyLLM::Files::MimeType.for(StringIO.new(Base64.decode64(data))))
|
|
46
42
|
end
|
|
47
43
|
end
|
|
48
44
|
|
|
@@ -71,7 +67,7 @@ module RubyLLM
|
|
|
71
67
|
end
|
|
72
68
|
|
|
73
69
|
def encoded_image(attachment)
|
|
74
|
-
raise UnsupportedAttachmentError, attachment.mime_type unless
|
|
70
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
75
71
|
|
|
76
72
|
attachment.encoded
|
|
77
73
|
end
|
|
@@ -23,14 +23,8 @@ module RubyLLM
|
|
|
23
23
|
# The G1 and V1 models take inputText alone; Bedrock rejects the V2
|
|
24
24
|
# tuning keys as extraneous.
|
|
25
25
|
def render_embedding_payload(text, model:, dimensions:, provider_options:, **)
|
|
26
|
-
payload = { inputText: text.to_s }
|
|
27
|
-
|
|
28
|
-
if titan_v2?(model)
|
|
29
|
-
payload[:dimensions] = dimensions if dimensions
|
|
30
|
-
payload[:normalize] = true
|
|
31
|
-
elsif dimensions
|
|
32
|
-
raise Error, "#{model} does not support custom dimensions"
|
|
33
|
-
end
|
|
26
|
+
payload = { inputText: text.to_s, dimensions: }.compact
|
|
27
|
+
payload[:normalize] = true if titan_v2?(model)
|
|
34
28
|
|
|
35
29
|
deep_merge_provider_options(payload, provider_options)
|
|
36
30
|
end
|
|
@@ -10,10 +10,7 @@ module RubyLLM
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
def signed_post(url, payload, additional_headers = {})
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@connection.post(url, payload, usage: @usage_tracker) do |req|
|
|
16
|
-
req.headers.merge!(@provider.sign_headers('POST', url, body))
|
|
13
|
+
@connection.post(url, JSON.generate(payload), usage: @usage_tracker) do |req|
|
|
17
14
|
req.headers.merge!(additional_headers) unless additional_headers.empty?
|
|
18
15
|
end
|
|
19
16
|
end
|
|
@@ -11,10 +11,23 @@ module RubyLLM
|
|
|
11
11
|
output.select { |entry| entry['type'] == 'message.output' }.each do |entry|
|
|
12
12
|
parse_conversation_parts(entry['content'], result)
|
|
13
13
|
end
|
|
14
|
+
result[:attachments].concat(output.filter_map do |entry|
|
|
15
|
+
next unless entry['type'] == 'tool.execution' && entry['name'] == 'image_generation'
|
|
16
|
+
|
|
17
|
+
parse_generated_image(entry.dig('info', 'result'))
|
|
18
|
+
end)
|
|
19
|
+
result[:attachments].uniq!(&:source)
|
|
14
20
|
result[:thinking] = nil if result[:thinking].empty?
|
|
15
21
|
result
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
def parse_generated_image(result)
|
|
25
|
+
return unless result
|
|
26
|
+
|
|
27
|
+
url = JSON.parse(result)['url']
|
|
28
|
+
Attachment.new(url, config: @config) if url
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
def parse_conversation_parts(content, result)
|
|
19
32
|
return result[:text] << content if content.is_a?(String)
|
|
20
33
|
|
|
@@ -5,6 +5,10 @@ module RubyLLM
|
|
|
5
5
|
module Mistral
|
|
6
6
|
class Conversations
|
|
7
7
|
module Chat # :nodoc:
|
|
8
|
+
SERVER_TOOL_USAGE_NAMES = {
|
|
9
|
+
'code_interpreter' => 'code_execution', 'document_library' => 'file_search'
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
8
12
|
module_function
|
|
9
13
|
|
|
10
14
|
def completion_url
|
|
@@ -150,9 +154,15 @@ module RubyLLM
|
|
|
150
154
|
input = usage['prompt_tokens'] && (usage['prompt_tokens'] + usage.fetch('connector_tokens', 0).to_i)
|
|
151
155
|
{
|
|
152
156
|
input_tokens: input,
|
|
153
|
-
output_tokens: usage['completion_tokens'], server_tool_use: usage['connectors']
|
|
157
|
+
output_tokens: usage['completion_tokens'], server_tool_use: parse_connector_usage(usage['connectors'])
|
|
154
158
|
}
|
|
155
159
|
end
|
|
160
|
+
|
|
161
|
+
def parse_connector_usage(connectors)
|
|
162
|
+
connectors.to_h.transform_keys do |connector|
|
|
163
|
+
"#{SERVER_TOOL_USAGE_NAMES.fetch(connector, connector)}_requests"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
156
166
|
end
|
|
157
167
|
end
|
|
158
168
|
end
|