ruby_llm 2.1.0 → 2.2.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 +162 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1598 -332
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +67 -28
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +134 -109
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +219 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
|
@@ -6,7 +6,7 @@ module RubyLLM
|
|
|
6
6
|
# Chat methods for the Gemini API implementation
|
|
7
7
|
module Chat
|
|
8
8
|
FINISH_REASONS = {
|
|
9
|
-
'STOP' => :stop, 'MAX_TOKENS' => :max_tokens,
|
|
9
|
+
'STOP' => :stop, 'MAX_TOKENS' => :max_tokens, 'CONTINUATION' => nil,
|
|
10
10
|
'SAFETY' => :content_filter, 'RECITATION' => :content_filter, 'BLOCKLIST' => :content_filter,
|
|
11
11
|
'PROHIBITED_CONTENT' => :content_filter, 'SPII' => :content_filter, 'IMAGE_SAFETY' => :content_filter,
|
|
12
12
|
'IMAGE_RECITATION' => :content_filter, 'IMAGE_PROHIBITED_CONTENT' => :content_filter,
|
|
@@ -23,7 +23,7 @@ module RubyLLM
|
|
|
23
23
|
def normalize_finish_reason(reason)
|
|
24
24
|
return nil if reason.nil?
|
|
25
25
|
|
|
26
|
-
finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
|
|
26
|
+
finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def completion_url
|
|
@@ -35,11 +35,13 @@ module RubyLLM
|
|
|
35
35
|
thinking: nil, citations: false, caching: nil, tool_prefs: nil)
|
|
36
36
|
warn_unsupported_citations(model) if citations && !model.supports?(:citations)
|
|
37
37
|
tool_prefs ||= {}
|
|
38
|
+
unfinished = messages.last if messages.last&.continuation
|
|
38
39
|
payload = {
|
|
39
|
-
contents:
|
|
40
|
+
contents: render_messages(messages.reject { |msg| msg.role == :system || msg.equal?(unfinished) }),
|
|
40
41
|
generationConfig: {}
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
payload[:continuationToken] = unfinished.continuation if unfinished
|
|
44
|
+
system_instruction = render_system_instruction(messages)
|
|
43
45
|
payload[:systemInstruction] = system_instruction if system_instruction
|
|
44
46
|
|
|
45
47
|
payload[:generationConfig][:temperature] = temperature unless temperature.nil?
|
|
@@ -49,7 +51,7 @@ module RubyLLM
|
|
|
49
51
|
payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking&.enabled?
|
|
50
52
|
|
|
51
53
|
if tools.any?
|
|
52
|
-
payload[:tools] =
|
|
54
|
+
payload[:tools] = render_tools(tools)
|
|
53
55
|
# Gemini doesn't support controlling parallel tool calls
|
|
54
56
|
payload[:toolConfig] = build_tool_config(tool_prefs[:choice]) unless tool_prefs[:choice].nil?
|
|
55
57
|
end
|
|
@@ -108,9 +110,10 @@ module RubyLLM
|
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
def build_thinking_config(_model, thinking)
|
|
111
|
-
|
|
113
|
+
off = thinking.enabled == false || (thinking.budget.is_a?(Integer) && thinking.budget.zero?)
|
|
114
|
+
return { includeThoughts: false, thinkingBudget: 0 } if off
|
|
112
115
|
|
|
113
|
-
config = { includeThoughts:
|
|
116
|
+
config = { includeThoughts: include_thoughts?(thinking.display) }
|
|
114
117
|
|
|
115
118
|
config[:thinkingLevel] = thinking.effort.to_s if thinking.effort
|
|
116
119
|
config[:thinkingBudget] = thinking.budget if thinking.budget.is_a?(Integer)
|
|
@@ -119,7 +122,15 @@ module RubyLLM
|
|
|
119
122
|
config
|
|
120
123
|
end
|
|
121
124
|
|
|
122
|
-
def
|
|
125
|
+
def include_thoughts?(display)
|
|
126
|
+
unless [nil, :summarized, :omitted].include?(display)
|
|
127
|
+
raise ArgumentError, 'Gemini thinking display must be summarized or omitted'
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
display != :omitted
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def provider_file_references?
|
|
123
134
|
true
|
|
124
135
|
end
|
|
125
136
|
|
|
@@ -133,20 +144,20 @@ module RubyLLM
|
|
|
133
144
|
|
|
134
145
|
private
|
|
135
146
|
|
|
136
|
-
def
|
|
147
|
+
def render_system_instruction(messages)
|
|
137
148
|
parts = messages.select { |msg| msg.role == :system }.flat_map do |msg|
|
|
138
149
|
text = msg.content.to_s
|
|
139
|
-
Media.
|
|
150
|
+
Media.render_content(text.empty? ? nil : text, msg.attachments)
|
|
140
151
|
end
|
|
141
152
|
|
|
142
153
|
{ parts: parts } if parts.any?
|
|
143
154
|
end
|
|
144
155
|
|
|
145
|
-
def
|
|
156
|
+
def render_messages(messages)
|
|
146
157
|
MessageFormatter.new(self, messages).format
|
|
147
158
|
end
|
|
148
159
|
|
|
149
|
-
def
|
|
160
|
+
def render_role(role)
|
|
150
161
|
case role
|
|
151
162
|
when :assistant then 'model'
|
|
152
163
|
when :system, :tool then 'user'
|
|
@@ -154,20 +165,21 @@ module RubyLLM
|
|
|
154
165
|
end
|
|
155
166
|
end
|
|
156
167
|
|
|
157
|
-
def
|
|
168
|
+
def render_parts(msg)
|
|
158
169
|
if msg.role == :assistant && msg.raw_content.is_a?(Array)
|
|
159
170
|
msg.raw_content
|
|
160
171
|
elsif msg.tool_call?
|
|
161
|
-
|
|
172
|
+
render_tool_call(msg)
|
|
162
173
|
elsif msg.tool_result?
|
|
163
|
-
|
|
174
|
+
render_tool_result(msg)
|
|
164
175
|
else
|
|
165
|
-
|
|
176
|
+
render_message_parts(msg)
|
|
166
177
|
end
|
|
167
178
|
end
|
|
168
179
|
|
|
169
|
-
def
|
|
170
|
-
|
|
180
|
+
def render_message_parts(msg)
|
|
181
|
+
content = msg.content unless msg.role == :assistant && msg.content.to_s.empty?
|
|
182
|
+
parts = Media.render_content(content, msg.attachments)
|
|
171
183
|
thinking = msg.thinking if msg.role == :assistant
|
|
172
184
|
return parts unless thinking
|
|
173
185
|
|
|
@@ -179,27 +191,29 @@ module RubyLLM
|
|
|
179
191
|
# Gemini signs the last part of an answer. An answer without parts gets
|
|
180
192
|
# the empty text part a stream ends with: Gemini refuses a part without data.
|
|
181
193
|
def sign_last_part(parts, signature)
|
|
182
|
-
parts = [Media.
|
|
194
|
+
parts = [Media.render_text('')] if parts.empty?
|
|
183
195
|
[*parts[...-1], parts.last.merge(thoughtSignature: signature)]
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
def parse_completion_body(data, raw:)
|
|
187
199
|
parts = data.dig('candidates', 0, 'content', 'parts') || []
|
|
188
|
-
tool_calls =
|
|
200
|
+
tool_calls = parse_tool_calls(data)
|
|
189
201
|
content, attachments = parse_content(data)
|
|
202
|
+
continuation = parse_continuation(data)
|
|
190
203
|
|
|
191
204
|
Message.new(
|
|
192
205
|
role: :assistant,
|
|
193
206
|
content: content,
|
|
194
207
|
attachments: attachments,
|
|
195
|
-
citations:
|
|
208
|
+
citations: parse_citations(data, parts),
|
|
196
209
|
thinking: Thinking.build(
|
|
197
|
-
text:
|
|
198
|
-
signature:
|
|
210
|
+
text: parse_thought_parts(parts),
|
|
211
|
+
signature: parse_thought_signature(parts)
|
|
199
212
|
),
|
|
200
213
|
tool_calls: tool_calls,
|
|
201
|
-
server_tool_calls:
|
|
202
|
-
raw_content: parts.any? { |
|
|
214
|
+
server_tool_calls: parse_server_tool_calls(data, parts),
|
|
215
|
+
raw_content: (without_search_suggestions(parts) if continuation || parts.any? { |p| server_tool_part?(p) }),
|
|
216
|
+
continuation: continuation,
|
|
203
217
|
input_tokens: input_tokens(data),
|
|
204
218
|
output_tokens: calculate_output_tokens(data),
|
|
205
219
|
cache_read_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
@@ -213,6 +227,14 @@ module RubyLLM
|
|
|
213
227
|
)
|
|
214
228
|
end
|
|
215
229
|
|
|
230
|
+
# Long decode hands back a token whenever it pauses, including at
|
|
231
|
+
# stream checkpoints and on MAX_TOKENS; only CONTINUATION leaves the
|
|
232
|
+
# response unfinished.
|
|
233
|
+
def parse_continuation(data)
|
|
234
|
+
candidate = data.dig('candidates', 0) || {}
|
|
235
|
+
candidate['continuationToken'] if candidate['finishReason'] == 'CONTINUATION'
|
|
236
|
+
end
|
|
237
|
+
|
|
216
238
|
# Gemini 3 bills each distinct, non-empty query a grounding ran.
|
|
217
239
|
def parse_server_tool_use(data)
|
|
218
240
|
searches = Array(data['candidates']).sum do |candidate|
|
|
@@ -221,11 +243,15 @@ module RubyLLM
|
|
|
221
243
|
{ 'web_search_requests' => searches }
|
|
222
244
|
end
|
|
223
245
|
|
|
246
|
+
# Gemini reports what a server-side tool loaded for the model, such as
|
|
247
|
+
# the frames of an agentic video, apart from the prompt, and bills it
|
|
248
|
+
# as input.
|
|
224
249
|
def input_tokens(data)
|
|
225
|
-
|
|
250
|
+
usage = data['usageMetadata'] || {}
|
|
251
|
+
prompt_tokens = usage['promptTokenCount']
|
|
226
252
|
return unless prompt_tokens
|
|
227
253
|
|
|
228
|
-
[prompt_tokens.to_i
|
|
254
|
+
[prompt_tokens.to_i + usage['toolUsePromptTokenCount'].to_i - usage['cachedContentTokenCount'].to_i, 0].max
|
|
229
255
|
end
|
|
230
256
|
|
|
231
257
|
def parse_content(data)
|
|
@@ -260,7 +286,7 @@ module RubyLLM
|
|
|
260
286
|
end
|
|
261
287
|
end
|
|
262
288
|
|
|
263
|
-
def
|
|
289
|
+
def parse_server_tool_calls(data, parts)
|
|
264
290
|
calls = parts.select { |part| server_tool_part?(part) }.map do |part|
|
|
265
291
|
ServerToolCall.new(
|
|
266
292
|
type: part.key?('executableCode') ? 'executable_code' : 'code_execution_result',
|
|
@@ -280,6 +306,7 @@ module RubyLLM
|
|
|
280
306
|
queries = candidate.dig('groundingMetadata', 'webSearchQueries')
|
|
281
307
|
if queries&.any?
|
|
282
308
|
calls << ServerToolCall.new(type: 'google_search', input: { 'queries' => queries },
|
|
309
|
+
result: web_grounding_chunks(candidate),
|
|
283
310
|
raw: { 'webSearchQueries' => queries },
|
|
284
311
|
search_suggestions: candidate.dig('groundingMetadata', 'searchEntryPoint',
|
|
285
312
|
'renderedContent'))
|
|
@@ -290,8 +317,15 @@ module RubyLLM
|
|
|
290
317
|
calls
|
|
291
318
|
end
|
|
292
319
|
|
|
320
|
+
# Grounding on a store of documents reports retrievedContext chunks,
|
|
321
|
+
# which are not web pages.
|
|
322
|
+
def web_grounding_chunks(candidate)
|
|
323
|
+
chunks = Array(candidate.dig('groundingMetadata', 'groundingChunks')).select { |chunk| chunk['web'] }
|
|
324
|
+
chunks unless chunks.empty?
|
|
325
|
+
end
|
|
326
|
+
|
|
293
327
|
# Normalizes grounding metadata (Google Search grounding) into citations.
|
|
294
|
-
def
|
|
328
|
+
def parse_citations(data, parts)
|
|
295
329
|
metadata = data.dig('candidates', 0, 'groundingMetadata')
|
|
296
330
|
return [] unless metadata
|
|
297
331
|
|
|
@@ -299,13 +333,12 @@ module RubyLLM
|
|
|
299
333
|
supports = metadata['groundingSupports'] || []
|
|
300
334
|
return chunk_citations(chunks) if supports.empty?
|
|
301
335
|
|
|
302
|
-
supports.flat_map { |support| support_citations(support, chunks,
|
|
336
|
+
supports.flat_map { |support| support_citations(support, chunks, parts) }
|
|
303
337
|
end
|
|
304
338
|
|
|
305
|
-
def support_citations(support, chunks,
|
|
339
|
+
def support_citations(support, chunks, parts)
|
|
306
340
|
segment = support['segment'] || {}
|
|
307
|
-
end_index = segment
|
|
308
|
-
start_index = segment['startIndex'] || (0 if end_index)
|
|
341
|
+
start_index, end_index = citation_indices(parts, segment)
|
|
309
342
|
|
|
310
343
|
Array(support['groundingChunkIndices']).filter_map do |index|
|
|
311
344
|
source = chunk_source(chunks[index])
|
|
@@ -315,13 +348,39 @@ module RubyLLM
|
|
|
315
348
|
url: source['uri'],
|
|
316
349
|
title: source['title'],
|
|
317
350
|
text: segment['text'],
|
|
318
|
-
start_index:
|
|
319
|
-
end_index:
|
|
351
|
+
start_index: start_index,
|
|
352
|
+
end_index: end_index,
|
|
320
353
|
source_index: index
|
|
321
354
|
)
|
|
322
355
|
end
|
|
323
356
|
end
|
|
324
357
|
|
|
358
|
+
def citation_indices(parts, segment)
|
|
359
|
+
indices = [segment.fetch('startIndex', 0), segment['endIndex']].map do |byte_index|
|
|
360
|
+
citation_offset(parts, segment, byte_index)
|
|
361
|
+
end
|
|
362
|
+
indices.all? && indices.first <= indices.last ? indices : [nil, nil]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Without a partIndex, offsets count across the visible text, which
|
|
366
|
+
# keeps thinking parts ahead of the answer from absorbing the span.
|
|
367
|
+
def citation_offset(parts, segment, byte_index)
|
|
368
|
+
return byte_to_char_index(visible_text(parts), byte_index) unless segment.key?('partIndex')
|
|
369
|
+
|
|
370
|
+
part_index = segment['partIndex']
|
|
371
|
+
return unless part_index.is_a?(Integer) && part_index.between?(0, parts.length - 1)
|
|
372
|
+
|
|
373
|
+
part = parts[part_index]
|
|
374
|
+
return if part['thought']
|
|
375
|
+
|
|
376
|
+
index = byte_to_char_index(part['text'], byte_index)
|
|
377
|
+
visible_text(parts.take(part_index)).length + index if index
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def visible_text(parts)
|
|
381
|
+
parts.reject { |part| part['thought'] }.sum('') { |part| part['text'].to_s }
|
|
382
|
+
end
|
|
383
|
+
|
|
325
384
|
def chunk_citations(chunks)
|
|
326
385
|
chunks.each_with_index.filter_map do |chunk, index|
|
|
327
386
|
source = chunk_source(chunk)
|
|
@@ -337,21 +396,27 @@ module RubyLLM
|
|
|
337
396
|
chunk['web'] || chunk['retrievedContext']
|
|
338
397
|
end
|
|
339
398
|
|
|
340
|
-
# Grounding segment indices are byte offsets
|
|
399
|
+
# Grounding segment indices are byte offsets within the selected UTF-8 part.
|
|
341
400
|
def byte_to_char_index(content, byte_index)
|
|
342
|
-
return
|
|
401
|
+
return unless content.is_a?(String) && byte_index.is_a?(Integer) &&
|
|
402
|
+
byte_index.between?(0, content.bytesize)
|
|
343
403
|
|
|
344
|
-
content.byteslice(0, byte_index)
|
|
404
|
+
prefix = content.byteslice(0, byte_index)
|
|
405
|
+
prefix.length if prefix.valid_encoding?
|
|
345
406
|
end
|
|
346
407
|
|
|
347
|
-
def
|
|
408
|
+
def parse_thought_parts(parts)
|
|
348
409
|
thought_parts = parts.select { |p| p['thought'] }
|
|
349
410
|
thoughts = thought_parts.filter_map { |p| p['text'] }.join
|
|
350
411
|
thoughts.empty? ? nil : thoughts
|
|
351
412
|
end
|
|
352
413
|
|
|
353
|
-
|
|
414
|
+
# Agentic video navigation steps carry signatures of their own, which
|
|
415
|
+
# Gemini refuses on the answer they precede.
|
|
416
|
+
def parse_thought_signature(parts)
|
|
354
417
|
parts.each do |part|
|
|
418
|
+
next if part.key?('toolCall') || part.key?('toolResponse')
|
|
419
|
+
|
|
355
420
|
signature = part['thoughtSignature'] ||
|
|
356
421
|
part['thought_signature'] ||
|
|
357
422
|
part.dig('functionCall', 'thoughtSignature') ||
|
|
@@ -363,16 +428,15 @@ module RubyLLM
|
|
|
363
428
|
end
|
|
364
429
|
|
|
365
430
|
def calculate_output_tokens(data)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
candidates + thoughts
|
|
431
|
+
counts = (data['usageMetadata'] || {}).values_at('candidatesTokenCount', 'thoughtsTokenCount').compact
|
|
432
|
+
counts.sum unless counts.empty?
|
|
369
433
|
end
|
|
370
434
|
|
|
371
435
|
def build_json_schema(schema)
|
|
372
436
|
normalized = RubyLLM::Support::Utils.deep_dup(schema[:schema])
|
|
373
437
|
normalized.delete(:strict)
|
|
374
438
|
normalized.delete('strict')
|
|
375
|
-
RubyLLM::Support::Utils.deep_stringify_keys(normalized)
|
|
439
|
+
Schemas.render_schema(RubyLLM::Support::Utils.deep_stringify_keys(normalized))
|
|
376
440
|
end
|
|
377
441
|
|
|
378
442
|
def structured_output_config(schema)
|
|
@@ -402,7 +466,10 @@ module RubyLLM
|
|
|
402
466
|
@index = next_index
|
|
403
467
|
else
|
|
404
468
|
remember_tool_calls if current_message.tool_call?
|
|
405
|
-
|
|
469
|
+
turn = build_standard_message(current_message)
|
|
470
|
+
# Vertex AI refuses a turn without parts, which is what an empty
|
|
471
|
+
# answer is once another provider's signature is left out.
|
|
472
|
+
formatted << turn if turn[:parts].any? || current_message.role != :assistant
|
|
406
473
|
@index += 1
|
|
407
474
|
end
|
|
408
475
|
end
|
|
@@ -430,7 +497,7 @@ module RubyLLM
|
|
|
430
497
|
end
|
|
431
498
|
|
|
432
499
|
parts = in_call_order(results).flat_map do |tool_message|
|
|
433
|
-
|
|
500
|
+
render_tool_result(tool_message, @tool_call_names.delete(tool_message.tool_call_id))
|
|
434
501
|
end
|
|
435
502
|
|
|
436
503
|
[parts, index]
|
|
@@ -451,17 +518,17 @@ module RubyLLM
|
|
|
451
518
|
end
|
|
452
519
|
|
|
453
520
|
def remember_tool_calls
|
|
454
|
-
current_message.tool_calls.each do |tool_call_id, tool_call|
|
|
521
|
+
current_message.tool_calls.to_h.each do |tool_call_id, tool_call|
|
|
455
522
|
@tool_call_names[tool_call_id] = tool_call.name
|
|
456
523
|
end
|
|
457
524
|
end
|
|
458
525
|
|
|
459
526
|
def build_standard_message(message)
|
|
460
|
-
parts = @provider.send(:
|
|
527
|
+
parts = @provider.send(:render_parts, message)
|
|
461
528
|
parts = @provider.send(:sign_step, parts) if message.tool_call? && current_turn?
|
|
462
529
|
|
|
463
530
|
{
|
|
464
|
-
role: @provider.send(:
|
|
531
|
+
role: @provider.send(:render_role, message.role),
|
|
465
532
|
parts: parts
|
|
466
533
|
}
|
|
467
534
|
end
|
|
@@ -472,8 +539,8 @@ module RubyLLM
|
|
|
472
539
|
@turn_start.nil? || @index > @turn_start
|
|
473
540
|
end
|
|
474
541
|
|
|
475
|
-
def
|
|
476
|
-
@provider.send(:
|
|
542
|
+
def render_tool_result(message, tool_name)
|
|
543
|
+
@provider.send(:render_tool_result, message, tool_name)
|
|
477
544
|
end
|
|
478
545
|
end
|
|
479
546
|
end
|
|
@@ -44,11 +44,11 @@ module RubyLLM
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def parse_embedding_batch_results(responses)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
parse_embedding_batch_group(key,
|
|
47
|
+
identified, unidentified = responses.partition { |inline| inline.dig('metadata', 'custom_id') }
|
|
48
|
+
RubyLLM.logger.warn 'Gemini returned batch results without metadata' if unidentified.any?
|
|
49
|
+
|
|
50
|
+
identified.group_by { |inline| inline.dig('metadata', 'custom_id') }.filter_map do |key, group|
|
|
51
|
+
parse_embedding_batch_group(key, group)
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -60,6 +60,10 @@ module RubyLLM
|
|
|
60
60
|
metadata = responses.first.fetch('metadata')
|
|
61
61
|
return if responses.size < metadata.fetch('embedding_count')
|
|
62
62
|
|
|
63
|
+
parse_embedding_batch_slot(index, key, responses, metadata)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_embedding_batch_slot(index, key, responses, metadata)
|
|
63
67
|
positions = responses.map { |inline| inline.dig('metadata', 'embedding_index') }
|
|
64
68
|
unless positions.sort == (0...responses.size).to_a
|
|
65
69
|
return [index, nil, batch_failure(key, 'Invalid or duplicate embedding record positions')]
|
|
@@ -26,7 +26,7 @@ module RubyLLM
|
|
|
26
26
|
Support::Utils.deep_merge({ requests: requests }, provider_options)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def embedding_media?
|
|
30
30
|
true
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -42,7 +42,7 @@ module RubyLLM
|
|
|
42
42
|
vectors = response.body['embeddings']&.map { |e| e['values'] }
|
|
43
43
|
vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
|
|
44
44
|
|
|
45
|
-
Embedding.new(vectors:, model:)
|
|
45
|
+
Embedding.new(vectors:, model:, input_tokens: response.body.dig('usageMetadata', 'promptTokenCount'))
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
private
|
|
@@ -52,7 +52,7 @@ module RubyLLM
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def media_embedding_payload(text, attachments, model:, dimensions:, task_type: nil, title: nil)
|
|
55
|
-
embedding_payload(Media.
|
|
55
|
+
embedding_payload(Media.render_content(text, attachments), model:, dimensions:, task_type:, title:)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def embedding_payload(parts, model:, dimensions:, task_type:, title:)
|
|
@@ -4,6 +4,10 @@ module RubyLLM
|
|
|
4
4
|
module Protocols
|
|
5
5
|
class Gemini
|
|
6
6
|
module FileTranscription # :nodoc: all
|
|
7
|
+
def rejected_options(operation)
|
|
8
|
+
operation == :transcribe ? %i[named_speakers] : super
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
|
|
8
12
|
speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil)
|
|
9
13
|
raise_transcription_streaming_unsupported if block_given?
|
|
@@ -11,9 +11,10 @@ module RubyLLM
|
|
|
11
11
|
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
12
12
|
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
|
|
13
13
|
provider_options: {})
|
|
14
|
+
@content_type = content_type
|
|
14
15
|
attachment = file_attachment(file, filename:)
|
|
15
|
-
|
|
16
|
-
upload_url = start_resumable_upload(attachment,
|
|
16
|
+
resource = { display_name: attachment.filename }.merge(provider_options.transform_keys(&:to_sym))
|
|
17
|
+
upload_url = start_resumable_upload(attachment, resource:)
|
|
17
18
|
response = upload_file_bytes(upload_url, attachment)
|
|
18
19
|
await_active(parse_file_response(response.body.fetch('file')))
|
|
19
20
|
end
|
|
@@ -51,7 +52,7 @@ module RubyLLM
|
|
|
51
52
|
gemini_file_name(file_id)
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
def start_resumable_upload(attachment,
|
|
55
|
+
def start_resumable_upload(attachment, resource:)
|
|
55
56
|
response = gemini_connection(request_json: true, response_json: true).post(gemini_upload_url) do |request|
|
|
56
57
|
request.headers.merge!(@provider.headers)
|
|
57
58
|
request.headers['X-Goog-Upload-Protocol'] = 'resumable'
|
|
@@ -59,7 +60,7 @@ module RubyLLM
|
|
|
59
60
|
request.headers['X-Goog-Upload-Header-Content-Length'] = attachment.content.bytesize.to_s
|
|
60
61
|
request.headers['X-Goog-Upload-Header-Content-Type'] = file_content_type(attachment)
|
|
61
62
|
request.headers['Content-Type'] = 'application/json'
|
|
62
|
-
request.body = { file:
|
|
63
|
+
request.body = { file: resource }
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
response.headers['x-goog-upload-url'] || response.headers['X-Goog-Upload-URL'] ||
|
|
@@ -9,13 +9,13 @@ module RubyLLM
|
|
|
9
9
|
ASPECT_RATIO = /\A\d+:\d+\z/
|
|
10
10
|
PIXEL_DIMENSIONS = /\A(\d+)\s*[x×]\s*(\d+)\z/i
|
|
11
11
|
|
|
12
|
-
def images_url(
|
|
12
|
+
def images_url(**)
|
|
13
13
|
id = model_id(@model)
|
|
14
14
|
|
|
15
15
|
"models/#{id}:#{image_endpoint_action(id)}"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def render_image_payload(prompt, model:, size:, count: nil, with: nil,
|
|
18
|
+
def render_image_payload(prompt, model:, size:, count: nil, with: nil, provider_options: {}, **)
|
|
19
19
|
@model = model
|
|
20
20
|
payload = if gemini_image_model?(model)
|
|
21
21
|
render_gemini_image_payload(prompt, with:, count:, size:)
|
|
@@ -52,7 +52,8 @@ module RubyLLM
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def render_imagen_payload(prompt, count: nil, size: nil)
|
|
55
|
-
|
|
55
|
+
image_config = build_image_config(size) || {}
|
|
56
|
+
raise ArgumentError, "Imagen takes an aspect ratio, got size #{size.inspect}" if image_config.key?(:imageSize)
|
|
56
57
|
|
|
57
58
|
{
|
|
58
59
|
instances: [
|
|
@@ -62,7 +63,7 @@ module RubyLLM
|
|
|
62
63
|
],
|
|
63
64
|
parameters: {
|
|
64
65
|
sampleCount: count || 1
|
|
65
|
-
}
|
|
66
|
+
}.merge(image_config)
|
|
66
67
|
}
|
|
67
68
|
end
|
|
68
69
|
|
|
@@ -76,7 +77,7 @@ module RubyLLM
|
|
|
76
77
|
contents: [
|
|
77
78
|
{
|
|
78
79
|
role: 'user',
|
|
79
|
-
parts: Media.
|
|
80
|
+
parts: Media.render_content(prompt, image_attachments(with))
|
|
80
81
|
}
|
|
81
82
|
],
|
|
82
83
|
generationConfig: generation_config
|
|
@@ -142,9 +143,9 @@ module RubyLLM
|
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
def gemini_image_parts(data)
|
|
145
|
-
Array(data['candidates']).
|
|
146
|
+
Array(data['candidates']).flat_map do |candidate|
|
|
146
147
|
parts = candidate.dig('content', 'parts') || []
|
|
147
|
-
parts.filter_map { |part| part['inlineData'] }.
|
|
148
|
+
parts.reject { |part| part['thought'] }.filter_map { |part| part['inlineData'] }.select do |inline_data|
|
|
148
149
|
image_mime_type?(inline_data['mimeType']) && inline_data['data']
|
|
149
150
|
end
|
|
150
151
|
end
|
|
@@ -157,7 +158,8 @@ module RubyLLM
|
|
|
157
158
|
def gemini_image_usage(data)
|
|
158
159
|
{
|
|
159
160
|
'input_tokens' => input_tokens(data),
|
|
160
|
-
'output_tokens' => calculate_output_tokens(data)
|
|
161
|
+
'output_tokens' => calculate_output_tokens(data),
|
|
162
|
+
'cache_read_tokens' => data.dig('usageMetadata', 'cachedContentTokenCount')
|
|
161
163
|
}.compact
|
|
162
164
|
end
|
|
163
165
|
|
|
@@ -169,7 +171,7 @@ module RubyLLM
|
|
|
169
171
|
|
|
170
172
|
def gemini_image_model?(model)
|
|
171
173
|
id = model_id(model).downcase
|
|
172
|
-
return true if id.start_with?('nano-banana', 'nanobanana')
|
|
174
|
+
return true if id.start_with?('nano-banana', 'nanobanana', 'gemini-nano-banana')
|
|
173
175
|
|
|
174
176
|
id.start_with?('gemini-') && id.include?('-image')
|
|
175
177
|
end
|
|
@@ -92,7 +92,7 @@ module RubyLLM
|
|
|
92
92
|
socket.close if event.dig('serverContent', 'generationComplete')
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
|
-
unless events.
|
|
95
|
+
unless events.any? { |event| event.dig('serverContent', 'generationComplete') }
|
|
96
96
|
raise Error, 'Google Live transcription ended before generation completed'
|
|
97
97
|
end
|
|
98
98
|
|