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
|
@@ -4,6 +4,8 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
# OpenRouter API integration.
|
|
6
6
|
class OpenRouter < Provider
|
|
7
|
+
BAD_GATEWAY = 502
|
|
8
|
+
|
|
7
9
|
# OpenRouter's dialect of the Chat Completions API.
|
|
8
10
|
class ChatCompletions < Protocols::ChatCompletions
|
|
9
11
|
include OpenRouter::Chat
|
|
@@ -30,22 +32,27 @@ module RubyLLM
|
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# OpenRouter's dialect of the System One API for decision models such as Jev and Clef.
|
|
36
|
+
class SystemOne < Protocols::SystemOne
|
|
37
|
+
include OpenRouter::Judgments
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
protocol :chat_completions, ChatCompletions, batches: Protocols::OpenRouter::Batches
|
|
34
41
|
protocol :responses, Protocols::OpenRouter::Responses
|
|
35
42
|
protocol :files, Protocols::OpenRouter::Files
|
|
43
|
+
protocol :system_one, SystemOne
|
|
36
44
|
|
|
37
|
-
def resolve_protocol(name, model, **
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
def resolve_protocol(name, model, operation: nil, **options)
|
|
46
|
+
name ||= (operation == :judge ? :system_one : :chat_completions) if operation
|
|
40
47
|
super
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
def api_base
|
|
44
|
-
|
|
51
|
+
configured_api_base(:openrouter_api_base, 'https://openrouter.ai/api/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def batch_api_base
|
|
48
|
-
"#{api_base
|
|
55
|
+
"#{api_base}/batches"
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def headers
|
|
@@ -77,7 +84,27 @@ module RubyLLM
|
|
|
77
84
|
end
|
|
78
85
|
end
|
|
79
86
|
|
|
87
|
+
# OpenRouter answers 200 when an upstream provider fails after routing,
|
|
88
|
+
# with an error object or an empty body. A stream has no body here.
|
|
89
|
+
def error_status(response)
|
|
90
|
+
return if response.request.stream_response?
|
|
91
|
+
|
|
92
|
+
body = response.body.to_s
|
|
93
|
+
return BAD_GATEWAY if body.strip.empty?
|
|
94
|
+
return unless body.include?('"error"')
|
|
95
|
+
|
|
96
|
+
error = parse_error_body(response).then { |parsed| parsed['error'] if parsed.is_a?(Hash) }
|
|
97
|
+
return unless error.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
code = error['code']
|
|
100
|
+
code.is_a?(Integer) && code >= 400 ? code : BAD_GATEWAY
|
|
101
|
+
end
|
|
102
|
+
|
|
80
103
|
class << self
|
|
104
|
+
def models_dev_provider_ids # :nodoc:
|
|
105
|
+
%w[openrouter]
|
|
106
|
+
end
|
|
107
|
+
|
|
81
108
|
def configuration_options
|
|
82
109
|
%i[openrouter_api_key openrouter_api_base openrouter_app_url openrouter_app_name]
|
|
83
110
|
end
|
|
@@ -7,7 +7,7 @@ module RubyLLM
|
|
|
7
7
|
module Chat
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def render_role(role)
|
|
11
11
|
role.to_s
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -15,8 +15,8 @@ module RubyLLM
|
|
|
15
15
|
Perplexity::Media.supported_attachment?(attachment)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
Perplexity::Media.
|
|
18
|
+
def render_content(content, attachments = [])
|
|
19
|
+
Perplexity::Media.render_content(content, attachments)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -15,7 +15,7 @@ module RubyLLM
|
|
|
15
15
|
def parse_embedding_response(response, model:, text:)
|
|
16
16
|
data = response.body
|
|
17
17
|
input_tokens = data.dig('usage', 'prompt_tokens')
|
|
18
|
-
vectors = Array(data['data']).map { |vector| decode_embedding(vector['embedding']) }
|
|
18
|
+
vectors = order_embedding_rows(Array(data['data'])).map { |vector| decode_embedding(vector['embedding']) }
|
|
19
19
|
vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
|
|
20
20
|
|
|
21
21
|
Embedding.new(vectors:, model:, input_tokens:)
|
|
@@ -11,22 +11,22 @@ module RubyLLM
|
|
|
11
11
|
%i[image pdf document text].include?(attachment.type)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
Protocols::ChatCompletions::Media.
|
|
14
|
+
def render_content(content, attachments = [])
|
|
15
|
+
Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
|
|
16
16
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
17
|
|
|
18
18
|
case attachment.type
|
|
19
19
|
when :image
|
|
20
|
-
Protocols::ChatCompletions::Media.
|
|
20
|
+
Protocols::ChatCompletions::Media.render_image(attachment)
|
|
21
21
|
when :pdf, :document
|
|
22
|
-
|
|
22
|
+
render_document(attachment)
|
|
23
23
|
when :text
|
|
24
|
-
Protocols::ChatCompletions::Media.
|
|
24
|
+
Protocols::ChatCompletions::Media.render_text_file(attachment)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def render_document(attachment)
|
|
30
30
|
{
|
|
31
31
|
type: 'file_url',
|
|
32
32
|
file_url: {
|
|
@@ -14,10 +14,11 @@ module RubyLLM
|
|
|
14
14
|
protocol :chat_completions, ChatCompletions
|
|
15
15
|
protocol :router_chat_completions, Protocols::Perplexity::Router
|
|
16
16
|
protocol :files, Protocols::Perplexity::Files
|
|
17
|
-
protocol :agent_responses, Protocols::Perplexity::Agent
|
|
17
|
+
protocol :agent_responses, Protocols::Perplexity::Agent, batches: Protocols::Perplexity::Batches
|
|
18
18
|
|
|
19
19
|
def api_base
|
|
20
|
-
|
|
20
|
+
configured_api_base(:perplexity_api_base, 'https://api.perplexity.ai',
|
|
21
|
+
endpoints: %r{/(?:chat/completions|v1/agent|v1/embeddings)\z})
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Chat runs on the Agent API. Embeddings and the model listing stay on
|
|
@@ -58,6 +59,10 @@ module RubyLLM
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
class << self
|
|
62
|
+
def models_dev_provider_ids # :nodoc:
|
|
63
|
+
%w[perplexity perplexity-agent]
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
def configuration_options
|
|
62
67
|
%i[perplexity_api_key perplexity_api_base]
|
|
63
68
|
end
|
|
@@ -66,6 +71,12 @@ module RubyLLM
|
|
|
66
71
|
%i[perplexity_api_key]
|
|
67
72
|
end
|
|
68
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def batch_protocol
|
|
78
|
+
batch_protocol_for_name(:agent_responses)
|
|
79
|
+
end
|
|
69
80
|
end
|
|
70
81
|
end
|
|
71
82
|
end
|
|
@@ -25,7 +25,11 @@ module RubyLLM
|
|
|
25
25
|
raise Error, "#{@provider.name} doesn't support token counting for Claude models"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
28
|
+
def thinking_block_binding?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def provider_file_references?
|
|
29
33
|
false
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -9,7 +9,13 @@ module RubyLLM
|
|
|
9
9
|
# Google credentials shared by every Vertex AI provider in the process.
|
|
10
10
|
# googleauth keeps the access token on the credentials object and
|
|
11
11
|
# refreshes it a minute before it expires, so sharing the object shares
|
|
12
|
-
# the token.
|
|
12
|
+
# the token. With a token timeout, token requests go through the
|
|
13
|
+
# middleware and adapter of Faraday's default connection, as googleauth's
|
|
14
|
+
# own would, with that timeout and its proxy. Faraday 1 drops a
|
|
15
|
+
# builder's adapter when it duplicates a builder without middleware, so
|
|
16
|
+
# the builder is shared, not duplicated. The connection is built for
|
|
17
|
+
# each token fetch, so it takes the proxy the default connection would
|
|
18
|
+
# use at that moment, from its settings or the environment.
|
|
13
19
|
class Credentials # :nodoc:
|
|
14
20
|
CACHE = Support::ProcessCache.new
|
|
15
21
|
|
|
@@ -17,7 +23,8 @@ module RubyLLM
|
|
|
17
23
|
|
|
18
24
|
def self.for(config)
|
|
19
25
|
key = config.vertexai_service_account_key
|
|
20
|
-
|
|
26
|
+
timeout = config.vertexai_token_timeout
|
|
27
|
+
CACHE.fetch([source(key), SCOPES, timeout]) { new(key, token_timeout: timeout) }
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
def self.cache
|
|
@@ -31,7 +38,8 @@ module RubyLLM
|
|
|
31
38
|
end
|
|
32
39
|
private_class_method :source
|
|
33
40
|
|
|
34
|
-
def initialize(service_account_key)
|
|
41
|
+
def initialize(service_account_key, token_timeout: nil)
|
|
42
|
+
@token_timeout = token_timeout
|
|
35
43
|
@authorizer = build(service_account_key)
|
|
36
44
|
@lock = Mutex.new
|
|
37
45
|
end
|
|
@@ -44,6 +52,22 @@ module RubyLLM
|
|
|
44
52
|
private
|
|
45
53
|
|
|
46
54
|
def build(service_account_key)
|
|
55
|
+
authorizer = google_credentials(service_account_key)
|
|
56
|
+
return authorizer unless @token_timeout && authorizer.respond_to?(:configure_connection)
|
|
57
|
+
|
|
58
|
+
authorizer.configure_connection(connection_builder: method(:token_connection))
|
|
59
|
+
authorizer
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def token_connection
|
|
63
|
+
default = Faraday.default_connection
|
|
64
|
+
connection = Faraday.new(builder: default.builder, headers: default.headers, params: default.params,
|
|
65
|
+
ssl: default.ssl, request: default.options.merge(timeout: @token_timeout))
|
|
66
|
+
connection.proxy = default.proxy_for_request(@authorizer.token_credential_uri.to_s)
|
|
67
|
+
connection
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def google_credentials(service_account_key)
|
|
47
71
|
require 'googleauth'
|
|
48
72
|
if service_account_key
|
|
49
73
|
::Google::Auth::ServiceAccountCredentials.make_creds(
|
|
@@ -23,7 +23,7 @@ module RubyLLM
|
|
|
23
23
|
raise ArgumentError, "#{model} takes task instructions and titles in the text" if task_type || title
|
|
24
24
|
|
|
25
25
|
payload = {
|
|
26
|
-
content: { parts: Protocols::Gemini::Media.
|
|
26
|
+
content: { parts: Protocols::Gemini::Media.render_content(text.is_a?(Array) ? text.first : text, with) },
|
|
27
27
|
outputDimensionality: dimensions
|
|
28
28
|
}.compact
|
|
29
29
|
Support::Utils.deep_merge(payload, provider_options)
|
|
@@ -11,12 +11,11 @@ module RubyLLM
|
|
|
11
11
|
"#{@provider.model_path(model)}:predict"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def embedding_media?
|
|
15
15
|
false
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, provider_options: {})
|
|
18
|
+
def render_embedding_payload(text, dimensions:, task_type: nil, title: nil, provider_options: {}, **)
|
|
20
19
|
instances = [text].flatten.map do |t|
|
|
21
20
|
{ content: t.to_s, task_type: task_type, title: title }.compact
|
|
22
21
|
end
|
|
@@ -10,6 +10,7 @@ module RubyLLM
|
|
|
10
10
|
include Providers::Mistral::Embeddings
|
|
11
11
|
include Providers::Mistral::Media
|
|
12
12
|
include Providers::Mistral::Models
|
|
13
|
+
include Providers::Mistral::OCR
|
|
13
14
|
|
|
14
15
|
# The Mistral model families Vertex AI serves directly. Shared by the
|
|
15
16
|
# registry (which models to list) and protocol_for (where to route them).
|
|
@@ -22,6 +23,10 @@ module RubyLLM
|
|
|
22
23
|
def stream_url
|
|
23
24
|
"#{@provider.model_path(@model.id, publisher: 'mistralai')}:streamRawPredict"
|
|
24
25
|
end
|
|
26
|
+
|
|
27
|
+
def ocr_url
|
|
28
|
+
completion_url
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
end
|
|
@@ -29,6 +29,10 @@ module RubyLLM
|
|
|
29
29
|
text-multilingual-embedding-002
|
|
30
30
|
].freeze
|
|
31
31
|
|
|
32
|
+
# Cloud Text-to-Speech voice models, which no publisher catalog lists.
|
|
33
|
+
# Each id is the model part of its voice names: en-US-Chirp3-HD-Charon.
|
|
34
|
+
TEXT_TO_SPEECH_MODELS = { 'Chirp3-HD' => 'Chirp 3: HD' }.freeze
|
|
35
|
+
|
|
32
36
|
# Every publisher with models Vertex AI serves as a service. The rest
|
|
33
37
|
# of the Model Garden is deploy-it-yourself and not callable directly.
|
|
34
38
|
PUBLISHERS = %w[google anthropic mistralai meta deepseek-ai qwen openai moonshotai zai-org].freeze
|
|
@@ -57,7 +61,7 @@ module RubyLLM
|
|
|
57
61
|
models = fetched.uniq(&:id)
|
|
58
62
|
log_catalog(counts, models)
|
|
59
63
|
|
|
60
|
-
models + build_known_models(models.map(&:id))
|
|
64
|
+
models + build_known_models(models.map(&:id)) + text_to_speech_models
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
private
|
|
@@ -77,7 +81,7 @@ module RubyLLM
|
|
|
77
81
|
def connection_for(location)
|
|
78
82
|
return @connection if location == configured_location
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
@provider.signed_connection(@provider.api_base_for(location))
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
def log_catalog(counts, models)
|
|
@@ -178,6 +182,24 @@ module RubyLLM
|
|
|
178
182
|
end
|
|
179
183
|
end
|
|
180
184
|
|
|
185
|
+
def text_to_speech_models
|
|
186
|
+
TEXT_TO_SPEECH_MODELS.map do |model_id, name|
|
|
187
|
+
Model.new(
|
|
188
|
+
id: model_id,
|
|
189
|
+
name:,
|
|
190
|
+
provider: @provider.slug,
|
|
191
|
+
family: 'chirp',
|
|
192
|
+
created_at: nil,
|
|
193
|
+
context_window: nil,
|
|
194
|
+
max_output_tokens: nil,
|
|
195
|
+
modalities: { input: %w[text], output: %w[audio] },
|
|
196
|
+
capabilities: %w[speech_generation],
|
|
197
|
+
pricing: nil,
|
|
198
|
+
metadata: { source: 'cloud_text_to_speech' }
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
181
203
|
def build_model_from_api_data(model_data, model_id)
|
|
182
204
|
Model.new(
|
|
183
205
|
id: model_id,
|
|
@@ -23,7 +23,7 @@ module RubyLLM
|
|
|
23
23
|
def render_transcription_payload(attachment, language:, speaker_names:, provider_options:, prompt:, **)
|
|
24
24
|
config = { languageCodes: language && Array(language), customVocabulary: prompt && Array(prompt),
|
|
25
25
|
diarization: speaker_names && true }.compact
|
|
26
|
-
payload = { contents: [{ role: 'user', parts: [
|
|
26
|
+
payload = { contents: [{ role: 'user', parts: [render_audio_part(attachment)] }],
|
|
27
27
|
generationConfig: { audioTranscriptionConfig: config } }
|
|
28
28
|
Support::Utils.deep_merge(payload, provider_options)
|
|
29
29
|
end
|
|
@@ -38,7 +38,7 @@ module RubyLLM
|
|
|
38
38
|
raise_if_transcription_blocked(data, text, response)
|
|
39
39
|
words = segments.flat_map { |segment| segment['words'] }
|
|
40
40
|
RubyLLM::Transcription.new(text:, model:, segments: segments.empty? ? nil : segments,
|
|
41
|
-
words: words.empty? ? nil : words, **
|
|
41
|
+
words: words.empty? ? nil : words, **parse_usage(data))
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def parse_transcription_segment(part)
|
|
@@ -4,6 +4,8 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Google Vertex AI implementation
|
|
6
6
|
class VertexAI < Provider
|
|
7
|
+
include Protocols::Gemini::RetryInfo
|
|
8
|
+
|
|
7
9
|
protocol :gemini, VertexAI::Gemini, batches: VertexAI::Gemini::Batches
|
|
8
10
|
protocol :anthropic, VertexAI::Anthropic, batches: VertexAI::Anthropic::Batches
|
|
9
11
|
protocol :mistral, VertexAI::Mistral
|
|
@@ -13,6 +15,7 @@ module RubyLLM
|
|
|
13
15
|
protocol :transcription, VertexAI::Transcription
|
|
14
16
|
protocol :live_transcription, VertexAI::LiveTranscription
|
|
15
17
|
protocol :ranking, Protocols::VertexAI::Ranking
|
|
18
|
+
protocol :text_to_speech, Protocols::VertexAI::TextToSpeech
|
|
16
19
|
protocol :research, Protocols::VertexAI::Research
|
|
17
20
|
protocol :files, Protocols::VertexAI::Files
|
|
18
21
|
|
|
@@ -47,6 +50,19 @@ module RubyLLM
|
|
|
47
50
|
def models_dev_model_id(id)
|
|
48
51
|
id&.split('@')&.first
|
|
49
52
|
end
|
|
53
|
+
|
|
54
|
+
def open_telemetry_name # :nodoc:
|
|
55
|
+
'gcp.vertex_ai'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def models_dev_provider_ids # :nodoc:
|
|
59
|
+
%w[google-vertex]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(config) # :nodoc:
|
|
64
|
+
super
|
|
65
|
+
@connection = signed_connection(api_base)
|
|
50
66
|
end
|
|
51
67
|
|
|
52
68
|
# Vertex AI hosts models from several publishers, each speaking its
|
|
@@ -55,8 +71,8 @@ module RubyLLM
|
|
|
55
71
|
def protocol_for(model, operation: nil, **)
|
|
56
72
|
return protocols[:ranking] if operation == :rerank
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
return
|
|
74
|
+
audio = audio_protocol_for(model.id, operation)
|
|
75
|
+
return audio if audio
|
|
60
76
|
|
|
61
77
|
if operation == :embed && %w[gemini-embedding-2 gemini-embedding-2-preview].include?(model.id)
|
|
62
78
|
return protocols[:embed_content]
|
|
@@ -124,12 +140,15 @@ module RubyLLM
|
|
|
124
140
|
end
|
|
125
141
|
|
|
126
142
|
def api_base_for(location)
|
|
127
|
-
|
|
143
|
+
if @config.vertexai_api_base
|
|
144
|
+
return configured_api_base(:vertexai_api_base, example: 'https://LOCATION-aiplatform.googleapis.com/v1beta1',
|
|
145
|
+
endpoints: %r{/projects/.*\z})
|
|
146
|
+
end
|
|
128
147
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
case location.to_s
|
|
149
|
+
when 'global' then 'https://aiplatform.googleapis.com/v1beta1'
|
|
150
|
+
when 'eu', 'us' then "https://aiplatform.#{location}.rep.googleapis.com/v1beta1"
|
|
151
|
+
else "https://#{location}-aiplatform.googleapis.com/v1beta1"
|
|
133
152
|
end
|
|
134
153
|
end
|
|
135
154
|
|
|
@@ -140,11 +159,25 @@ module RubyLLM
|
|
|
140
159
|
|
|
141
160
|
def ranking_connection # :nodoc:
|
|
142
161
|
base = @config.vertexai_ranking_api_base || 'https://discoveryengine.googleapis.com/v1'
|
|
143
|
-
@ranking_connection ||=
|
|
144
|
-
|
|
162
|
+
@ranking_connection ||= signed_connection(
|
|
163
|
+
base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Cloud Text-to-Speech answers at the multi-region or region named by
|
|
168
|
+
# vertexai_location, never at the global endpoint unless asked for it.
|
|
169
|
+
def text_to_speech_connection # :nodoc:
|
|
170
|
+
@text_to_speech_connection ||= signed_connection(
|
|
171
|
+
text_to_speech_api_base, headers: { 'X-Goog-User-Project' => @config.vertexai_project_id }
|
|
145
172
|
)
|
|
146
173
|
end
|
|
147
174
|
|
|
175
|
+
# Signs each attempt as it is sent, so a retry never replays an access
|
|
176
|
+
# token that expired while the previous attempt waited.
|
|
177
|
+
def signed_connection(base_url, headers: {}) # :nodoc:
|
|
178
|
+
Transport::Connection.new(self, @config, api_base: base_url, headers:, signer: ->(_env) { self.headers })
|
|
179
|
+
end
|
|
180
|
+
|
|
148
181
|
# The rescue can't name Google::Auth::AuthorizationError directly:
|
|
149
182
|
# when googleauth is missing, evaluating the constant would replace
|
|
150
183
|
# the helpful install error with a NameError.
|
|
@@ -170,6 +203,7 @@ module RubyLLM
|
|
|
170
203
|
vertexai_batch_gcs_uri
|
|
171
204
|
vertexai_ranking_api_base
|
|
172
205
|
vertexai_ranking_config
|
|
206
|
+
vertexai_token_timeout
|
|
173
207
|
]
|
|
174
208
|
end
|
|
175
209
|
|
|
@@ -180,6 +214,19 @@ module RubyLLM
|
|
|
180
214
|
|
|
181
215
|
private
|
|
182
216
|
|
|
217
|
+
def text_to_speech_api_base
|
|
218
|
+
location = @config.vertexai_location.to_s
|
|
219
|
+
host = location == 'global' ? 'texttospeech.googleapis.com' : "#{location}-texttospeech.googleapis.com"
|
|
220
|
+
"https://#{host}/v1"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def audio_protocol_for(id, operation)
|
|
224
|
+
case operation
|
|
225
|
+
when :transcribe then transcription_protocol_for(id)
|
|
226
|
+
when :speak then protocols[:text_to_speech] if Protocols::VertexAI::TextToSpeech::MODELS.include?(id)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
183
230
|
def transcription_protocol_for(id)
|
|
184
231
|
case id
|
|
185
232
|
when 'gemini-3.5-transcribe-preview' then protocols[:transcription]
|
|
@@ -6,7 +6,7 @@ module RubyLLM
|
|
|
6
6
|
# Chat implementation for xAI
|
|
7
7
|
# https://docs.x.ai/docs/api-reference#chat-completions
|
|
8
8
|
module Chat
|
|
9
|
-
def
|
|
9
|
+
def render_role(role)
|
|
10
10
|
role.to_s
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -16,8 +16,8 @@ module RubyLLM
|
|
|
16
16
|
)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
Protocols::ChatCompletions::Media.
|
|
19
|
+
def render_content(content, attachments = [])
|
|
20
|
+
Protocols::ChatCompletions::Media.render_content(
|
|
21
21
|
content,
|
|
22
22
|
attachments,
|
|
23
23
|
document_attachments: :none,
|
|
@@ -76,13 +76,18 @@ module RubyLLM
|
|
|
76
76
|
def parse_batch_status(raw_status, completed:)
|
|
77
77
|
return :pending unless completed
|
|
78
78
|
|
|
79
|
-
raw_status
|
|
79
|
+
case raw_status
|
|
80
|
+
when 'failed' then :failed
|
|
81
|
+
when 'cancelled' then :cancelled
|
|
82
|
+
else :succeeded
|
|
83
|
+
end
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
def xai_batch_status(state, completed:)
|
|
83
87
|
return 'failed' unless state['error'].to_s.empty?
|
|
88
|
+
return 'processing' unless completed
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
state['num_cancelled'].to_i.positive? ? 'cancelled' : 'completed'
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def completed_batch_state?(state)
|
|
@@ -96,7 +101,7 @@ module RubyLLM
|
|
|
96
101
|
result.dig('response', 'chat_get_completion')
|
|
97
102
|
|
|
98
103
|
if body
|
|
99
|
-
|
|
104
|
+
parsed_batch_result(index, request_id, body) { parse_completion_body(body, raw: body) }
|
|
100
105
|
else
|
|
101
106
|
[index, nil, batch_failure(request_id, batch_error_message(result))]
|
|
102
107
|
end
|
|
@@ -35,7 +35,7 @@ module RubyLLM
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def image_references(sources)
|
|
38
|
-
|
|
38
|
+
Support::Utils.to_safe_array(sources).filter_map do |source|
|
|
39
39
|
next if blank_attachment?(source)
|
|
40
40
|
|
|
41
41
|
{ type: 'image_url', url: image_reference_url(source) }
|
|
@@ -43,7 +43,7 @@ module RubyLLM
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def image_reference_url(source)
|
|
46
|
-
attachment = Attachment.new(source, config: @config)
|
|
46
|
+
attachment = source.is_a?(Attachment) ? source : Attachment.new(source, config: @config)
|
|
47
47
|
return attachment.provider_file_id if attachment.provider_file?
|
|
48
48
|
return attachment.source.to_s if attachment.url?
|
|
49
49
|
|
|
@@ -68,12 +68,17 @@ module RubyLLM
|
|
|
68
68
|
data: image_data['b64_json'],
|
|
69
69
|
mime_type: image_data['mime_type'] || 'image/png',
|
|
70
70
|
model: model,
|
|
71
|
-
usage: index.zero? ? (data
|
|
71
|
+
usage: index.zero? ? image_usage(data) : {}
|
|
72
72
|
)
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def
|
|
76
|
+
def image_usage(data)
|
|
77
|
+
usage = data['usage'] || {}
|
|
78
|
+
usage.merge('cost' => reported_cost(usage)).compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_paint_inputs!(mask:, **)
|
|
77
82
|
raise Error, 'xAI image editing does not support a mask parameter' if mask
|
|
78
83
|
end
|
|
79
84
|
|
|
@@ -4,8 +4,8 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
class XAI
|
|
6
6
|
# Models metadata for xAI list models. The list endpoint omits the
|
|
7
|
-
#
|
|
8
|
-
# grok-
|
|
7
|
+
# TTS and STT services, so those are appended under the grok-tts and
|
|
8
|
+
# grok-stt names from xAI's model catalog.
|
|
9
9
|
module Models
|
|
10
10
|
module_function
|
|
11
11
|
|
|
@@ -33,6 +33,16 @@ module RubyLLM
|
|
|
33
33
|
SERVER_TOOL_ALIASES
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# xAI answers tool_search and defer_loading with 403 for every
|
|
37
|
+
# account outside its alpha, so Chat brings its own search.
|
|
38
|
+
def tool_search?
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def document_urls?
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
def parse_usage(usage)
|
|
37
47
|
super.merge(reported_cost: reported_cost(usage))
|
|
38
48
|
end
|
|
@@ -9,11 +9,11 @@ module RubyLLM
|
|
|
9
9
|
module Speech
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
-
def speech_url(
|
|
12
|
+
def speech_url(**)
|
|
13
13
|
'tts'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def render_speech_payload(input,
|
|
16
|
+
def render_speech_payload(input, voice:, format:, provider_options: {}, **)
|
|
17
17
|
payload = {
|
|
18
18
|
text: input,
|
|
19
19
|
voice_id: voice,
|